454 lines
15 KiB
Python
Executable file
454 lines
15 KiB
Python
Executable file
from django.utils import simplejson
|
|
from django.utils.encoding import smart_unicode
|
|
from django.core.paginator import Paginator
|
|
|
|
from utils import get_fields_and_servicemethods
|
|
from exceptions import StoreException, ServiceException
|
|
from services import JsonService, servicemethod
|
|
|
|
__all__ = ('Store', 'ModelQueryStore')
|
|
|
|
class StoreMetaclass(type):
|
|
""" This class (mostly) came from django/forms/forms.py
|
|
See the original class 'DeclarativeFieldsMetaclass' for doc and comments.
|
|
"""
|
|
def __new__(cls, name, bases, attrs):
|
|
|
|
# Get the declared StoreFields and service methods
|
|
fields, servicemethods = get_fields_and_servicemethods(bases, attrs)
|
|
|
|
attrs['servicemethods'] = servicemethods
|
|
|
|
# Tell each field the name of the attribute used to reference it
|
|
# in the Store
|
|
for fieldname, field in fields.items():
|
|
setattr(field, '_store_attr_name', fieldname)
|
|
attrs['fields'] = fields
|
|
|
|
return super(StoreMetaclass, cls).__new__(cls, name, bases, attrs)
|
|
|
|
class BaseStore(object):
|
|
""" The base Store from which all Stores derive
|
|
"""
|
|
|
|
class Meta(object):
|
|
""" Inner class to hold store options.
|
|
|
|
Same basic concept as Django's Meta class
|
|
on Model definitions.
|
|
"""
|
|
pass
|
|
|
|
def __init__(self, objects=None, stores=None, identifier=None, label=None, is_nested=False):
|
|
""" Store instance constructor.
|
|
|
|
Arguments (all optional):
|
|
|
|
objects:
|
|
The list (or any iterable, ie QuerySet) of objects that will
|
|
fill the store.
|
|
|
|
stores:
|
|
One or more Store objects to combine together into a single
|
|
store. Useful when using ReferenceFields to build a store
|
|
with objects of more than one 'type' (like Django models
|
|
via ForeignKeys, ManyToManyFields etc.)
|
|
|
|
identifier:
|
|
The 'identifier' attribute used in the store.
|
|
|
|
label:
|
|
The 'label' attribute used in the store.
|
|
|
|
is_nested:
|
|
This is required, if we want to return the items as direct
|
|
array and not as dictionary including
|
|
{'identifier': "id", 'label', ...}
|
|
It mainly is required, if children of a tree structure needs
|
|
to be rendered (see TreeStore).
|
|
"""
|
|
|
|
# Instantiate the inner Meta class
|
|
self._meta = self.Meta()
|
|
|
|
# Move the fields into the _meta instance
|
|
self.set_option('fields', self.fields)
|
|
|
|
# Set the identifier
|
|
if identifier:
|
|
self.set_option('identifier', identifier)
|
|
elif not self.has_option('identifier'):
|
|
self.set_option('identifier', 'id')
|
|
|
|
# Set the label
|
|
if label:
|
|
self.set_option('label', label)
|
|
elif not self.has_option('label'):
|
|
self.set_option('label', 'label')
|
|
|
|
# Is this a nested store? (indicating that it should be rendered as array)
|
|
self.is_nested = is_nested
|
|
|
|
# Set the objects
|
|
if objects != None:
|
|
self.set_option('objects', objects)
|
|
elif not self.has_option('objects'):
|
|
self.set_option('objects', [])
|
|
|
|
# Set the stores
|
|
if stores:
|
|
self.set_option('stores', stores)
|
|
elif not self.has_option('stores'):
|
|
self.set_option('stores', [])
|
|
|
|
# Instantiate the stores (if required)
|
|
self.set_option('stores', [ isinstance(s, Store) and s or s() for s in self.get_option('stores') ])
|
|
|
|
# Do we have service set?
|
|
try:
|
|
self.service = self.get_option('service')
|
|
self.service.store = self
|
|
|
|
# Populate all the declared servicemethods
|
|
for method in self.servicemethods.values():
|
|
self.service.add_method(method)
|
|
|
|
except StoreException:
|
|
self.service = None
|
|
|
|
self.request = None # Placeholder for the Request object (if used)
|
|
self.data = self.is_nested and [] or {} # The serialized data in it's final form
|
|
|
|
def has_option(self, option):
|
|
""" True/False whether the given option is set in the store
|
|
"""
|
|
try:
|
|
self.get_option(option)
|
|
except StoreException:
|
|
return False
|
|
return True
|
|
|
|
def get_option(self, option):
|
|
""" Returns the given store option.
|
|
Raises a StoreException if the option isn't set.
|
|
"""
|
|
try:
|
|
return getattr(self._meta, option)
|
|
except AttributeError:
|
|
raise StoreException('Option "%s" not set in store' % option)
|
|
|
|
def set_option(self, option, value):
|
|
""" Sets a store option.
|
|
"""
|
|
setattr(self._meta, option, value)
|
|
|
|
def __call__(self, request):
|
|
""" Called when an instance of this store is called
|
|
(ie as a Django 'view' function from a URLConf).
|
|
|
|
It accepts the Request object as it's only param, which
|
|
it makes available to other methods at 'self.request'.
|
|
|
|
Returns the serialized store as Json.
|
|
"""
|
|
self.request = request
|
|
|
|
if self.service:
|
|
self._merge_servicemethods()
|
|
if not self.is_nested:
|
|
self.data['SMD'] = self.service.get_smd( request.get_full_path() )
|
|
|
|
if request.method == 'POST':
|
|
return self.service(request)
|
|
|
|
return self.to_json()
|
|
|
|
def __str__(self):
|
|
""" Renders the store as Json.
|
|
"""
|
|
return self.to_json()
|
|
|
|
def __repr__(self):
|
|
""" Renders the store as Json.
|
|
"""
|
|
count = getattr(self.get_option('objects'), 'count', '__len__')()
|
|
return '<%s: identifier: %s, label: %s, objects: %d>' % (
|
|
self.__class__.__name__, self.get_option('identifier'), self.get_option('label'), count)
|
|
|
|
def get_identifier(self, obj):
|
|
""" Returns a (theoretically) unique key for a given
|
|
object of the form: <appname>.<modelname>__<pk>
|
|
"""
|
|
return smart_unicode('%s__%s' % (
|
|
obj._meta,
|
|
obj._get_pk_val(),
|
|
), strings_only=True)
|
|
|
|
def get_label(self, obj):
|
|
""" Calls the object's __unicode__ method
|
|
to get the label if available or just returns
|
|
the identifier.
|
|
"""
|
|
try:
|
|
return obj.__unicode__()
|
|
except AttributeError:
|
|
return self.get_identifier(obj)
|
|
|
|
def _merge_servicemethods(self):
|
|
""" Merges the declared service methods from multiple
|
|
stores into a single store. The store reference on each
|
|
method will still point to the original store.
|
|
"""
|
|
# only run if we have a service set
|
|
if self.service:
|
|
|
|
for store in self.get_option('stores'):
|
|
if not store.service: # Ignore when no service is defined.
|
|
continue
|
|
|
|
for name, method in store.service.methods.items():
|
|
try:
|
|
self.service.get_method(name)
|
|
raise StoreException('Combined stores have conflicting service method name "%s"' % name)
|
|
except ServiceException: # This is what we want
|
|
|
|
# Don't use service.add_method since we want the 'foreign' method to
|
|
# stay attached to the original store
|
|
self.service.methods[name] = method
|
|
|
|
def _merge_stores(self):
|
|
""" Merge all the stores into one.
|
|
"""
|
|
for store in self.get_option('stores'):
|
|
|
|
# The other stores will (temporarily) take on this store's 'identifier' and
|
|
# 'label' settings
|
|
orig_identifier = store.get_option('identifier')
|
|
orig_label = store.get_option('label')
|
|
for attr in ('identifier', 'label'):
|
|
store.set_option(attr, self.get_option(attr))
|
|
|
|
self.data['items'] += store.to_python()['items']
|
|
|
|
# Reset the old values for label and identifier
|
|
store.set_option('identifier', orig_identifier)
|
|
store.set_option('label', orig_label)
|
|
|
|
def add_store(self, *stores):
|
|
""" Add one or more stores to this store.
|
|
|
|
Arguments (required):
|
|
|
|
stores:
|
|
One or many Stores (or Store instances) to add to this store.
|
|
|
|
Usage:
|
|
|
|
>>> store.add_store(MyStore1, MyStore2(), ...)
|
|
>>>
|
|
"""
|
|
# If a non-instance Store is given, instantiate it.
|
|
stores = [ isinstance(s, Store) and s or s() for s in stores ]
|
|
self.set_option('stores', list( self.get_option('stores') ) + stores )
|
|
|
|
def to_python(self, objects=None):
|
|
""" Serialize the store into a Python dictionary.
|
|
|
|
Arguments (optional):
|
|
|
|
objects:
|
|
The list (or any iterable, ie QuerySet) of objects that will
|
|
fill the store -- the previous 'objects' setting will be restored
|
|
after serialization is finished.
|
|
"""
|
|
|
|
if objects is not None:
|
|
# Save the previous objects setting
|
|
old_objects = self.get_option('objects')
|
|
self.set_option('objects', objects)
|
|
self._serialize()
|
|
self.set_option('objects', old_objects)
|
|
else:
|
|
self._serialize()
|
|
|
|
return self.data
|
|
|
|
def to_json(self, *args, **kwargs):
|
|
""" Serialize the store as Json.
|
|
|
|
Arguments (all optional):
|
|
|
|
objects:
|
|
(The kwarg 'objects')
|
|
The list (or any iterable, ie QuerySet) of objects that will
|
|
fill the store.
|
|
|
|
All other args and kwargs are passed to simplejson.dumps
|
|
"""
|
|
objects = kwargs.pop('objects', None)
|
|
return simplejson.dumps( self.to_python(objects), *args, **kwargs )
|
|
|
|
def _start_serialization(self):
|
|
""" Called when serialization of the store begins
|
|
"""
|
|
if not self.is_nested:
|
|
self.data['identifier'] = self.get_option('identifier')
|
|
|
|
# Don't set a label field in the store if it's not wanted
|
|
if bool( self.get_option('label') ) and not self.is_nested:
|
|
self.data['label'] = self.get_option('label')
|
|
|
|
if self.is_nested:
|
|
self.data = []
|
|
else:
|
|
self.data['items'] = []
|
|
|
|
def _start_object(self, obj):
|
|
""" Called when starting to serialize each object in 'objects'
|
|
|
|
Requires an object as the only argument.
|
|
"""
|
|
# The current object in it's serialized state.
|
|
self._item = {self.get_option('identifier'): self.get_identifier(obj)}
|
|
|
|
label = self.get_option('label')
|
|
|
|
# Do we have a 'label' and is it already the
|
|
# name of one of the declared fields?
|
|
if label and ( label not in self.get_option('fields').keys() ):
|
|
|
|
# Have we defined a 'get_label' method on the store?
|
|
if callable( getattr(self, 'get_label', None) ):
|
|
self._item[label] = self.get_label(obj)
|
|
|
|
def _handle_field(self, obj, field):
|
|
""" Handle the given field in the Store
|
|
"""
|
|
# Fill the proxied_args on the field (for get_value methods that use them)
|
|
field.proxied_args.update({
|
|
'RequestArg': self.request,
|
|
'ObjectArg': obj,
|
|
'ModelArg': obj.__class__,
|
|
'FieldArg': field,
|
|
'StoreArg': self,
|
|
})
|
|
|
|
# Get the value
|
|
self._item[field.store_field_name] = field.get_value()
|
|
|
|
def _end_object(self, obj):
|
|
""" Called when serializing an object ends.
|
|
"""
|
|
if self.is_nested:
|
|
self.data.append(self._item)
|
|
else:
|
|
self.data['items'].append(self._item)
|
|
self._item = None
|
|
|
|
def _end_serialization(self):
|
|
""" Called when serialization of the store ends
|
|
"""
|
|
pass
|
|
|
|
def _serialize(self):
|
|
""" Serialize the defined objects and stores into it's final form
|
|
"""
|
|
self._start_serialization()
|
|
for obj in self.get_option('objects'):
|
|
self._start_object(obj)
|
|
|
|
for field in self.get_option('fields').values():
|
|
self._handle_field(obj, field)
|
|
|
|
self._end_object(obj)
|
|
self._end_serialization()
|
|
self._merge_stores()
|
|
|
|
class Store(BaseStore):
|
|
""" Just defines the __metaclass__
|
|
|
|
All the real functionality is implemented in
|
|
BaseStore
|
|
"""
|
|
__metaclass__ = StoreMetaclass
|
|
|
|
class ModelQueryStore(Store):
|
|
""" A store designed to be used with dojox.data.QueryReadStore
|
|
|
|
Handles paging, sorting and filtering
|
|
|
|
At the moment it requires a custom subclass of QueryReadStore
|
|
that implements the necessary mechanics to handle server queries
|
|
the the exported Json RPC 'fetch' method. Soon it will support
|
|
QueryReadStore itself.
|
|
"""
|
|
def __init__(self, *args, **kwargs):
|
|
"""
|
|
"""
|
|
|
|
objects_per_query = kwargs.pop('objects_per_query', None)
|
|
|
|
super(ModelQueryStore, self).__init__(*args, **kwargs)
|
|
|
|
if objects_per_query is not None:
|
|
self.set_option('objects_per_query', objects_per_query)
|
|
elif not self.has_option('objects_per_query'):
|
|
self.set_option('objects_per_query', 25)
|
|
|
|
def filter_objects(self, request, objects, query):
|
|
""" Overridable method used to filter the objects
|
|
based on the query dict.
|
|
"""
|
|
return objects
|
|
|
|
def sort_objects(self, request, objects, sort_attr, descending):
|
|
""" Overridable method used to sort the objects based
|
|
on the attribute given by sort_attr
|
|
"""
|
|
return objects
|
|
|
|
def __call__(self, request):
|
|
"""
|
|
"""
|
|
self.request = request
|
|
|
|
# We need the request.GET QueryDict to be mutable.
|
|
query_dict = {}
|
|
for k,v in request.GET.items():
|
|
query_dict[k] = v
|
|
|
|
# dojox.data.QueryReadStore only handles sorting by a single field
|
|
sort_attr = query_dict.pop('sort', None)
|
|
descending = False
|
|
if sort_attr and sort_attr.startswith('-'):
|
|
descending = True
|
|
sort_attr = sort_attr.lstrip('-')
|
|
|
|
# Paginator is 1-indexed
|
|
start_index = int( query_dict.pop('start', 0) ) + 1
|
|
|
|
# Calculate the count taking objects_per_query into account
|
|
objects_per_query = self.get_option('objects_per_query')
|
|
count = query_dict.pop('count', objects_per_query)
|
|
|
|
# We don't want the client to be able to ask for a million records.
|
|
# They can ask for less, but not more ...
|
|
if count == 'Infinity' or count > objects_per_query:
|
|
count = objects_per_query
|
|
|
|
objects = self.filter_objects(request, self.get_option('objects'), query_dict)
|
|
objects = self.sort_objects(request, objects, sort_attr, descending)
|
|
|
|
paginator = Paginator(objects, count)
|
|
|
|
page_num = 1
|
|
for i in xrange(1, paginator.num_pages + 1):
|
|
if paginator.page(i).start_index() <= start_index <= paginator.page(i).end_index():
|
|
page_num = i
|
|
break
|
|
|
|
page = paginator.page(page_num)
|
|
|
|
data = self.to_python(objects=page.object_list)
|
|
data['numRows'] = paginator.count
|
|
return data
|