From 667dbb6b472599a8b9de5e4fc4fba23291bcd129 Mon Sep 17 00:00:00 2001 From: Jeroen van Riel Date: Mon, 26 Jul 2021 11:30:37 +0200 Subject: [PATCH 01/12] Small comment improvement --- binder/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/binder/views.py b/binder/views.py index c12c1760..5ed60abc 100644 --- a/binder/views.py +++ b/binder/views.py @@ -1432,7 +1432,7 @@ def store_m2m_field(obj, field, value, request): if validation_errors: raise sum(validation_errors, None) - # Skip re-fetch and serialization via get_objs if we're in + # Skip re-fetch and serialization via _get_objs if we're in # multi-put (data is discarded!). if getattr(request, '_is_multi_put', False): return None @@ -2073,7 +2073,7 @@ def _multi_put_deletions(self, deletions, new_id_map, request): def multi_put(self, request): - logger.info('ACTIVATING THE MULTI-PUT!!!1!') + logger.info('ACTIVATING THE MULTI-PUT!!!!!') # Hack to communicate to _store() that we're not interested in # the new data (for perf reasons). From 2ed262db4b3f5e7167cdd413e8d61ff3e3246982 Mon Sep 17 00:00:00 2001 From: Jeroen van Riel Date: Mon, 26 Jul 2021 11:43:39 +0200 Subject: [PATCH 02/12] Some cosmetic improvements --- binder/views.py | 45 +++++++-------------------------------------- 1 file changed, 7 insertions(+), 38 deletions(-) diff --git a/binder/views.py b/binder/views.py index 5ed60abc..83f6aea5 100644 --- a/binder/views.py +++ b/binder/views.py @@ -33,6 +33,8 @@ from .models import FieldFilter, BinderModel, ContextAnnotation, OptionalAnnotation, BinderFileField from .json import JsonResponse, jsonloads +logger = logging.getLogger(__name__) + def split_par_aware(content): start = 0 @@ -129,11 +131,6 @@ def annotate(qs, request=None, annotations=None): return qs - -logger = logging.getLogger(__name__) - - - # Used to truncate request bodies. def ellipsize(msg, length=2048): msglen = len(msg) @@ -156,6 +153,7 @@ def sign(num): RelatedModel = namedtuple('RelatedModel', ['fieldname', 'model', 'reverse_fieldname']) FilterDescription = namedtuple('FilterDescription', ['filter', 'need_distinct']) + # Stolen and improved from https://stackoverflow.com/a/30462851 def image_transpose_exif(im): exif_orientation_tag = 0x0112 # contains an integer, 1 through 8 @@ -216,6 +214,7 @@ def prefix_db_expression(value, prefix): raise ValueError('Unknown expression type, cannot apply db prefix: %s', value) + class ModelView(View): # Model this is a view for. Use None for views not tied to a particular model. model = None @@ -440,7 +439,6 @@ def _model_name(cls): return ''.join((x + '_' if x.islower() and y.isupper() else x.lower() for x, y in zip(mn, mn[1:] + 'x'))) - # Use this to instantiate other views you need. It returns a properly initialized view instance. # Call like: foo_view_instance = self.get_view(FooView) def get_view(self, cls): @@ -449,14 +447,12 @@ def get_view(self, cls): return view - # Use this to instantiate the default view for a specific model class. # Call like: foo_view_instance = self.get_model_view(FooModel) def get_model_view(self, model): return self.get_view(self.router.model_view(model)) - # Return a list of RelatedObjects for all _visible_ reverse relations (from both FKs and m2ms). def _get_reverse_relations(self): return [ @@ -578,7 +574,6 @@ def _get_obj(self, pk, request, include_annotations=None): raise self.model.DoesNotExist() - # Split ['animals(name:contains=lion)'] # in ['animals': ['name:contains=lion']] # { 'animals': {'filters': ['name:contains=lion'], 'subrels': {}}} @@ -617,7 +612,7 @@ def _parse_wheres(self, wheres, withs): # Find which objects of which models to include according to for the objects in . # returns three dictionaries: - # - withs: { related_modal_name: [ids] } + # - withs: { related_model_name: [ids] } # - mappings: { with_name: related_model_name } # - related_name_mappings: { with_name: related_model_reverse_key } # @@ -1047,7 +1042,6 @@ def _parse_filter(self, field, value, request, include_annotations, partial=''): return FilterDescription(q, need_distinct) - def _filter_field(self, field_name, qualifier, value, invert, request, include_annotations, partial=''): try: if field_name in self.hidden_fields: @@ -1084,7 +1078,6 @@ def _filter_field(self, field_name, qualifier, value, invert, request, include_a .format(field.__class__.__name__, self.model.__name__, field_name)) - def _parse_order_by(self, queryset, field, request, partial=''): head, *tail = field.split('.') @@ -1114,7 +1107,6 @@ def _parse_order_by(self, queryset, field, request, partial=''): return (queryset, partial + head, nulls_last) - def search(self, queryset, search, request): if not search: return queryset @@ -1156,7 +1148,6 @@ def filter_deleted(self, queryset, pk, deleted, request): raise BinderRequestError('Invalid value: deleted={{{}}}.'.format(request.GET.get('deleted'))) - def _paginate(self, queryset, request): limit = self.limit_default if request.GET.get('limit') == 'none': @@ -1186,12 +1177,10 @@ def _paginate(self, queryset, request): return queryset - def get_queryset(self, request): return self.model.objects.all() - def order_by(self, queryset, request): #### order_by order_bys = list(filter(None, request.GET.get('order_by', '').split(','))) @@ -1348,7 +1337,6 @@ def _sanity_check_meta_results(self, request, response_data): logger.error('Detected anomalous total record count versus data response length. Please check if there are any scopes returning Q() objects which follow one-to-many links!') - def binder_validation_error(self, obj, validation_error, pk=None): model_name = self.get_model_view(obj.__class__)._model_name() @@ -1448,7 +1436,6 @@ def store_m2m_field(obj, field, value, request): return data - # NOTE: This is misnamed because it also stores the reverse side # of OneToOne fields. def _store_m2m_field(self, obj, field, value, request): @@ -1534,15 +1521,12 @@ def _store_m2m_field(self, obj, field, value, request): raise sum(validation_errors, None) - - # Override _store_field example for a "FOO" field # Try to override setters using these methods, if at all possible. # def _store__FOO(self, obj, field, value, request): # return self._store_field(obj, field, value, request) - # Store on . # If the field is a m2m, it should do all validation and then return a list of ids # which will be actually set when the object is known to be saved. @@ -1689,7 +1673,6 @@ def _store_field(self, obj, field, value, request, pk=None): raise BinderInvalidField(self.model.__name__, field) - def _require_model_perm(self, perm_type, request, pk=None): if hasattr(self, 'perms_via'): model = self.perms_via @@ -1706,7 +1689,6 @@ def _require_model_perm(self, perm_type, request, pk=None): logger.debug('passed permission check: {}'.format(perm)) - def _obj_diff(self, old, new, name): if isinstance(old, dict) and isinstance(new, dict): changes = [] @@ -1735,7 +1717,6 @@ def _obj_diff(self, old, new, name): return [] - # Put data and with on one big pile, that's easier for us def _multi_put_parse_request(self, request): body = jsonloads(request.body) @@ -1769,7 +1750,6 @@ def _multi_put_parse_request(self, request): return data, deletions - # Sort object values by model/id def _multi_put_collect_objects(self, data): objects = {} @@ -1874,7 +1854,6 @@ def _multi_put_convert_backref_to_forwardref(self, objects): return objects - def _multi_put_calculate_dependencies(self, objects): logger.info('Resolving dependencies for {} objects'.format(len(objects))) dependencies = {} @@ -1912,7 +1891,6 @@ def _multi_put_calculate_dependencies(self, objects): return dependencies - # Actually sort the objects by dependency (and within dependency layer by model/id) def _multi_put_order_dependencies(self, dependencies): ordered_objects = [] @@ -1940,7 +1918,6 @@ def _multi_put_order_dependencies(self, dependencies): return ordered_objects - def _multi_put_save_objects(self, ordered_objects, objects, request): new_id_map = {} validation_errors = [] @@ -2095,9 +2072,11 @@ def multi_put(self, request): return JsonResponse({'idmap': output}) + def _get_request_values(self, request): return jsonloads(request.body) + def put(self, request, pk=None): if pk is None: return self.multi_put(request) @@ -2137,12 +2116,10 @@ def put(self, request, pk=None): return JsonResponse(data) - def patch(self, request, pk=None): return self.put(request, pk) - def post(self, request, pk=None): self._require_model_perm('add', request) @@ -2167,7 +2144,6 @@ def post(self, request, pk=None): return JsonResponse(data) - def delete(self, request, pk=None, undelete=False, skip_body_check=False): if not undelete: self._require_model_perm('delete', request) @@ -2195,12 +2171,10 @@ def delete(self, request, pk=None, undelete=False, skip_body_check=False): return HttpResponse(status=204) # No content - def delete_obj(self, obj, undelete, request): return self.soft_delete(obj, undelete, request) - def soft_delete(self, obj, undelete, request): # Not only for soft delets, actually handles all deletions try: @@ -2238,7 +2212,6 @@ def soft_delete(self, obj, undelete, request): raise self.binder_validation_error(obj, ve) - def dispatch_file_field(self, request, pk=None, file_field=None): if not request.method in ('GET', 'POST', 'DELETE'): raise BinderMethodNotAllowed() @@ -2417,7 +2390,6 @@ def dispatch_file_field(self, request, pk=None, file_field=None): return JsonResponse( {"data": {file_field_name: None}} ) - def filefield_get_name(self, instance=None, request=None, file_field=None): try: method = getattr(self, 'filefield_get_name_' + file_field.field.name) @@ -2426,7 +2398,6 @@ def filefield_get_name(self, instance=None, request=None, file_field=None): return method(instance=instance, request=request, file_field=file_field) - def view_history(self, request, pk=None, **kwargs): if request.method != 'GET': raise BinderMethodNotAllowed() @@ -2454,7 +2425,6 @@ def api_catchall(request): return e.response(request=request) - def debug_changesets_24h(request): if request.method != 'GET': raise BinderMethodNotAllowed() @@ -2471,7 +2441,6 @@ def debug_changesets_24h(request): return history.view_changesets_debug(request, changesets.order_by('-id')) - def handler500(request): try: request_id = request.request_id From ce7b671133208bda9a36f3fbf1320479e7c68dc0 Mon Sep 17 00:00:00 2001 From: Jeroen van Riel Date: Tue, 24 Aug 2021 11:39:02 +0200 Subject: [PATCH 03/12] Add compound document documentation (how to use with query parameter) --- docs/api.md | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/docs/api.md b/docs/api.md index 31b0cbf1..87215cd5 100644 --- a/docs/api.md +++ b/docs/api.md @@ -50,7 +50,6 @@ To use a partial case-insensitive match, you can use `api/animal?.name:icontains Note that currently, it is not possible to search on many-to-many fields. #### More advanced searching - Sometimes you want to search on multiple fields at once. ```python @@ -66,6 +65,73 @@ Ordering is a simple matter of enumerating the fields in the `order_by` query pa The default sort order is ascending. If you want to sort in descending order, simply prefix the attribute name with a minus sign. This honors the scoping, so `api/animal?order_by=-name,id` will sort by `name` in descending order and by `id` in ascending order. +### Fetching related resources (aka compound documents) +When fetching an object that has relations, it is very convenient to receive these related models in the same response. Related resources may be requested by specifying a list of model types in the `with` query parameter using "dotted relationship" notation. Consider the following example: an animal belongs to at most one zoo, which may have multiple contact persons. Suppose that we want to list all animals with their zoo and all contact persons for each zoo, then we would make the request + +`api/animal/?with=zoo.contacts` + +which produces the following response (some fields are left out for clarity) + +```json +{ + "data": [ + { + "name": "Scooby Doo", + "id": 1, + "zoo": 1, + + ... + + } + ], + "with": { + "zoo": [ + { + "name": "Dierentuin", + "id": 1, + "contacts": [ + 1 + ], + + ... + + } + ], + "contact_person": [ + { + "name": "Tom", + "id": 1, + "zoos": [ + 1 + ], + + ... + + } + ] + }, + "with_mapping": { + "zoo": "zoo", + "zoo.contacts": "contact_person" + }, + "with_related_name_mapping": { + "zoo": "animals", + "zoo.contacts": "zoos" + }, + + ... + +} +``` + +We note that there is currently only one animal (Scooby Doo) in our database. The `with` clause includes a list of related objects per related object type (`related_model_name`), which includes the zoo that the animal belongs to and a list of contact persons that belong to that zoo. The translation between the dotted relationship and the actual name of the related model is given in the `with_mapping` entry. The `with_related_name_mapping` entry gives backwards relationship name (`related_name`) that belongs to the last part of each dotted relationship. So for example, we see that in `zoo.contacts`, the `related_name` for the last `contacts` many-to-many relation is `zoos`. To summarize: + +- `withs: { related model name: [ids] }` +- `mappings: { dotted relationship: related model name }` +- `related_name_mappings: { dotted relationship: related model reverse key }` + +Note that the `with` query parameter is heavily used by [mobx-spine](https://github.com/CodeYellowBV/mobx-spine). For some more background we refer to the `json-api` specification of [Compound Documents](https://jsonapi.org/format/#document-compound-documents), which is related to our implementation. + ### Saving a model From b702bd0dc9c4c214fea7c26398f82d8e8e2130b5 Mon Sep 17 00:00:00 2001 From: Jeroen van Riel Date: Tue, 24 Aug 2021 11:50:01 +0200 Subject: [PATCH 04/12] Extend example models --- docs/api.md | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/docs/api.md b/docs/api.md index 87215cd5..4dfe5289 100644 --- a/docs/api.md +++ b/docs/api.md @@ -2,9 +2,9 @@ Binder automatically exposes a fairly powerful API for all your models. -## Registering an API endpoint +## Registering API endpoints -We’ll use this example model, added in `models.py`. +In order to illustrate the API, we’ll use the following minimal set of models (similar to the models found in the test application): ```python from binder.models import BinderModel @@ -12,33 +12,41 @@ from django.db import models class Animal(BinderModel): name = models.TextField() + zoo = models.ForeignKey('Zoo', on_delete=models.CASCADE, related_name='animals', blank=True, null=True) + +class Zoo(BinderModel): + name = models.TextField() + contacts = models.ManyToManyField('ContactPerson', blank=True, related_name='zoos') + +class ContactPerson(BinderModel): + name = models.CharField(unique=True, max_length=50) ``` -In `views.py`, add the following: +Each model is registered as a separate API endpoint by defining a `ModelView` for it: ```python from binder.views import ModelView - from .models import Animal - class AnimalView(ModelView): model = Animal -``` -And that’s it! - -## Using an API endpoint +class ZooView(ModelView): + model = Zoo + +class ContactPersonView(ModelView): + model = ContactPerson +``` -After registering the model, a couple of new routes are at your disposal: +After registering the models, a couple of routes is immediately available for each of them: - `GET api/animal/` - view collection of models - `GET api/animal/[id]/` - view a specific model - `POST api/animal/` - create a new model -- `PUT api/animal/` - create or update (nested) models - `PUT api/animal/[id]/` - update a specific model +- `PUT api/animal/` - create, update or delete multiple models at once ("Multi PUT") - `DELETE api/animal/[id]/` - delete a specific model -- `POST api/animal/[id]/` - undelete a specific model +- `POST api/animal/[id]/` - undelete a specific "soft-deleted" (see below) model ### Filtering on the collection From 67dbf5661f6633f8c092f47c55af20a13c0c13f0 Mon Sep 17 00:00:00 2001 From: Jeroen van Riel Date: Tue, 24 Aug 2021 11:58:02 +0200 Subject: [PATCH 05/12] Improve Multi PUT docs and add examples for relation fields --- docs/api.md | 82 +++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 70 insertions(+), 12 deletions(-) diff --git a/docs/api.md b/docs/api.md index 4dfe5289..58efdf6b 100644 --- a/docs/api.md +++ b/docs/api.md @@ -48,6 +48,8 @@ After registering the models, a couple of routes is immediately available for ea - `DELETE api/animal/[id]/` - delete a specific model - `POST api/animal/[id]/` - undelete a specific "soft-deleted" (see below) model +## Viewing data + ### Filtering on the collection #### Simple field filtering @@ -141,9 +143,10 @@ We note that there is currently only one animal (Scooby Doo) in our database. T Note that the `with` query parameter is heavily used by [mobx-spine](https://github.com/CodeYellowBV/mobx-spine). For some more background we refer to the `json-api` specification of [Compound Documents](https://jsonapi.org/format/#document-compound-documents), which is related to our implementation. -### Saving a model +## Writing data -Creating a new model is possible with `POST api/animal/`, and updating a model with `PUT api/animal/`. Both requests accept a JSON body, like this: +### Creating and updating a single object +Creating a new object is possible with `POST api/animal/`, and updating an object with `PUT api/animal/[id]`. Both requests accept a JSON body, like this: ```json { @@ -163,7 +166,7 @@ If the request succeeds, it will return a `200` response, with a JSON body: } ``` -If you leave the `name` field blank, and `blank=True` is not set on the field, this will result in a response with status `400`; +If the request did not pass validation, errors are included in the response, grouped by field name. For example, if you leave the `name` field blank, and `blank=True` is not set on the field, this will result in a response with status `400`: ```json { @@ -180,17 +183,16 @@ If you leave the `name` field blank, and `blank=True` is not set on the field, t } ``` -#### Multi PUT - -For models with relations, you often don't want to make a separate request to save each model. Multi PUT makes it easy to save related models in one request. +### Creating and updating multiple objects using Multi PUT +Instead of having to make separate requests for each model type, it is common practice to group operations on a bunch of (possibly related) objects together in a single request. For some additional background on this technique, we refer to the `json-api` specification of [Atomic Operations](https://jsonapi.org/ext/atomic), to which our specification of Multi PUT is somewhat related. -Imagine that the `Animal` model from above is linked to a `Zoo` model; +Remember that the `Animal` model that we defined is linked to the `Zoo` model by: ```python -zoo = models.ForeignKey(Zoo, on_delete=models.CASCADE, related_name='+') +zoo = models.ForeignKey('Zoo', on_delete=models.CASCADE, related_name='animals', blank=True, null=True) ``` -Now you can create a new animal and zoo in one request to `PUT api/animal/`; +Now you can create objects for a zoo housing two animals in one request to `PUT api/animal/`: ```json { @@ -212,11 +214,11 @@ Now you can create a new animal and zoo in one request to `PUT api/animal/`; } ``` -The negative `id` indicates that it is made up. Because those models are not created yet, they don't have an `id`. By using a "fake" `id`, it is possible to reference a model in another model. +The negative `id` indicates that it is made up. Because those objects are not created yet, they don't have an `id`. By using a "fake" `id`, it is possible to reference an object in another object. The fake `id` has to be unique per model type. So you can use `-1` once for `Animal`, and once for `Zoo`. The backend does not care what number you use exactly, as long as it is negative. -If this request succeeds, you'll get back a mapping of the fake ids and the real ones; +If this request succeeds, you'll get back a mapping from the fake ids to the real ones: ```json { @@ -233,7 +235,63 @@ If this request succeeds, you'll get back a mapping of the fake ids and the real } ``` -It is also possible to update existing models with multi PUT. If you use a "real" id instead of a fake one, the model will be updated instead of created. +It is also possible to update existing models with Multi PUT. If you use a "real" id instead of a fake one, the model will be updated instead of created. + +It is also possible to delete existing models with Multi PUT by providing a `deletions` list containing the ids of the models that need to be deleted. It is also possible to remove related models by specifying a list of ids for each related model type in the `with_deletions` dictionary. For example, we may make the following request to the endpoint for `animal`, which deletes three animals, one zoo and two care takers: + +```json +{ + "deletions": [ + 1, + 2, + 3 + ], + "with_deletions": { + "zoo": [ + 1, + ], + "care_taker": [ + 2, + 4 + ] + } +} +``` + + +### Updating relationship fields +Updating relations is rather straightforward either using a direct PUT request or a Multi PUT request. Set the foreign key field to the id of the object you want to link to or include the id in the list in case of a reverse foreign key or many-to-many relation. When leaving out an id in the list of related object ids, you are effectively "unlinking" the object, which also triggers the action as defined by the `on_delete` setting (CASCADE, PROTECT, SET_NULL). + +We will now shortly illustrate how to update foreign keys (one-to-many) in both directions and many-to-many relations. Suppose that we have the following objects and relationships: two animals (1 and 2), one zoo (1) that houses both animals and two contact persons (1 and 2) that are both affiliated to the zoo. The following API calls may be made using curl, see [Test Application](test-app.md). You may verify their effects using the Django admin panels. + +**Forward foreign key.** +Let's unlink Animal 2 from the zoo: +```json +PUT api/animal/2/ +{ "zoo": null } +``` + +**Reverse foreign key.** +Now let's add Animal 2 back to the zoo and at the same time unlink Animal 1 by updating the `related_name` field on the zoo: +```json +PUT api/zoo/1/ +{ "animals": [ 2 ] } +``` + +**M2M zoos.** +Let's unlink Contact Person 1 from the zoo: +```json +PUT api/contact_person/1/ +{ "zoos": [ ] } +``` + +**M2M contacts.** +Let's now swap Contact Person 1 back in and remove Contact Person 2 from the zoo: +```json +PUT api/zoo/1/ +{ "contacts": [ 2 ] } +``` + ### Uploading files From 5b25b5ad1a0b25f8f0c8bc7628d5e8c445ad956b Mon Sep 17 00:00:00 2001 From: Jeroen van Riel Date: Tue, 24 Aug 2021 12:00:23 +0200 Subject: [PATCH 06/12] Cosmetic improvement --- docs/api.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/api.md b/docs/api.md index 58efdf6b..e6b1806f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -307,9 +307,9 @@ Then, to upload the file, do a `POST api////` with t To retrieve the file, do `GET api////` -TODO: +**TODO** - permissions --- change permission model + - change permission model ## Hacking the API @@ -409,7 +409,7 @@ class FooView(BaseView): -TODO: +**TODO** - how to add custom saving logic - how to add custom viewing logic From fae5c3c2bfb7d75a149e5787395c01a794c6add9 Mon Sep 17 00:00:00 2001 From: Jeroen van Riel Date: Tue, 24 Aug 2021 12:02:56 +0200 Subject: [PATCH 07/12] Containerize Test App Add Docker setup for the test application and add some hints on how to explore the demo and make api calls. --- README.md | 9 +++++++++ docs/api.md | 2 +- docs/images/django-admin.png | Bin 0 -> 55223 bytes docs/test-app.md | 20 ++++++++++++++++++++ docs/test_app.md | 18 ------------------ project/Dockerfile | 6 ++++++ project/docker-compose.yml | 22 ++++++++++++++++++++++ project/project/settings.py | 10 +++++++--- 8 files changed, 65 insertions(+), 22 deletions(-) create mode 100644 docs/images/django-admin.png create mode 100644 docs/test-app.md delete mode 100644 docs/test_app.md create mode 100644 project/Dockerfile create mode 100644 project/docker-compose.yml diff --git a/README.md b/README.md index da9d2020..ec371a2b 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,15 @@ Code Yellow backend framework for SPA webapps with REST-like API. **This framework is a work-in-progress. There is no complete documentation yet. We are using it for a couple of projects and fine-tuning it.** +## Playing around with the test application + +The `project/` folder contains a test application that shows basic usage of most features. You may play around with it by starting a backend and database service using +``` +docker-compose up +``` +Once the containers have started, you may point your browser at `localhost:8000`, which exposes the Django admin panel at `/admin/` (which you may use to login in) and the Binder api at `/api/`. See [Test Application](docs/test-app.md) for more information. + + ## Running the tests There are two ways to run the tests: diff --git a/docs/api.md b/docs/api.md index e6b1806f..eeaf3271 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,6 +1,6 @@ # API -Binder automatically exposes a fairly powerful API for all your models. +Binder automatically exposes a fairly powerful API for all registered models. You may want to use the [test application](./test-app.md) for trying out the below described features. ## Registering API endpoints diff --git a/docs/images/django-admin.png b/docs/images/django-admin.png new file mode 100644 index 0000000000000000000000000000000000000000..716c35020af717ff3219e5b81bbab1de49a0817c GIT binary patch literal 55223 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%0As=PtP$oA!h#*cXc33XgY)noTu+=)@fMaas6u7 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 C^G+rJ literal 0 HcmV?d00001 diff --git a/docs/test-app.md b/docs/test-app.md new file mode 100644 index 00000000..89919c3f --- /dev/null +++ b/docs/test-app.md @@ -0,0 +1,20 @@ +# Test Application + +In order to test the implementation and to provide a minimal working example on how to use Binder, a demo application for administring a zoo is included in the `project/` folder. Note that this folder contains a symlink to the `testapp` folder that is used for unit testing Binder. + +Start the application by running `docker-compose up` from the `project/` folder. The default Django admin panel is enabled, so you can easily create models using the web interface, see image below. However, feel free to dive into the terminal and use `curl`, see below. + +![admin interface in test application](images/django-admin.png) + +You may need to create a superuser using the Django's command line utility, which is accessible by executing + +`docker-compose exec web ./manage.py createsuperuser` + +from the `project/` directory. + +## Using `curl` +You can hit the api directly using `curl`. A simple PUT request can for example be made using + +`curl -b 'sessionid=' -d @animal.json -X PUT localhost:8000/api/animal/1/` + +Log in to the Django admin via a browser to obtain the `sessionid` cookie, which you may copy from the developer tools. You may want to use the flag `-s` ('silent') to hide the progress bar of `curl`. diff --git a/docs/test_app.md b/docs/test_app.md deleted file mode 100644 index c236f152..00000000 --- a/docs/test_app.md +++ /dev/null @@ -1,18 +0,0 @@ -# Using the test project - -Binder comes with a test-project that allows developers to play with -the API, try things out, etc. To get it running: - - - `cd django-binder` - - Ensure you have a virtualenv (`virtualenv --python=python3 venv`). - - Ensure the dependencies are installed (`pip install -Ur project/packages.pip`, note `psycopg2`!). - - `cd project` - - Create a postgres DB for the project (`createdb binder`) - - The testapp doesn't have migrations, you'll need to make them (`./manage.py makemigrations testapp`) - - And apply them (`./manage.py migrate`) - - You'll need a user (`./manage.py createsuperuser`) - - And then you can play with the project (`./manage.py runserver localhost:8010`) - -A note about migrations: when the models in `tests/testapp` have changed, you are responsible for making and applying the migrations. -We don't commit the migrations to the repo. -If you're in a mess, `dropdb binder; createdb binder` and recreate the migrations. diff --git a/project/Dockerfile b/project/Dockerfile new file mode 100644 index 00000000..309772d5 --- /dev/null +++ b/project/Dockerfile @@ -0,0 +1,6 @@ +FROM python:3 +ENV PYTHONUNBUFFERED 1 +WORKDIR /code +COPY packages.pip /code/ +RUN pip install -r packages.pip +COPY . /code/ diff --git a/project/docker-compose.yml b/project/docker-compose.yml new file mode 100644 index 00000000..f2156f8f --- /dev/null +++ b/project/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3' + +services: + + db: + image: postgres:11.5 + environment: + - POSTGRES_DB=postgres + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + + web: + build: . + command: python manage.py runserver 0.0.0.0:8000 + volumes: + - .:/code + - ./binder:/code/binder + - ./testapp:/code/testapp + ports: + - "8000:8000" + depends_on: + - db diff --git a/project/project/settings.py b/project/project/settings.py index c9e56f70..b0888f1d 100644 --- a/project/project/settings.py +++ b/project/project/settings.py @@ -83,9 +83,13 @@ DATABASES = { 'default': { - 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'NAME': 'binder', - } + 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'NAME': 'postgres', + 'USER': 'postgres', + 'PASSWORD': 'postgres', + 'HOST': 'db', + 'PORT': 5432, +} } From 15baf49d98d82fac67e5b0623593de52c6dc501f Mon Sep 17 00:00:00 2001 From: Jeroen van Riel Date: Tue, 24 Aug 2021 12:09:54 +0200 Subject: [PATCH 08/12] Add link to docs from README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ec371a2b..0bf9c7b6 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Build Status](https://travis-ci.org/CodeYellowBV/django-binder.svg?branch=master)](https://travis-ci.org/CodeYellowBV/django-binder) [![codecov](https://codecov.io/gh/CodeYellowBV/django-binder/branch/master/graph/badge.svg)](https://codecov.io/gh/CodeYellowBV/django-binder) -Code Yellow backend framework for SPA webapps with REST-like API. +Code Yellow backend framework for SPA webapps with REST-like API. Dive into the [documentation](docs/api.md) to get started. **This framework is a work-in-progress. There is no complete documentation yet. We are using it for a couple of projects and fine-tuning it.** From 956aca9fc181f3749e1cb6e5183c828bdfa8316d Mon Sep 17 00:00:00 2001 From: Jeroen van Riel Date: Tue, 24 Aug 2021 12:10:59 +0200 Subject: [PATCH 09/12] Update api.md --- docs/api.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/api.md b/docs/api.md index eeaf3271..f0f5e583 100644 --- a/docs/api.md +++ b/docs/api.md @@ -90,7 +90,7 @@ which produces the following response (some fields are left out for clarity) "id": 1, "zoo": 1, - ... + // ... } ], @@ -103,7 +103,7 @@ which produces the following response (some fields are left out for clarity) 1 ], - ... + // ... } ], @@ -115,7 +115,7 @@ which produces the following response (some fields are left out for clarity) 1 ], - ... + // ... } ] @@ -129,7 +129,7 @@ which produces the following response (some fields are left out for clarity) "zoo.contacts": "zoos" }, - ... + // ... } ``` From ae8d7ed06ee0b19e8ab0ce2449a3d8bcfeba3465 Mon Sep 17 00:00:00 2001 From: Jeroen van Riel Date: Tue, 24 Aug 2021 12:16:54 +0200 Subject: [PATCH 10/12] Update api.md --- docs/api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api.md b/docs/api.md index f0f5e583..6b6b3b1b 100644 --- a/docs/api.md +++ b/docs/api.md @@ -82,7 +82,7 @@ When fetching an object that has relations, it is very convenient to receive the which produces the following response (some fields are left out for clarity) -```json +```json5 { "data": [ { From e0e0a70bd0c22a812ca76b666e1a980a0d427197 Mon Sep 17 00:00:00 2001 From: Jeroen van Riel Date: Tue, 24 Aug 2021 12:19:25 +0200 Subject: [PATCH 11/12] Update api.md --- docs/api.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/api.md b/docs/api.md index 6b6b3b1b..ad3038d7 100644 --- a/docs/api.md +++ b/docs/api.md @@ -90,7 +90,7 @@ which produces the following response (some fields are left out for clarity) "id": 1, "zoo": 1, - // ... + // ... } ], @@ -103,7 +103,7 @@ which produces the following response (some fields are left out for clarity) 1 ], - // ... + // ... } ], @@ -115,7 +115,7 @@ which produces the following response (some fields are left out for clarity) 1 ], - // ... + // ... } ] From 227496f09cdb113fe7452fda102b3f937a79f760 Mon Sep 17 00:00:00 2001 From: Jeroen van Riel Date: Tue, 24 Aug 2021 12:24:02 +0200 Subject: [PATCH 12/12] Small doc fixes --- docs/permissions.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/permissions.md b/docs/permissions.md index 63bc6bf0..84e7342b 100644 --- a/docs/permissions.md +++ b/docs/permissions.md @@ -5,11 +5,11 @@ To install and use the permission system do the following: - First create a file `permissions.py` in the main package. This will define the permissions in the system - Add the following content to the `permissions.py` -``` +```python permissions = { 'default': [ # Default permissions everybody has ('auth.view_user', 'own'), - ('auth.unmasquerade_user', None), # If you are masquarade, the user must be able to unmasquarade + ('auth.unmasquerade_user', None), # If you are masquerade, the user must be able to unmasquerade ('auth.login_user', None), ('auth.logout_user', None), ], @@ -17,7 +17,7 @@ permissions = { ``` This will allow everybody to login, logout, unmasquerade, and view their own user account. - Add the following to settings.py -``` +```python from .permissions import permissions BINDER_PERMISSION = permissions ```