django ponyjs documentation - read the docs · django ponyjs documentation, release 0.2.0...

21
Django PonyJS Documentation Release 0.2.0 Sergei Maertens <[email protected]> October 04, 2016

Upload: others

Post on 23-May-2020

110 views

Category:

Documents


0 download

TRANSCRIPT

Django PonyJS DocumentationRelease 0.2.0

Sergei Maertens <[email protected]>

October 04, 2016

Contents

1 What’s PonyJS? 3

2 Intended usage/public 52.1 Getting started . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52.2 RESTful API client . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72.3 Formsets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132.4 Related Django packages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16

3 Contact 173.1 Indices and tables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17

i

ii

Django PonyJS Documentation, Release 0.2.0

A Javascript framework for Django.

Contents 1

Django PonyJS Documentation, Release 0.2.0

2 Contents

CHAPTER 1

What’s PonyJS?

PonyJS is a Javascript library for Django developers. It provides a set of classes/utilities to write DRY (don’t repeatyourself) Javascript, without having to write those abstractions yourself.

Currently, PonyJS offers two ‘modules’:

• A RESTful API client with ORM syntax (mostly read only at the moment)

• A class to deal with formsets.

See Getting started for installation instructions.

3

Django PonyJS Documentation, Release 0.2.0

4 Chapter 1. What’s PonyJS?

CHAPTER 2

Intended usage/public

Django PonyJS is not meant to replace single-page-application (SPA) frameworks like AngularJS, EmberJS, Back-bone.js or maybe even ReactJS, if that counts.

It is aimed at developers who use Django views and templates for the major part, and enhance certain pages/apps withclient-side code. It’s intended to be a custom approach where a full blown SPA framework would be too much.

Note: Django PonyJS is under heavy development. This means that this documentation may lag behind and that the(private) API might change without notice. However, we’ll do our best to keep a changelog for early adopters.

Contents:

2.1 Getting started

Django PonyJS is written entirely in EcmaScript 6 (ES6, also known as EcmaScript 2015). This is the latest acceptedspec for EcmaScript/Javascript and soon-to-be-available in all major browsers.

ES6 has some real improvements, such as a real module system with proper scoping in a syntactically pleasant way.The imports almost look like Python imports, through arrow functions the scope of this is preserved, ‘native’ classes,native Promises...

ES7 adds even more interesting features, like decorators!

The code is transpiled to ES5, through a transpiler like Babel or Traceur. This transpiler makes sure all new featuresthat are not yet natively available in the browser continue to work, by converting the ES6 code to ES5 code.

Note: Babel is officially supported. Traceur may work, but is currently untested.

To handle all this new and fancy Javascript, SystemJS is used. It’s a Javascript module loader that understandsdifferent formats: ES6 imports, CommonJS and AMD. This means you can use most libraries that exist in the wild,even if they come from a different ecosystem.

jspm is a great package manager built on top of SystemJS for the browser. Both are written by Guy Bedford, who’sbeen putting in tons of effort with very little gains, at least check out his work!

5

Django PonyJS Documentation, Release 0.2.0

2.1.1 Dependencies

NodeJS and npm

On OS-level you’ll need nodejs and npm, node’s package manager, which should come with nodejs itself.

NPM is used to install jspm and friends.

apt-get install nodejs

or for other operating systems:

yum install nodejsbrew install nodepacman -S nodejs npm

See the NodeJS github for more documentation.

JSPM

Next, you’ll need to install jspm.

If it’s the first time installing, it can be installed globally. Later, the CLI will run against your version-pinned localjspm install.

npm install -g jspm # -g flag for global, sudo may be required

Installing globally usually ensures that jspm is available in your $PATH.

jspm uses Node package.json. If you don’t have one yet, you can create one by running:

npm init # and follow the prompts.

You can now install a local version of jspm, which is recommended to pin your dependency versions - this helpsavoiding surprises.

npm install jspm

It’s now time to initialize your jspm project. This is an interactive prompt again, but we’ll need to deviate from thedefaults a bit to make it play nice with Django.

jspm init

Would you like jspm to prefix the jspm package.json properties under jspm? [yes]: yes # easier to keep track of jspm-specific settings/packages

Enter server baseURL (public folder path) [/]: static # same as settings.STATIC_ROOT, relative to package.json

Enter jspm packages folder [static/jspm_packages]: # keep it within settings.STATIC_ROOT

Enter config file path [static/config.js]: my-project/static/config.js # must be kept in version control, so somewhere where collectstatic can find it

Enter client baseURL (public folder URL) [/]: /static/ # set to settings.STATIC_URL

Do you wish to use a transpiler? [yes]: # current browsers don't have full support for ES6 yet

Which ES6 transpiler would you like to use, Traceur or Babel? [traceur]: babel # better tracebacks

Take some time to read the JSPM docs if you’re not familiar with it yet.

6 Chapter 2. Intended usage/public

Django PonyJS Documentation, Release 0.2.0

Note: A few settings are remarkable. We placed jspm_packages in settings.STATIC_ROOT. This meansthat collectstatic will not post-process the files in here, which can be a problem. Django SystemJS handles this specificuse case as it is intended for jspm-users. There is an inherent limitation within JSPM which should be lifted with the0.18 release.

2.1.2 Installing Django PonyJS

jspm has its own registry which fetches packages from npm and github by default. PonyJS will always be releasedon github, and github only for the time being. It doesn’t make sense to publish on NPM as it’s browser-only.

Install a pinned version of PonyJS by running:

jspm install ponyjs=github:sergei-maertens/ponyjs@^0.0.4

This installs the library under the ponyjs alias, which makes imports more convenient. You can change the alias toyour liking.

Example usage in your own code:

import $ from 'jquery';import { Model } from 'ponyjs/models.js';

let Pizza = Model('Pizza');

$('#my-button').on('click', function(event) {event.preventDefault();

Pizza.objects.filter({name__startswith: 'diablo'}).then(pizzas => {// do something with pizzas, which is a list of Pizza instances.

});

return false;});

For more examples, be sure to check the rest of the documentation.

2.2 RESTful API client

The biggest feature of this library (so far) is a REST client for your backend.

After declaring your models in Javascript (with minimal code), it’s possible to make asynchronous requests to yourbackend REST API with an ORM-like syntax. No need to manually fiddle around with URLs and $.ajax, that’swhat PonyJS takes care of.

Of course, this does need a little bit of initial setup/configuration, which is outlined in three separate documents.

2.2.1 Configuring the API endpoint(s)

To set-up the API communication, your backend needs to be configured. A single json-file is expected in yoursettings.STATIC_ROOT: conf/api.json. This file is similar to settings.DATABASES, where a mini-mum of one backend configuration is expected with the default key.

2.2. RESTful API client 7

Django PonyJS Documentation, Release 0.2.0

Note: It is assumed that you have your backend API set up correctly and the endpoints exist. For a sample configu-ration check Django REST framework documentation.

Example of myproject/myproject/static/conf/api.json:

{"default": {"baseUrl": "http://example.com","basePath": "api/[version]","options": {

"version": "v1",},"csrfCookie": "csrftoken","csrfHeader": "X-CSRFToken",

}}

Currently, only the version token is supported. All tokens between square brackets are optional. Another - simpler- example is:

{"default": {"baseUrl": "http://api.example.com"

}}

CSRF

Django’s suggested solution for AJAX and CSRF handling is included in a DRY way. By default PonyJS looks at thecsrftoken cookie and sends the X-CSRFToken header for POST, PUT and DELETE requests. These cookie andheader names can be configured per API with the csrfCookie and csrfHeader keys.

Multiple APIs

If you have multiple APIs to talk to, you have to configure these as well:

{"default": {"baseUrl": "http://api.example.com"

},"external": {"baseUrl": "http://other-api.example.com"

}}

This alias can then be used in querysets:

Pizza.objects.using('external').all()

Note: Authentication options are not possible yet. Token auth / oauth will be implemented in a later stage.

8 Chapter 2. Intended usage/public

Django PonyJS Documentation, Release 0.2.0

2.2.2 Defining models

You already have your models defined on the server-side, so we try to not violate the DRY principle on the client-side.Models are your layer to access the API, and as such the backend API is considered to be the Single-Source-of-Truth.

A model needs just enough information to be able to communicate with the API.

Note: Unfortunately, Javascript doesn’t have true multiple inheritance and/or meta classes, so the model-definitioncan seem a bit awkward.

import { Model } from 'ponyjs/models.js';

// Provide the model name + declaration of fields/meta/managers// If `model_name` is omitted, lower case representation of the model's name is usedclass Pizza extends Model('Pizza', {

Meta: {app_label: 'pizzas',model_name: 'pizza'

}});

Dealing with relations

Dealing with related fields based on the API output is hard, because there’s a lot of variance in possible outputs. Thedjango-rest-framework RelatedField classes were used for a reference implementation.

It’s your job as a developer to specify what kind of data is expected from the API for related fields.

Consider a simple Book-Author relation, where Book has a ForeignKey to Author. An example response for a book-detail could be:

# GET /api/v1/books/book/42/{

id: 42,title: 'The meaning of life',author: 7

}

In this particular case, the serializer outputs the primary key of the related object for the actual field. So, to accessbook.author in Javascript, an extra API request needs to be made to retrieve that author:

# GET /api/v1/author/7/{

id: 7,first_name: 'Oscar',last_name: 'Wilde'

}

However, there are other possible outputs, like nested objects:

# GET /api/v1/books/book/42/{

id: 42,title: 'The meaning of life',author: {id: 7,first_name: 'Oscar',

2.2. RESTful API client 9

Django PonyJS Documentation, Release 0.2.0

last_name: 'Wilde'}

}

So, to be able to differentiate between the possible outputs, explicit relation-field setup is required:

import { Model, NestedRelatedField, PrimaryKeyRelatedField } from 'ponyjs/models.js';

let Author = Model('Author');

let Book = Model('Book', {author: NestedRelatedField(Author),Meta: {

app_label: 'books',}

});

let Book2 = Model('Book', {author: PrimaryKeyRelatedField(Author),Meta: {

app_label: 'books',}

});

Then, when you retrieve the actual book instance, you can access the related field through a promise, and you will getan actual Author instance back:

// GET /api/v1/books/book/42/let book = Book.objects.get({id: 42});book.author.then(author => {

console.log(author instanceof Author);// true

});

Note: Due to the asynchronous nature of Javascript, promises must be used to access related fields because theycan potentially send out extra network requests (for example with PrimaryKeyRelatedFields). If no extra requests areneeded, the promise resolves instantly.

Endpoint configuration

By default, endpoints will be built in the form {baseUrl}/app_label/model_name. If no app_label wasprovided in the model definition, it will be left out.

The auto-generated list endpoint for Pizza would be http://example.com/api/v1/pizzas/pizza/,while the detail endpoint would be http://example.com/api/v1/pizzas/pizza/:id/. Each:key is interpolated with the object itself, so a Pizza instance new Pizza({id: 10}) would resolve tohttp://example.com/api/v1/pizzas/pizza/10/.

These auto-discovered endpoints can of course be specified manually:

class Pizza extends Model('Pizza', {Meta: {

app_label: 'pizzas',endpoints: {

list: 'my_pizzas/p/',detail: 'my_pizzas/p/:slug/'

10 Chapter 2. Intended usage/public

Django PonyJS Documentation, Release 0.2.0

}}

});

The list url would then become http://example.com/api/v1/my_pizzas/p/ and detail becomeshttp://example.com/api/v1/my_pizzas/p/:slug/.

Define model against the non-default api

It’s also possible to specify an alternative API for a model:

import { Manager } from 'ponyjs/models/manager.js';

class Pizza extends Model('Pizza', {

objects: new Manager('external'),

Meta: {app_label: 'pizzas'

}});

This configures the default manager (objects) to talk to the alternative url.

2.2.3 Usage

Retrieving data from the API

This process is similar to how Django works, but is then Promisified to deal with the async nature of HTTPrequests.

To retrieve all objects (possibly paginated) from your endpoint, you build a queryset instance:

let queryset = Pizza.objects.all();

This queryset is lazy in the sense that you can operate on it, without making the HTTP request until you call then onit, which evaluates the queryset and turns it into an asynchronous request.

This means that you can modify the queryset how you like:

let queryset = Pizza.objects.all();queryset = queryset.filter({foo: 'bar'}).filter({foo: 'baz'});

Each queryset method returns a modified copy of the queryset, leaving the initial form intact (so you can build base-querysets for example). The arguments to filter are turned into GET parameters, and specifying the same parameterwill append it.

The above example would make a GET request to the url /pizzas/pizza/?foo=bar&foo=baz.

QuerySet.all and QuerySet.filter make list-calls and will send GET requests toModel._meta.endpoints.list.

There is also QuerySet.get, which will send the request to the detail endpoint if parameters are passed in.

let promise = Pizza.objects.get({id: 10}); // will request Pizza._meta.endpoints.detail

2.2. RESTful API client 11

Django PonyJS Documentation, Release 0.2.0

However, it’s also possible to call .getwithout parameters on a queryset, and it works similar as Django: it will returnthe only object matching. It’s possible that this will throw MultipleObjectsReturned or DoesNotExistexceptions if the queryset was not correctly constructed.

let promise = Pizza.objects.filter({id: 10}).get();

Promises

Note that the variable promise was used in the previous examples. This indicates how the async nature of Xml-HttpRequests works. The requests is fired and the Javascript continues executing, eventually returning back to thepromise success/error callbacks.

The usage with a regular (list) queryset is like this:

Pizza.objects.all().then(pizzas => {console.log(pizzas); // [<Pizza 1>, <Pizza 2>]console.log(pizzas.paginator); // <Paginator> or undefined

}, (error) => {// handle error

});

// or, for details

Pizza.objects.get({id: 1}).then(pizza => {pizza.eat();

});

Entering the promise is done through the then method of querysets, or the get method for details. These are themoments where the requests are effectively sent.

The pizza variables are actual Pizza model instances, and as such, they have all methods you defined.

From the first example it can also be seen that on the return value, a paginator key may be present. This is the caseif the response was paginated, and it’s a ponyjs.models.paginator.Paginator instance.

Creating/updating/deleting data through the API

Other than retrieving data, it must also be possible to do write actions via the API.

Create

Similar to Django’s ORM, managers support the create method. This lets you run code like:

let promise = Pizza.objects.create({name: 'Hawaii',vegan: false,

});

This method returns a promise as well, which eventually resolves to a model instance based on the REST API response.

If server side validations occur, these are available in the catch promise handler:

Pizza.objects.create({vegan: 'invalid-value',

}).then(pizza => {// ...

12 Chapter 2. Intended usage/public

Django PonyJS Documentation, Release 0.2.0

}).catch(errors => {console.log(errors);// {// 'name': 'This field is required.',// 'vegan': 'Fill in a valid value.',// }

});

Warning: At this point, validation errors are detected by looking at the HttpResponse status code. If a HTTP 400is detected, the error is wrapped in a ponyjs.models.query.ValidationError instance. If a differentHTTP status code is returned (like a 50x), the errors variable will look different.This will be redesigned, but for the time being it’s your responsibility to check the type of error.

Update

WIP

Delete

WIP

You’ll need to create a json file with API information before you can make any requests. How to set this up is outlinedin Configuring the API endpoint(s).

After that, you’ll need to set up some minimal ‘models’ to interact with the API, see Defining models. Essentially,here you specify which endpoints to use and how to interpret the serializer output.

Once your models have been setup, you can actually start using them. This is implemented with the concept ofPromises to deal with the asynchronous nature. A quick example may look like this:

Pizza.objects.filter({vegan: true}).then(pizzas => {menu.render(pizzas);

});

See the usage documentation for more details.

2.3 Formsets

Formsets are a great tool in Django to be able to quickly edit related objects, or edit a set of objects on one page. Theyare frequently used to be able to add an arbitry number of objects, or delete some objects.

Formsets operate with a prefix and an index to be able to distinguish the different forms (each ‘formset form’ representsan object, or maybe just an arbitrary form).

Formsets combined with Javascript to add/remove forms in the page without a full page reload are powerfull, butcrufty: you need to manually keep track of the number of forms and make sure that the indices don’t make any‘jumps’, else Django might complain.

The admin has a JS module to handle that fairly generic: django.contrib.admin.static.admin.js.inlines,but it dictates a certain markup.

The Formset class from ponyJS is a more generic implementation. It’s an OOP orientation to handleadding/removing forms painless.

2.3. Formsets 13

Django PonyJS Documentation, Release 0.2.0

2.3.1 Basic usage

Usage is simple: import the class and instantiate an instance.

It does require you to specify the template for a new form, because no assumptions are made whether you use HTMLtemplates, React or any other tool. See New form template for more details.

The constructor takes two arguments, the first one is required: the prefix of the formset. The second one is anoptional object with options.

import Formset from 'ponyjs/forms/formsets.js';

let formset = new Formset('form');

Adding a form

Adding a form takes care of rendering the template to a new form and incrementing the total form count in the formsetmanagement form. It’s your responsibility to insert the form HTML in the DOM where you want it. The returnedindex is the new index of the added form.

let [html, index] = formset.addForm();// $('#my-node').append(html);

Setting form data

If you need to update the form data for a certain index, there’s an utility function: Formset.setData. It takes anindex and an object of unprefixed form field names:

formset.setData(3, {'my-input': 'new value'});

Deleting a form

To delete a form, you need to know it’s index.

Deleting a form takes care of removing the form node from the DOM, determined byFormset.options.formCssClass and updates the total form count.

formset.deleteForm(2);

2.3.2 Options

The formset instance can take an options object.

// defaults are shownlet options = {

// the CSS class that wraps a single form. Required for Formset.deleteFormformCssClass: 'form',// a possible template string for a new form. All ``__prefix__``// occurrences will be replaced with the new index.template: null,

}

14 Chapter 2. Intended usage/public

Django PonyJS Documentation, Release 0.2.0

2.3.3 New form template

There are two ways to specify the template for a new form: supply it as a string in the options, or override thetemplate property by subclassing Formset.

Specify template as an option

This is probably the most straight-forward way, but violates DRY:

let formset = new Formset('my-prefix', {template: '<div class="form"><input name="my-prefix-__prefix__-my_input"></div>'

});

It leads to big HTML chunks in your Javascript, and is therefore not recommended.

Subclass Formset

This is the best ‘DRY’ method: you can put {{ formset.empty_form }} somewhere in your template, wrappedin a div with an ID empty-form for example. Django renders the entire formset form with a __prefix__ index.

To use that as a template, you simple do:

class MyFormset extends Formset {get template() {

if (!this._template) {this._template = $('#empty-form').html();

}return this._template;

}}

The Formset.template property is a getter, and it’s thus possible to cache the template on the instance, as seen in theexample.

You could also use a client side template engine to render the formset template from somewhere.

2.3.4 Formset properties

Each Formset instances has some public properties/attributes.

• Formset.totalForms: this reports the total amount of forms according to the hidden input from the man-agement form. It’s both a getter and a setter.

• Formset.maxForms: reports the maximum number of allowed forms according to the hidden input from themanagement form. Getter only.

• Formset.template: returns the template used for rendering the new form. Getter only. ThrowsError(’Not implemented’) if the template has not been specified.

• Formset.addForm: see Adding a form.

• Formset.setData: see Setting form data.

• Formset.deleteForm: see Deleting a form.

2.3. Formsets 15

Django PonyJS Documentation, Release 0.2.0

2.4 Related Django packages

2.4.1 Django Rest Framework

The API interaction is being based on DRF, so it should work (almost) out of the box.

Current limitations are that only json output is supported, so be sure to leave the JSONRenderer enabled. PonyJSsends the appropriate headers, which are handled correctly by DRF.

Pagination

PonyJS automagically tries to detect if a response is paginated or not - if it’s a Javascript Object (curly braces) that’sreturned from GET list calls, it is assumed that the response is paginated. Else, it’s not.

As the server handles pagination, it’s not (easily) possible on the client to figure out the paginate_by configuration,which may even vary by endpoint. To be able to build the paginator correctly, it is required that you include thepaginate_by output in your paginator class. This can be configured as such:

# in settings.pyREST_FRAMEWORK = {

'DEFAULT_PAGINATION_CLASS': 'project.api.pagination.PageNumberPagination',...

}

# project/api/pagination.pyfrom rest_framework.pagination import PageNumberPagination

class PageNumberPagination(PageNumberPagination):

def get_paginated_response(self, data):response = super(PageNumberPagination, self).get_paginated_response(data)response.data['paginate_by'] = self.get_page_size(self.request)return response

2.4.2 Tastypie

Currently unsupported. Create a Github issue or submit a PR to add support.

2.4.3 Django-SystemJS

Django SystemJS integrates Django and jspm. It provides a templatetag and management command that wrap aroundthe jspm CLI. It makes it easy to discover and bundle jspm apps in your project.

16 Chapter 2. Intended usage/public

CHAPTER 3

Contact

Bug reports, issues, pull-requests can all be submitted on Github.

If you need help, submit an issue, or maybe try and find me on the Django IRC channel, username xBBTx. You canalso tweet me.

3.1 Indices and tables

• genindex

• modindex

• search

17