Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve docs #185

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,19 @@
[![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.**

## 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:
Expand Down
49 changes: 9 additions & 40 deletions binder/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -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 [
Expand Down Expand Up @@ -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': {}}}
Expand Down Expand Up @@ -617,7 +612,7 @@ def _parse_wheres(self, wheres, withs):

# Find which objects of which models to include according to <withs> for the objects in <queryset>.
# 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 }
#
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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('.')

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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':
Expand Down Expand Up @@ -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(',')))
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -1432,7 +1420,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
Expand All @@ -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):
Expand Down Expand Up @@ -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 <value> on <obj>.<field>
# 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.
Expand Down Expand Up @@ -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
Expand All @@ -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 = []
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 = {}
Expand Down Expand Up @@ -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 = {}
Expand Down Expand Up @@ -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 = []
Expand Down Expand Up @@ -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 = []
Expand Down Expand Up @@ -2073,7 +2050,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).
Expand All @@ -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)
Expand Down Expand Up @@ -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)

Expand All @@ -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)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand All @@ -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()
Expand Down Expand Up @@ -2454,7 +2425,6 @@ def api_catchall(request):
return e.response(request=request)



def debug_changesets_24h(request):
if request.method != 'GET':
raise BinderMethodNotAllowed()
Expand All @@ -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
Expand Down
Loading