Add basic GET views.
authorMikko Värri <vmj@linuxbox.fi>
Thu, 16 Sep 2010 01:36:45 +0000 (04:36 +0300)
committerMikko Värri <vmj@linuxbox.fi>
Thu, 16 Sep 2010 01:36:45 +0000 (04:36 +0300)
recycloid_api/templates/recycloid_api/response.json [new file with mode: 0644]
recycloid_api/templates/recycloid_api/response.xml [new file with mode: 0644]
recycloid_api/templatetags/__init__.py [new file with mode: 0644]
recycloid_api/templatetags/timestamp.py [new file with mode: 0644]
recycloid_api/urls.py [new file with mode: 0644]
recycloid_api/views.py

diff --git a/recycloid_api/templates/recycloid_api/response.json b/recycloid_api/templates/recycloid_api/response.json
new file mode 100644 (file)
index 0000000..a51fe99
--- /dev/null
@@ -0,0 +1,123 @@
+{% load timestamp %}
+{
+    "response": {
+{% if meta %}{% for key, value in meta.items %}
+        "@{{ key }}": "{{ value }}",
+{% endfor %}{% endif %}
+{% if error %}
+        "error": {
+            "@code": "{{ error.code }}",
+            "text": "{{ error.text }}"
+        },
+{% endif %}
+{% if servers %}
+        "servers": {
+            "@href": "{% url recycloid-api-servers %}",
+            "server": [
+{% for server in servers %}
+                {
+                    "@uuid": "{{ server.uuid }}",
+                    "url": "{{ server.url }}",
+                    "links": {
+                        "self": { "@href": "{% url recycloid-api-server server.uuid %}" },
+                        "owners": { "@href": "{% url recycloid-api-server server.uuid %}owners/" },
+                        "stashes": { "@href": "{% url recycloid-api-server server.uuid %}stashes/" }
+                    }
+                },
+{% endfor %}
+            ]
+        },
+{% endif %}
+{% if owners %}
+        "owners": {
+            "@href": "{% url recycloid-api-owners %}",
+            "owner": [
+{% for owner in owners %}
+                {
+                    "@uuid": "{{ owner.uuid }}",
+                    "@server": "{{ owner.server.uuid }}",
+                    "description": "{{ owner.description|escape }}",
+                    "image": "{{ owner.thumbnail }}",
+                    "links": {
+                        "self": { "@href": "{% url recycloid-api-owners owner.uuid %}" },
+                        "everything": { "@href": "{% url recycloid-api-owners owner.uuid %}+everything" },
+                        "stashes": { "@href": "{% url recycloid-api-owners owner.uuid %}stashes/" },
+                        "server": { "@href": "{% url recycloid-api-servers owner.server.uuid %}" }
+                    }
+                },
+{% endfor %}
+            ]
+        },
+{% endif %}
+{% if stashes %}
+        "stashes": {
+            "@href": "{% url recycloid-api-stashes %}",
+            "stash": [
+{% for stash in stashes %}
+                {
+                    "@uuid": "{{ stash.uuid }}",
+                    "@owner": "{{ stash.owner.uuid }}",
+                    "@server": "{{ stash.server.uuid }}",
+                    "title": "{{ stash.title|escape }}",
+                    "description": "{{ stash.description|escape }}",
+                    "image": "{{ stash.thumbnail }}",
+                    "location": {
+                        "latitude": "{{ stash.latitude }}",
+                        "longitude": "{{ stash.longitude }}"
+                    },
+                    "items": { "@items": "{{ stash.item_count }}" },
+                    "links": {
+                        "self": { "@href": "{% url recycloid-api-stashes stash.uuid %}" },
+                        "items": { "@href": "{% url recycloid-api-stashes stash.uuid %}items/" },
+                        "owner": { "@href": "{% url recycloid-api-owners stash.owner.uuid %}" },
+                        "server": { "@href": "{% url recycloid-api-servers stash.server.uuid %}" }
+                    }
+                },
+{% endfor %}
+            ]
+        },
+{% endif %}
+{% if items %}
+        "items": {
+            "@href": "{% url recycloid-api-items %}",
+            "item": [
+{% for item in items %}
+                {
+                    "@uuid": "{{ item.uuid }}",
+                    "@stash": "{{ item.stash.uuid }}",
+                    "title": "{{ item.title|escape }}",
+                    "description": "{{ item.description|escape }}",
+                    "expires": "{{ item.expires|timestamp }}",
+                    "modified": "{{ item.modified|timestamp }}",
+                    "created": "{{ item.created|timestamp }}",
+                    "image": "{{ item.thumbnail }}",
+                    "links": {
+                        "self": { "@href": "{% url recycloid-api-items item.uuid %}" },
+                        "images": { "@href": "{% url recycloid-api-items item.uuid %}images/" },
+                        "stash": { "@href": "{% url recycloid-api-stashes item.stash.uuid %}" }
+                    }
+                },
+{% endfor %}
+            ]
+        },
+{% endif %}
+{% if images %}
+        "images": {
+            "@href": "{% url recycloid-api-images %}",
+            "image": [
+{% for image in images %}
+                {
+                    "@uuid": "{{ image.uuid }}",
+                    "@item": "{{ image.item.uuid }}",
+                    "url": "{{ image.url }}",
+                    "links": {
+                        "self": { "@href": "{% url recycloid-api-images image.uuid %}" },
+                        "item": { "@href": "{% url recycloid-api-items image.item.uuid %}" }
+                    }
+                },
+{% endfor %}
+            ]
+        },
+{% endif %}
+    }
+}
\ No newline at end of file
diff --git a/recycloid_api/templates/recycloid_api/response.xml b/recycloid_api/templates/recycloid_api/response.xml
new file mode 100644 (file)
index 0000000..7bdc955
--- /dev/null
@@ -0,0 +1,95 @@
+{% spaceless %}
+{% load timestamp %}
+<?xml version="1.0" encoding="UTF-8"?>
+<response{% if meta %}{% for key, value in meta.items %} {{ key }}="{{ value }}"{% endfor %}{% endif %}>
+{% if error %}
+    <error code="{{ error.code }}">
+        <text>{{ error.text }}</text>
+    </error>
+{% endif %}
+{% if servers %}
+    <servers href="{% url recycloid-api-servers %}">
+    {% for server in servers %}
+        <server uuid="{{ server.uuid }}">
+            <url>{{ server.url }}</url>
+            <links>
+                <self href="{% url recycloid-api-server server.uuid %}" />
+                <stashes href="{% url recycloid-api-server server.uuid %}stashes/" />
+                <owners href="{% url recycloid-api-server server.uuid %}owners/" />
+            </links>
+        </server>
+    {% endfor %}
+    </servers>
+{% endif %}
+{% if owners %}
+    <owners href="{% url recycloid-api-owners %}">
+    {% for owner in owners %}
+        <owner uuid="{{ owner.uuid }}" server="{{ owner.server.uuid }}">
+            <description>{{ owner.description|escape }}</description>
+            <image>{{ owner.thumbnail }}</image>
+            <links>
+                <self href="{% url recycloid-api-owner owner.uuid %}" />
+                <everything href="{% url recycloid-api-owner owner.uuid %}+everything" />
+                <stashes href="{% url recycloid-api-owner owner.uuid %}stashes/" />
+                <server href="{% url recycloid-api-server owner.server.uuid %}" />
+            </links>
+        </owner>
+    {% endfor %}
+    </owners>
+{% endif %}
+{% if stashes %}
+    <stashes href="{% url recycloid-api-stashes %}">
+    {% for stash in stashes %}
+        <stash uuid="{{ stash.uuid }}" owner="{{ stash.owner.uuid }}" server="{{ stash.server.uuid }}">
+            <title>{{ stash.title|escape }}</title>
+            <description>{{ stash.description|escape }}</description>
+            <image>{{ stash.thumbnail }}</image>
+            <location>
+                <latitude>{{ stash.latitude }}</latitude>
+                <longitude>{{ stash.longitude }}</longitude>
+            </location>
+            <items items="{{ stash.item_count }}" />
+            <links>
+                <self href="{% url recycloid-api-stash stash.uuid %}" />
+                <items href="{% url recycloid-api-stash stash.uuid %}items/" />
+                <owner href="{% url recycloid-api-owner stash.owner.uuid %}" />
+                <server href="{% url recycloid-api-server stash.server.uuid %}" />
+            </links>
+        </stash>
+    {% endfor %}
+    </stashes>
+{% endif %}
+{% if items %}
+    <items href="{% url recycloid-api-items %}">
+    {% for item in items %}
+        <item uuid="{{ item.uuid }}" stash="{{ item.stash.uuid }}">
+            <title>{{ item.title|escape }}</title>
+            <description>{{ item.description|escape }}</description>
+            <expires>{{ item.expires|timestamp }}</expires>
+            <modified>{{ item.modified|timestamp }}</modified>
+            <created>{{ item.created|timestamp }}</created>
+            <image>{{ item.thumbnail }}</image>
+            <links>
+                <self href="{% url recycloid-api-item item.uuid %}" />
+                <images href="{% url recycloid-api-item item.uuid %}images/" />
+                <stash href="{% url recycloid-api-stash item.stash.uuid %}" />
+            </links>
+        </item>
+    {% endfor %}
+    </items>
+{% endif %}
+{% if images %}
+    <images href="{% url recycloid-api-images %}">
+    {% for image in images %}
+        <image uuid="{{ image.uuid }}" item="{{ image.item.uuid }}">
+            <url>{{ image.url }}</url>
+            <links>
+                <self href="{% url recycloid-api-image image.uuid %}" />
+                <item href="{% url recycloid-api-item image.item.uuid %}" />
+            </links>
+        </image>
+    {% endfor %}
+    </images>
+{% endif %}
+</response>
+{% endspaceless %}
diff --git a/recycloid_api/templatetags/__init__.py b/recycloid_api/templatetags/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/recycloid_api/templatetags/timestamp.py b/recycloid_api/templatetags/timestamp.py
new file mode 100644 (file)
index 0000000..7249a1f
--- /dev/null
@@ -0,0 +1,18 @@
+import datetime
+import time
+
+from django import template
+
+register = template.Library()
+
+
+@register.filter
+def timestamp(value):
+    """Turns a datetime object into timestamp"""
+    if value is None:
+        return u''
+    if time.daylight == 0:
+        return value
+    delta = datetime.timedelta(seconds=time.altzone)
+    utc = value + delta
+    return utc.strftime('%Y-%m-%dT%H:%M:%SZ')
diff --git a/recycloid_api/urls.py b/recycloid_api/urls.py
new file mode 100644 (file)
index 0000000..497b27d
--- /dev/null
@@ -0,0 +1,49 @@
+from django.conf.urls.defaults import *
+
+# 012345678-0123-0123-0123-0123456789AB
+uuid = "[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}"
+
+server = r'(?P<server>' + uuid + r')'
+owner = r'(?P<owner>' + uuid + r')'
+stash = r'(?P<stash>' + uuid + r')'
+item = r'(?P<item>' + uuid + r')'
+image = r'(?P<image>' + uuid + r')'
+
+urlpatterns = patterns('recycloid_api.views',
+        url(r'^servers/$', 'servers', name='recycloid-api-servers'),
+        url(r'^servers/' + server + r'/$', 'servers', name='recycloid-api-server'),
+        url(r'^servers/' + server + r'/stashes/$', 'stashes'),
+        url(r'^servers/' + server + r'/stashes/' + stash + r'/$', 'stashes'),
+        url(r'^servers/' + server + r'/stashes/' + stash + r'/items/$', 'items'),
+        url(r'^servers/' + server + r'/stashes/' + stash + r'/items/' + item + r'/$', 'items'),
+        url(r'^servers/' + server + r'/stashes/' + stash + r'/items/' + item + r'/images/$', 'images'),
+        url(r'^servers/' + server + r'/stashes/' + stash + r'/items/' + item + r'/images/' + image + r'/$', 'images'),
+        url(r'^servers/' + server + r'/owners/$', 'owners'),
+        url(r'^servers/' + server + r'/owners/' + owner + r'/$', 'owners'),
+        url(r'^servers/' + server + r'/owners/' + owner + r'/stashes/$', 'stashes'),
+        url(r'^servers/' + server + r'/owners/' + owner + r'/stashes/' + stash + r'/$', 'stashes'),
+        url(r'^servers/' + server + r'/owners/' + owner + r'/stashes/' + stash + r'/items/$', 'items'),
+        url(r'^servers/' + server + r'/owners/' + owner + r'/stashes/' + stash + r'/items/' + item + r'/$', 'items'),
+        url(r'^servers/' + server + r'/owners/' + owner + r'/stashes/' + stash + r'/items/' + item + r'/images/$', 'images'),
+        url(r'^servers/' + server + r'/owners/' + owner + r'/stashes/' + stash + r'/items/' + item + r'/images/' + image + r'/$', 'images'),
+        url(r'^owners/$', 'owners', name='recycloid-api-owners'),
+        url(r'^owners/' + owner + r'/$', 'owners', name='recycloid-api-owner'),
+        url(r'^owners/' + owner + r'/stashes/$', 'stashes'),
+        url(r'^owners/' + owner + r'/stashes/' + stash + r'/$', 'stashes'),
+        url(r'^owners/' + owner + r'/stashes/' + stash + r'/items/$', 'items'),
+        url(r'^owners/' + owner + r'/stashes/' + stash + r'/items/' + item + r'/$', 'items'),
+        url(r'^owners/' + owner + r'/stashes/' + stash + r'/items/' + item + r'/images/$', 'images'),
+        url(r'^owners/' + owner + r'/stashes/' + stash + r'/items/' + item + r'/images/' + image + r'/$', 'images'),
+        url(r'^stashes/$', 'stashes', name='recycloid-api-stashes'),
+        url(r'^stashes/' + stash + r'/$', 'stashes', name='recycloid-api-stash'),
+        url(r'^stashes/' + stash + r'/items/$', 'items'),
+        url(r'^stashes/' + stash + r'/items/' + item + r'/$', 'items'),
+        url(r'^stashes/' + stash + r'/items/' + item + r'/images/$', 'images'),
+        url(r'^stashes/' + stash + r'/items/' + item + r'/images/' + image + r'/$', 'images'),
+        url(r'^items/$', 'items', name='recycloid-api-items'),
+        url(r'^items/' + item + r'/$', 'items', name='recycloid-api-item'),
+        url(r'^items/' + item + r'/images/$', 'images'),
+        url(r'^items/' + item + r'/images/' + image + r'/$', 'images'),
+        url(r'^images/$', 'images', name='recycloid-api-images'),
+        url(r'^images/' + image + r'/$', 'images', name='recycloid-api-image'),
+)
index 60f00ef..453cc1a 100644 (file)
@@ -1 +1,307 @@
-# Create your views here.
+import codecs
+import re
+
+from django.http import HttpResponse
+from django.shortcuts import render_to_response
+
+from recycloid_api.parsers import JsonParser, XmlParser
+from recycloid_models.models import StashServer, StashOwner, Stash, StashItem, StashItemImage
+
+
+response_template = {
+    'application/xml': 'recycloid_api/response.xml',
+    'application/json': 'recycloid_api/response.json',
+    }
+
+accept_delim_re = re.compile(r'\s*,\s*')
+accept_param_re = re.compile(r'\s*;\s*([^=\s]+)\s*=\s*([^;]+)')
+
+
+class HttpResponseNotAcceptable(HttpResponse):
+    status_code = 406
+
+    def __init__(self, available_mediatypes):
+        HttpResponse.__init__(self)
+        self.content = available_mediatypes
+
+
+def __response_format(request):
+    """
+    Determines whether this request wants XML or JSON representation.
+
+    :param request: HttpRequest
+    :return: "xml", "json" or None (if something unsupported is requested.
+    """
+    if 'HTTP_ACCEPT' not in request.META:
+        return ('application/xml', 'utf-8')
+
+    # Available mediatypes and their default params (q and charset)
+    mediatypes = {'*/*': [-1.0, 'utf-8'],
+                  'application/*': [-1.0, 'utf-8'],
+                  'application/xml': [-1.0, 'utf-8'],
+                  'application/json': [-1.0, 'utf-8'],
+                  }
+
+    # Get params from acceptable mediatypes
+    for mediatype in accept_delim_re.split(request.META['HTTP_ACCEPT']):
+        range = mediatype.split(';')[0].strip()
+        params = dict(accept_param_re.findall(mediatype))
+        for mediatype in mediatypes.keys():
+            if range == mediatype:
+                qparam = params.get('q', 1.0)
+                encoding = params.get('charset', 'utf-8')
+                try:
+                    qparam = float(qparam)
+                    codecs.getdecoder(encoding)
+                except ValueError:
+                    print "invalid q param %s" % qparam
+                    pass
+                except LookupError:
+                    pass
+                finally:
+                    qparam = min(max(0.0, qparam), 1.0)
+                    if mediatypes[mediatype][0] < qparam:
+                        mediatypes[mediatype][0] = qparam
+                        mediatypes[mediatype][1] = encoding
+
+    # Sort the available mediatypes by qparam
+    mediatypes = sorted([(k, v[0], v[1]) for k, v in mediatypes.items()],
+                        cmp=lambda x,y: cmp(x[1], y[1]),
+                        reverse=True)
+    if mediatypes[0][1] >= 0.0:
+        if mediatypes[0][0] in ('*/*', 'application/*', 'application/xml'):
+            return ('application/xml', mediatypes[0][2])
+        else:
+            return ('application/json', mediatypes[0][2])
+
+    # Accept header was there but nothing matched
+    return (None, None)
+
+
+def __limit(request, queryset):
+    """
+    Limits the QuerySet, if offset and/or limit is given in the request.
+
+    :param request: HttpRequest
+    :param queryset: QuerySet to limit
+    :return: A dictionary containing the meta used to limit the query set, and the limited queryset.
+    """
+    offset = int(request.GET.get('offset', -1))
+    limit = int(request.GET.get('limit', -1))
+
+    if offset <= 0 and limit < 0:
+        return {}, queryset
+
+    if offset > 0 and limit < 0:
+        meta = {'offset': offset}
+        qry = request.GET.copy()
+        del qry['offset']
+        meta['previous'] = '%s?%s' % (request.path, qry.urlencode())
+        if meta['previous'].endswith('?'):
+            meta['previous'] = meta['previous'][:-1]
+        return meta, queryset[offset:]
+
+    if offset <= 0 and limit > 0:
+        meta = {'limit': limit}
+        qry = request.GET.copy()
+        qry['offset'] = limit
+        meta['next'] = '%s?%s' % (request.path, qry.urlencode())
+        return meta, queryset[:limit]
+
+    meta = {'offset': offset, 'limit': limit}
+    qry = request.GET.copy()
+    qry['offset'] = offset + limit
+    meta['next'] = '%s?%s' % (request.path, qry.urlencode())
+    qry['offset'] = offset - limit
+    if qry['offset'] <= 0:
+        del qry['offset']
+    meta['previous'] = '%s?%s' % (request.path, qry.urlencode())
+    return meta, queryset[offset:offset+limit]
+
+
+def __response(request, context):
+    mimetype, encoding = __response_format(request)
+    if mimetype:
+        return render_to_response(response_template[mimetype], context, mimetype=mimetype)
+    else:
+        return HttpResponseNotAcceptable('application/xml, application/json')
+
+
+def __add(obj, key, reg, context):
+    """
+    Adds obj to context if it is not there already.
+
+    :param obj:  recycloid_models.models.IdentifiedModel instance
+    :param key:  
+    :param reg:  dict where to register which objects have been added to context
+    :param objs:  list where to add obj, unless it already is there
+    """
+    if key not in reg or not isinstance(reg[key], dict):
+        reg[key] = {}
+    if key not in context or not isinstance(context[key], list):
+        context[key] = []
+    if obj.uuid not in reg[key]:
+        reg[key][obj.uuid] = True
+        context[key].append(obj)
+    return
+
+def __add_owner_related(owner, reg, context):
+    __add(owner.server, 'servers', reg, context)
+    return
+
+def __add_stash_related(stash, reg, context):
+    __add_owner_related(stash.owner, reg, context)
+    __add(stash.server, 'servers', reg, context)
+    __add(stash.owner, 'owners', reg, context)
+    return
+
+def __add_item_related(item, reg, context):
+    __add_stash_related(item.stash, reg, context)
+    __add(item.stash, 'stashes', reg, context)
+    return
+
+def __add_image_related(image, reg, context):
+    __add_item_related(image.item, reg, context)
+    __add(image.item, 'items', reg, context)
+    return
+
+
+def __object_list_response(request, objects, name, add_related_func):
+    # [TODO] Search
+
+    if add_related_func:
+        objects = objects.select_related()
+
+    total = objects.count()
+
+    meta, objects = __limit(request, objects)
+    meta['total'] = total
+
+    context = {'meta': meta, name: objects}
+
+    if add_related_func:
+        reg = {}
+        for obj in objects:
+            add_related_func(obj, reg, context)
+
+    return __response(request, context)
+
+
+def servers(request, server=None):
+    """
+    Returns a list of servers or one server.
+
+    :param request: HttpRequest
+    :param server: String representation of a UUID of a server or None.
+    :return: HttpResponse
+    """
+    if server:
+        servers = StashServer.objects.filter(uuid__exact=server)
+    else:
+        servers = StashServer.objects.all()
+
+    return __object_list_response(request, servers, 'servers', None)
+
+
+def owners(request, server=None, owner=None):
+    """
+    Returns a list of owners or one owner.
+
+    :param request: HttpRequest
+    :param server: String representation of a UUID of a server or None.
+    :param owner: String representation of a UUID of a owner or None.
+    :return: HttpResponse
+    """
+    if owner:
+        owners = StashOwner.objects.filter(uuid__exact=owner)
+    else:
+        owners = StashOwner.objects.all()
+
+    if server:
+        owners = owners.filter(server__uuid__exact=server)
+
+    return __object_list_response(request, owners, 'owners', __add_owner_related)
+
+
+def stashes(request, server=None, owner=None, stash=None):
+    """
+    Returns a list of stashes or one stash.
+
+    :param request: HttpRequest
+    :param server: String representation of a UUID of a server or None.
+    :param owner: String representation of a UUID of a owner or None.
+    :param stash: String representation of a UUID of a stash or None.
+    :return: HttpResponse
+    """
+    if stash:
+        stashes = Stash.objects.filter(uuid__exact=stash)
+    else:
+        stashes = Stash.objects.all()
+
+    if owner:
+        stashes = stashes.filter(owner__uuid__exact=owner)
+
+    if server:
+        stashes = stashes.filter(server__uuid__exact=server)
+
+    return __object_list_response(request, stashes, 'stashes', __add_stash_related)
+
+
+def items(request, server=None, owner=None, stash=None, item=None):
+    """
+    Returns a list of items or one item.
+
+    :param request: HttpRequest
+    :param server: String representation of a UUID of a server or None.
+    :param owner: String representation of a UUID of a owner or None.
+    :param stash: String representation of a UUID of a stash or None.
+    :param item: String representation of a UUID of a item or None.
+    :return: HttpResponse
+    """
+    if item:
+        items = StashItem.objects.filter(uuid__exact=item)
+    else:
+        items = StashItem.objects.all()
+
+    if stash:
+        items = items.filter(stash__uuid__exact=stash)
+
+    if owner:
+        items = items.filter(stash__owner__uuid=owner)
+
+    if server:
+        items = items.filter(stash__server__uuid=server)
+
+    return __object_list_response(request, items, 'items', __add_item_related)
+
+
+def images(request, server=None, owner=None, stash=None, item=None, image=None):
+    """
+    Returns a list of images or one image.
+
+    :param request: HttpRequest
+    :param server: String representation of a UUID of a server or None.
+    :param owner: String representation of a UUID of a owner or None.
+    :param stash: String representation of a UUID of a stash or None.
+    :param item: String representation of a UUID of a item or None.
+    :param image: String representation of a UUID of a image or None.
+    :return: HttpResponse
+    """
+    if image:
+        images = StashItemImage.objects.filter(uuid__exact=image)
+    else:
+        images = StashItemImage.objects.all()
+
+    if item:
+        images = images.filter(item__uuid__exact=item)
+
+    if stash:
+        images = images.filter(item__stash__uuid=stash)
+
+    if owner:
+        images = images.filter(item__stash__owner__uuid=owner)
+
+    if server:
+        images = images.filter(item__stash__owner__server__uuid=server)
+
+    return __object_list_response(request, images, 'images', __add_image_related)