Skip to content

Commit

Permalink
gundotio#56: Implement a view without model
Browse files Browse the repository at this point in the history
- Created a workaround to user a fake model
- updated documentation explaining how to use it
  • Loading branch information
wisersoftwareengineer committed Mar 27, 2023
1 parent 4d82b94 commit 6915503
Show file tree
Hide file tree
Showing 7 changed files with 148 additions and 24 deletions.
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Table of contents
- [UpdateAPI](#updateapi)
- [ActionAPI](#actionapi)
- [DeleteAPI](#deleteapi)
- [WithoutModel](#withoutmodel)
- [Browsable API](#browsable-api)
- [Bundle loading](#bundle-loading)
- [Debugging](#debugging)
Expand Down Expand Up @@ -361,6 +362,35 @@ class BookDetailAPI(DeleteAPI, DetailAPI):
Deletes return a 204 no content response, no serializer is required.


### WithoutModel

If you want to build an API without any model, you just need to extend the `WithoutModel` class
and then one of the classes above - like [ListAPI](#listapi) - or overwrite one of the handles -
like `get` or `post`.

```python

class CustomInfoAPI(WithoutModel, ListAPI):

def get(self, request, *args, **kwargs):
return self.render_to_response(data={"field": value})

```

You can also overwrite the `get_queryset` method and define the `Serializer` class

```python

class CustomInfoAPI(WithoutModel, ListAPI):

serializer = MyCustomSerializer

def get_queryset(self):
return MyModel.objects.filter(...)

```


Browsable API
-------------

Expand Down
70 changes: 62 additions & 8 deletions tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -511,21 +511,75 @@ def test_user_update(client, db, method, user):
assert result["email"] == "[email protected]"


def test_when_using_a_detail_view_without_model_must_return_expected_result(client):
response = client.get("/without-model/detail")
def test_when_getting_details_of_a_view_without_model_must_return_empty_result(
client, db):

response = client.get(f"/without-model/{uuid4()}")
assert response
assert response.status_code == 200, response
assert response.json() == {}


@parametrize("method", ["PATCH", "PUT"])
def test_when_changing_details_of_a_view_without_model_must_return_expected_result(client, db, method):
payload = dict(field="value")
response = client.generic(method, f"/without-model/{uuid4()}", payload)
result = response.json()
assert response.status_code == 200, result
assert result == {}


def test_when_deleting_details_of_a_view_without_model_must_return_expected_result(client, db, task):
response = client.delete(f"/without-model/{task.custom_id}")
assert response.status_code == 204, response.content
assert response.content == b""


def test_when_posting_to_a_view_without_model_must_return_empty_response(client, db):

response = client.post("/without-model/", dict(name="Task Name"))
assert response.status_code == 201
assert response.json() == {}


def test_when_using_a_list_view_without_model_but_with_queryset_must_return_expected_result(
client, db, task):

task_id = task.custom_id
task_name = task.name

response = client.get("/without-model/")
assert response
assert response.status_code == 200, response
assert response.json() == {"field_name": "field_value"}
assert response.json() == {
'pagination': {
'count': 0,
'page': 1,
'pages': 1
},
'data': []
}


def test_when_using_a_list_view_without_model_must_return_expected_result(client):
with pytest.raises(AttributeError):
response = client.get("/without-model/")
response = client.get("/without-model-overriding-handler/")
assert response
assert response.status_code == 200, response
assert response.json() == {"data": [{"field_name": "field_value"}]}


def test_when_using_a_list_view_without_model_with_custom_queryset_must_return_expected_result(
client, db, task):

def test_when_using_a_list_view_without_model_but_with_queryset_must_return_expected_result(client):
with pytest.raises(AttributeError):
response = client.get("/without-model-queryset/")
task_id = task.custom_id
task_name = task.name

response = client.get("/custom-tasks/")
assert response
assert response.status_code == 200, response
assert response.json() == {
"pagination": {"count": 1, "page": 1, "pages": 1 },
"data": [{"id": str(task_id), "name": task_name}]
}


5 changes: 3 additions & 2 deletions tests/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
path("user/", views.UserSelf.as_view()),
path("users/", views.UserList.as_view()),
path("users/<int:id>/", views.UserDetail.as_view()),
path("without-model-overriding-handler/", views.ViewWithoutModelListOverridingHandler.as_view()),
path("without-model/<uuid:task_id>", views.ViewWithoutModelDetail.as_view()),
path("without-model/", views.ViewWithoutModelList.as_view()),
path("without-model/detail", views.ViewWithoutModelDetail.as_view()),
path("without-model-queryset/", views.ViewWithoutModelListWithQuerySet.as_view()),
path("custom-tasks/", views.CustomTaskAPI.as_view()),
]
24 changes: 14 additions & 10 deletions tests/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
from django.db.models import F, Prefetch, Value, Model
from django.db.models.functions import Concat

from tests.models import Profile
from tests.serializers import ProfileSerializer, UserSerializer, DetachedModelSerializer
from tests.models import Profile, Task
from tests.serializers import ProfileSerializer, UserSerializer, DetachedModelSerializer, TaskSerializer
from worf.exceptions import AuthenticationError
from worf.permissions import Authenticated, PublicEndpoint, Staff
from worf.views import ActionAPI, CreateAPI, DeleteAPI, DetailAPI, ListAPI, UpdateAPI
from worf.views import ActionAPI, CreateAPI, DeleteAPI, DetailAPI, ListAPI, UpdateAPI, WithoutModel


class ProfileList(CreateAPI, ListAPI):
Expand Down Expand Up @@ -112,18 +112,22 @@ def get_instance(self):
return self.request.user


class ViewWithoutModelDetail(DetailAPI):
model = None
class ViewWithoutModelDetail(WithoutModel, DeleteAPI, UpdateAPI, DetailAPI):
pass

def get(self, *args, **kwargs):
return self.render_to_response(data={"field_name": "field_value"})

class ViewWithoutModelList(WithoutModel, CreateAPI, ListAPI):
pass


class ViewWithoutModelList(ListAPI):
class ViewWithoutModelListOverridingHandler(WithoutModel, ListAPI):

def get(self, *args, **kwargs):
return self.render_to_response(data={"data": [{"field_name": "field_value"}]})


class ViewWithoutModelListWithQuerySet(ListAPI):
pass
class CustomTaskAPI(WithoutModel, ListAPI):
serializer = TaskSerializer

def get_queryset(self):
return Task.objects.all()
7 changes: 5 additions & 2 deletions worf/assigns.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@


class AssignAttributes:

def get_model_attr(self, key):
return getattr(self.model, key)

def save(self, instance, bundle):
items = [
(key, getattr(self.model, key), value) for key, value in bundle.items()
(key, self.get_model_attr(key), value) for key, value in bundle.items()
]

for key, attr, value in items:
if isinstance(value, models.Model):
setattr(instance, key, value)
Expand Down
2 changes: 1 addition & 1 deletion worf/views/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from worf.views.action import ActionAPI # noqa: F401
from worf.views.base import AbstractBaseAPI, APIResponse # noqa: F401
from worf.views.base import AbstractBaseAPI, APIResponse, WithoutModel, NoModel # noqa: F401
from worf.views.create import CreateAPI # noqa: F401
from worf.views.delete import DeleteAPI # noqa: F401
from worf.views.detail import DetailAPI, DetailUpdateAPI # noqa: F401
Expand Down
34 changes: 33 additions & 1 deletion worf/views/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.cache import never_cache
from django.db import models

from worf.casing import camel_to_snake, snake_to_camel
from worf.conf import settings
Expand All @@ -25,10 +26,41 @@
WorfError,
)
from worf.renderers import render_response
from worf.serializers import SerializeModels
from worf.serializers import SerializeModels, Serializer
from worf.validators import ValidateFields


class NoModel(models.Model):
"""A dummy model to pass through all the code that is deeply coupled with DJango models
TODO remove inheritance with django model to avoid any side effect
"""

def refresh_from_db(self):
warnings.warn("Trying to 'refresh' a detached model")

def delete(self):
warnings.warn("Trying to 'delete' a detached model")


class WithoutModel:

model = NoModel
payload_key = "data"
serializer = Serializer

def validate(self):
warnings.warn("APIs without models have no validation")

def save(self, instance, bundle):
warnings.warn("When using an API without model you MUST implement save method")

def get_queryset(self):
return NoModel.objects.none()

def get_instance(self):
return NoModel()


@method_decorator(never_cache, name="dispatch")
class APIResponse(View):
def __init__(self, *args, **kwargs):
Expand Down

0 comments on commit 6915503

Please sign in to comment.