Simplified API

This API was first introduced in this github issue

The simplified API is a simple task oriented API which simplifies the most common needs. It is a API which lends itself naturally to be exposed as a RESTful web service API. This API is the public interface for devilry in any language and platform:

  • RESTful web services
  • javascript
  • serverside python
  • client libraries using the RESTful web API.

This means that a developer who has played around with the javascript in the Devilry templates, can use the same API if he or she wants to develop a serverside addon or a command-line script using the official devilry python bindings.

What is it?

The devilry core is very powerful and flexible. This unfortunatly makes it a bit complex. The simplified API makes all the information provided by the core available through a much more simple API. Instead of the recursive database API provided by the core, developers get a API where they can say things like:

  • give me all deliveries by this user on this assignent.
  • update the feedback on this delivery
  • get all files in this delivery
  • ...

What is it not?

You can access all data in the devilry core via the simplified API, but it is limited to the most common ways of accessing this data. This means that you have to use the core directly if you wish to make complex queries which is not needed by a common task. To put it simple: It is not an API for doing things that are not supported by the devilry web interface.

Example

The code for a simplified class might look like this:

@simplified_modelapi
class Subject(object):
    class Meta:
        model = models.Subject
        resultfields = FieldSpec('id', 'short_name', 'long_name')
        searchfields = FieldSpec('short_name', 'long_name')
        methods = ['search', 'read']

    @classmethod
    def create_searchqryset(cls, user, **kwargs):
        return cls._meta.model.published_where_is_examiner(user)

    @classmethod
    def read_authorize(cls, user, obj):
        if not cls._meta.model.published_where_is_examiner(user).filter(id=obj.id):
            raise PermissionDenied()

This is the API an examiner would use to access a subject in Devilry. Every Simplified API class has to have the fields model, resultfields, searchfields and methods. Depending on what methods you define you will also need to implement some @classmethods in your Simplified class. Lets run through the details:

class Meta:
    ...

This class contains all the required fields. Lets look at what thiese fields do.

model defines the database model to be used:

class Meta:
    model = models.Subject
    ...

resultfields defines the fields you will get access to when you search/read from the model using this API:

class Meta:
    ...
    resultfields = FieldSpec(...)
    ...

searchfields defines what fields in the model will be used when searching for this object:

class Meta:
    ...
    searchfields = FieldSpec(...)
    ...

And methods define what methods will be created for this class:

class Meta:
    ...
    methods = [...]

Now let look at the required methods for class Subject.

@classmethod
def create_searchqryset(cls, user, **kwargs):
    return cls._meta.model.published_where_is_examiner(user)

runs a query in the database for the information user has access to with the parameters given in kwargs and returns this queryset. This is needed for all database-lookups.

@classmethod
def read_authorize(cls, user, obj):
    if not cls._meta.model.published_where_is_examiner(user).filter(id=obj.id):
        raise PermissionDenied()

simply checkes that user has access to the information he/she/it tries to access. This is needed for the read method we defined in methods.

Now its time to start using this API. If you look at the methods field you will se that the defined methods are read and search, and if we look at the searchfields we can search based on short_name and long_name. Say we have a subject with short_name = ‘dev10’ and another subject with short_name = ‘dev11’. And we have a user with username = ‘bob’ as an examiner in both subjects.

from simplified import Subject

examinerbob = User.objects.get(username="bob")
qrywrap = Subject.search(examinerbob, query="dev1")

Then qrywrap would contain the two subjects dev10 and dev11, since both short_name’s match our query. However, of we run:

from simplified import Subject

examinerbob = User.objects.get(username="bob")
qrywrap = Subject.search(examinerbob, query="10")

our query will only match dev10, and qrywrap will only contain the subject dev10.

Public API

Every function and class in the public API is made available directly in devilry.simplified.NAME.

exception devilry.simplified.SimplifiedException

Bases: exceptions.Exception

Base class for Simplified exceptions.

exception devilry.simplified.PermissionDenied

Bases: devilry.simplified.exceptions.SimplifiedException

Signals that a user tries to access something they are not permitted to access.

exception devilry.simplified.InvalidNumberOfResults

Bases: devilry.simplified.exceptions.SimplifiedException

Raised when search() does not return exactly the number of results specified in the exact_number_of_results parameter.

exception devilry.simplified.InvalidUsername(username)

Bases: devilry.simplified.exceptions.SimplifiedException

Raised when trying to set an invalid username.

exception devilry.simplified.FilterValidationError

Bases: devilry.simplified.exceptions.SimplifiedException

Raised when an invalid filter is given to devilry.simplified.SimplifiedModelApi.search().

class devilry.simplified.QryResultWrapper(resultfields, searchfields, django_qryset, orderbyfields=[])

Bases: object

Wrapper around a django QuerySet.

This object can be iterated and indexed:

print resultdct[0]['myfield']
for resultdct in qryresultwrapper:
    print resultdct['myfield']
resultfields

The underlying django queryset provides access to far more data than what we usually require. This list contains the fields provided when methods in this class convert the model instances in the wrapped django queryset into a dict.

searchfields

The fields that were searched when generating this result.

_insecure_django_qryset

The django queryset which this object wraps to provide security. Should not be used unless the public methods of this class fails to provide the required functionality. Any use of _insecure_django_qryset is considered a security risc.

total

The total number of matches before slicing (before applying start and limit).

count()

Shortcut for _insecure_django_qryset.count().

class devilry.simplified.FieldSpec(*always_available_fields, **additional_fieldgroups)

Bases: object

Specifies and groups fields for search and read results.

Parameters:
  • always_available_fields – Fields that is always available.
  • additional_fieldgroups – Each key is the name of a fieldgroup, and the value is a list/tuple/iterable of field names.
additional_aslist()

Returns a list of the keys in additional_fieldgroups.

all_aslist()

Get all fields in always_available_fields and in additional_fieldgroups.

aslist(fieldgroups=None)

Get the fields in always_available_fields and all fields in any of the fieldgroups.

Parameters:fieldgroups – Tuple/list of fieldgroups. Fieldgroups are keys in additional_fieldgroups (see __init__).
iterfieldnames()

Iterate over all fieldnames in this FieldSpec.

localfieldgroups_aslist()

Get all fieldgroups containing fields belonging to the current table.

Fields not belonging to the current table are any field containing __.

localfields_aslist()

Get all fields belonging to the current table.

Fields not belonging to the current table are any field containing __.

devilry.simplified.simplified_modelapi(cls)

Decorator which creates a simplified API for a Django model.

The cls must have an inner class named Meta with the following attributes:

model
Then Django model. Required.
methods

A list of supported CRUD+S methods. Required. Legal values are:

  • create
  • read
  • update
  • delete
  • search
resultfields
A FieldSpec which defines what fields to return from read() and search(). Required.
searchfields
A FieldSpec which defines what fields to search in search(). Required.
editablefields

A list of fields that are editable. If this is not specified, it defaults to resultfields.localfields_aslist() with the primary key field removed if it is a AutoField.

Only fields in this set may be given as field_values to update() or create(). Furthermore, these fields are used to generate forms in Low level RESTful web service API.

editable_fieldgroups
A list of result field groups which should at least contain all fields in editablefields. Defaults to resultfields.localfieldgroups_aslist()
filters
A devilry.simplified.FilterSpecs limiting the possible filters to perform. Defaults to an empty FilterSpec.

The cls must have the following methods for handling permissions:

read_authorize
Authorization method used for each call to read().
write_authorize
Authorization method used for each call to any CRUD+S method except for read().
create_searchqryset
Method used to create the queryset filtered in search(). Required if "search" is in Meta.methods.

The decorator adds the following attributes to cls:

_meta
Alias for the Meta class (above).
supports_create
Boolean variable: is 'create' in _meta.methods.
supports_read
Boolean variable: is 'read' in _meta.methods.
supports_update
Boolean variable: is 'update' in _meta.methods.
supports_delete
Boolean variable: is 'delete' in _meta.methods.
class devilry.simplified.SimplifiedModelApi

Bases: object

Base class for all simplified APIs.

classmethod create(user, **field_values)

Create the given object.

Parameters:user – Django user object.
Field_values :The values to set on the given object.
Returns:The primary key of the newly created object.
Throws PermissionDenied:
 If the given user does not have permission to edit this object, if the object does not exist, or if any of the field_values is not in cls._meta.editablefields.
classmethod create_searchqryset(user)

Create search queryset.

Returns:A Django QuerySet.
classmethod createmany(user, *list_of_field_values)

Create many.

This does the same as calling create() many times, except that it does it all in a single transaction. This means that the database rolls back the changes unless they all succeed.

Param :list_of_field_values List of field_values dicts (see create()).
Returns:List of the primary keys of the created objects.
classmethod delete(user, pk)

Delete the given object. If the object is_empty() it can be deleted by any user with write_authorize(), if not then only a Superuser may delete the object

Parameters:
  • user – Django user object.
  • pk – Id of object or kwargs to the get method of the configured model.
Returns:

The primary key of the deleted object.

Throws PermissionDenied:
 

If the given user does not have permission to delete this object, if the object does not exist, or if the user does not have permission to recursively delete this objects and all its children.

classmethod deletemany(user, *list_of_pks)

Delete many.

This does the same as calling delete() many times, except that it does it all in a single transaction. This means that the database rolls back the changes unless they all succeed.

Param :list_of_pks List of primary-keys/ids of the objects to delete.
Returns:List of the primary keys of the deleted objects.
classmethod handle_fieldgroups(user, result_fieldgroups, search_fieldgroups, filters)

Can be overridden to change fieldgroups before they are sent into the QryResultWrapper. For example, if certain fieldgroups contain senstive data for anonymous assignments, we can add those fieldgroups if a filter for a specific assignment is provided in filters and that assignment is not anonymous.

Returns:(result_fieldgroups, search_fieldgroups)
classmethod is_empty(obj)

Check if the given obj is empty. Defaults to returning False.

Can be implemented in subclasses to enable superadmins to recursively delete() the obj.

classmethod post_save(user, obj)

Invoked after the obj has been saved.

Override this to set custom/generated values on obj after it has been validated and saved. The default does nothing.

classmethod pre_full_clean(user, obj)

Invoked after the user has been authorize by write_authorize() and before obj.full_clean() in create() and update().

Override this to set custom/generated values on obj before it is validated and saved. The default does nothing.

classmethod read(user, pk, result_fieldgroups=[])

Read the requested item and return a dict with the fields specified in Meta.resultfields and additional fields specified in result_fieldgroups.

Parameters:
  • user – Django user object.
  • pk – Id of object or kwargs to the get method of the configured model.
  • result_fieldgroups – Fieldgroups from the additional_fieldgroups specified in result_fieldgroups.
Throws PermissionDenied:
 

If the given user does not have permission to view this object, or if the object does not exist.

classmethod read_authorize(user, obj)

Check if the given user has read permission on the given obj.

Defaults to cls.write_authorize(user, obj)

Raises PermissionDenied:
 If the given user do not have read permission on the given obj.
classmethod search(user, query='', start=0, limit=50, orderby=None, result_fieldgroups=None, search_fieldgroups=None, filters={}, exact_number_of_results=None)

Search for objects.

Parameters:
  • query – A string to search for within the model specified in Meta.model. The fields to search for is specified in Meta.search_fieldgroups.
  • start – Return results from this index in the results from the given query. Defaults to 0.
  • limit – Limit results to this number of items. Defaults to 50.
  • orderby – List of fieldnames. Fieldnames can be prefixed by '-' for descending ordering. Order the result by these fields. For example, if Meta.resultfields contains the short_name and long_name fields, we can order our results by ascending short_name and descending long_name as this: orderby=('short_name', '-long_name'). This defaults to cls._meta.ordering (see devilry.simplified.simplified_modelapi()).
  • result_fieldgroups – Adds additional fields to the result. Available values are the fieldgroups in Meta.resultfields.additional_fieldgroups.
  • search_fieldgroups – Adds additional fields which are searched for the query string. Available values are the fieldgroups in Meta.searchfields.additional_fieldgroups.
  • filters – List of filters that can be parsed by devilry.simplified.FilterSpec.parse().
  • exact_number_of_results – Expect this exact number of results. If unspecified (None), this check is not performed. If the check fails, devilry.simplified.InvalidNumberOfResults is raised.
Returns:

The result of the search.

Return type:

QryResultWrapper

classmethod update(user, pk, **field_values)

Update the given object.

Parameters:
  • user – Django user object.
  • pk – Id of object or kwargs to the get method of the configured model.
Param :

field_values: The values to set on the given object.

Returns:

The primary key of the updated object.

Throws PermissionDenied:
 

If the given user does not have permission to edit this object, if the object does not exist, or if any of the field_values is not in cls._meta.editablefields.

classmethod updatemany(user, *list_of_field_values)

Update many.

This does the same as calling update() many times, except that it does it all in a single transaction. This means that the database rolls back the changes unless they all succeed.

Param :list_of_field_values List of field_values dicts (see update()). Each dict _must_ have a _pk_ key that maps to the primary-key/id value.
Returns:List of the primary keys of the updated objects.
classmethod write_authorize(user, obj)

Check if the given user has write (update/create) permission on the given obj.

Defaults to raising :exc:NotImplementedError

Raises PermissionDenied:
 If the given user do not have write permission on the given obj.
class devilry.simplified.FilterSpecs(*filterspecs_and_foreignkeyfilterspecs)

Bases: object

Container of FilterSpec and ForeignFilterSpec.

iterfieldnames()

Iterate over all fieldnames in this FilterSpecs. Used in @simplified_modelapi to validate the fields in this FilterSpecs. Note that PatternFilterSpec are not validated.

parse(filters)

Validate the given filters and translate them into a Django query.

Parameters:filters

A list of filters on the following format:

[{'field': 'myfieldname', 'comp': '>', 'value': 30},
 {'field': 'myotherfieldname', 'comp': '=', 'value': 'myname'}]
Throws FilterValidationError:
 If any of the filters are not in the filterspecs.
class devilry.simplified.FilterSpec(fieldname, supported_comp=('exact', 'iexact', '<', '>', '<=', '>=', 'contains', 'icontains', 'startswith', 'endswith'), type_converter=<function nullConverter at 0x10c47ec80>)

Bases: object

Specifies that a specific field can be filtered, and what comp it can use.

Parameters:
  • fieldname – The field to allow filtering on.
  • supported_comp – The allowed comp for this field.
class devilry.simplified.ForeignFilterSpec(parentfield, *filterspecs)

Bases: object

Specify FilterSpec for foreign tables without having to type __ for each table. Instead of:

FilterSpecs(FilterSpec('parentnode__parentnode__short_name'),
            FilterSpec('parentnode__parentnode__longname'))

We can do:

FilterSpecs(ForeignFilterSpec('parentnode__parentnode',
                              FilterSpec('short_name'),
                              FilterSpec('long_name'))
class devilry.simplified.PatternFilterSpec(*args, **kwargs)

Bases: devilry.simplified.filterspec.FilterSpec

Pattern based field spec.

NOTE: Should only be used when _really_ required, since it is less secure and slows down filter validation (which is done on each search()).

utils

General purpose utilities used by the simplified API. If any functions here proves useful outside this module, they should me moved to devilry.utils.

devilry.simplified.utils.modelinstance_to_dict(instance, fieldnames)[source]

Convert the given django model instance into a dict where each fieldname in fieldnames is a key.

Parameters:
  • instance – A django model instance.
  • fieldname – List of fieldname names. Can also be foreign keys, such as parentnode__parentnode__short_name, or devilry.simplified.OneToMany.
devilry.simplified.utils.fix_expected_data_missing_database_fields(test_groups, expected_res, search_res=None)[source]

For internal test use ONLY.

Table Of Contents

Previous topic

Disk Layout

Next topic

Low level RESTful web service API

This Page