Ordering

Ordering is very similar to filtering. It is provided via method _order_queryset that you can override to change its default implementation. Resource class BaseModelResource again uses manager to simplify custom modifications:

class BaseModelResource(DefaultRESTModelResource, BaseObjectResource):
    order_manager = DefaultModelOrderManager()

    def _order_queryset(self, qs):
        if self.order_manager:
            return self.order_manager.order(self, qs, self.request)
        else:
            return qs

Method _order_queryset uses manager to order/sort output data. You can change the manager with order_manager property. If you set order_manager to None filtering will be disabled. Default order manager is DefaultModelOrderManager.

Order manager

Order manager purposes are:

  • parse input data that contains information about ordering and split this data to pairs <order identifier, order direction>
  • check if concrete identifier is allowed to order
  • convert identifiers to django ordering identifiers
  • finally apply ordering on django QuerySet

Allowed identifiers are defined in order_fields or extra_order_fields that can be defined on model or resource (or both).

DefaultModelOrderManager

It is only one pre-implemented Pyston ordering manager. Input data is parsed from querystring named order or HTTP header named X-Order. Both have the same format. Order terms are split with a , char. Each term consists of a direction char and an identifier. Missing direction char means ascending and char ‘-‘ means descending order. Identifiers are very similar to django identifiers where relations are joined with a string ‘__’. Example:

first_name,-last_name,created_issues__created_at

Ordering field configuration

By default the fields that can be ordered is generated as a join of order_fields and extra_order_fields. But you can change this behaviour by overriding the method get_order_fields_rfs. Values of order_fields and extra_order_fields are firstly taken from a resource and if they are not set in resource thay are obtained from model RESTMeta. If order_fields is not defined in RESTMeta it is replaced with response of the method get_allowed_fields_rfs which returns all fields that a client of the resource can read.

As example we define two models Issue and User and two resources:

class User(models.Model):

    created_at = models.DateTimeField(null=False, blank=False, auto_now_add=True)
    email = models.EmailField(null=False, blank=False, unique=True)
    contract = models.FileField(null=True, blank=True, upload_to='documents/')
    is_superuser = models.BooleanField(default=True)
    first_name = models.CharField(null=True, blank=True, max_length=100)
    last_name = models.CharField(null=True, blank=True, max_length=100)
    manual_created_date = models.DateTimeField(verbose_name=_('manual created date'), null=True, blank=True)

    class RESTMeta:
        fields = ('created_at', 'email', 'contract', 'solving_issue', 'first_name', 'last_name', 'is_superuser',
                  'manual_created_date')
        detailed_fields = ('created_at', '_obj_name', 'email', 'contract', 'solving_issue', 'first_name',
                           'last_name', 'watched_issues__name', 'watched_issues__id', 'manual_created_date')
        general_fields = ('email', 'first_name', 'last_name', 'watched_issues__name', 'watched_issues__id',
                          'manual_created_date')
        direct_serialization_fields = ('created_at', 'email', 'contract', 'solving_issue', 'first_name',
                                       'last_name', 'manual_created_date')
        order_fields = ('email', 'solving_issue')
        extra_order_fields = ('created_at',)


class Issue(models.Model):

    created_at = models.DateTimeField(null=False, blank=False, auto_now_add=True)
    name = models.CharField(max_length=100, null=False, blank=False)
    watched_by = models.ManyToManyField('app.User', blank=True, related_name='watched_issues')
    created_by = models.ForeignKey('app.User', null=False, blank=False, related_name='created_issues')
    solver = models.OneToOneField('app.User', null=True, blank=True, related_name='solving_issue')
    leader = models.OneToOneField('app.User', null=False, blank=False, related_name='leading_issue')
    description = models.TextField(null=True, blank=True)

    class RESTMeta:
        extra_order_fields = ('solver__created_at',)


class IssueResource(BaseModelResource):

    model = Issue
    fields = ('id', 'created_at', '_obj_name', 'name', ('created_by', ('id', 'contract', 'created_at')), 'solver',
              'leader', 'watched_by')
    detailed_fields = ('id', 'created_at', '_obj_name', 'name', ('created_by', ('id', 'contract',)), 'solver',
                       'leader', 'watched_by')
    general_fields = ('id', '_obj_name', 'name', ('created_by', ('id', 'contract', 'created_at')), 'watched_by')
    create_obj_permission = True
    read_obj_permission = True
    update_obj_permission = True
    delete_obj_permission = True


class UserResource(BaseModelResource):

    model = User
    create_obj_permission = True
    read_obj_permission = True
    update_obj_permission = True
    delete_obj_permission = True
    extra_order_fields = ()

As you can see order_fields and extra_order_fields are set inside model RESTMeta for User model. From RESTMeta is allowed to filter three fields (‘email’, ‘solving_issue’, ‘created_at’). But because extra_order_fields is overridden inside UserResource client can order only with (‘email’, ‘solving_issue’).

Model Issue has only set extra_order_fields which allows to order Issues by User.created_at via related field solver. Other order fields are generated from all readable fields which are obtained as a join of attributes fields, detailed_fields and general_fields.

Decorators

Ordering provides one helper to support ordering for methods as well, secorator order_by. We can use our issue tracker as an example. Issue has a description, but for some reason we can only show the first 50 chars of the description. We can order according this value too, for this purpose we can define method short_description and use order_by decorator:

from pyston.utils.decorators import order_by

class Issue(models.Model):

    description = models.TextField(verbose_name=_('description'), null=True, blank=True)

    @order_by('description')
    def short_description(self):
        return self.description[:50] if self.description is not None else None

You can now use order term ‘short_description’ to order data /api/issue/?order=short_description.