diff --git a/src/rest/emitters.py b/src/rest/emitters.py index bafbf372e..87faa559b 100644 --- a/src/rest/emitters.py +++ b/src/rest/emitters.py @@ -18,7 +18,7 @@ class TemplatedEmitter(BaseEmitter): template = None def emit(self, output): - content = json.dumps(output, indent=4) + content = json.dumps(output, indent=4, sort_keys=True) template = loader.get_template(self.template) context = RequestContext(self.request, { 'content': content, diff --git a/src/rest/resource.py b/src/rest/resource.py index f14b2ba5d..c57ebfad9 100644 --- a/src/rest/resource.py +++ b/src/rest/resource.py @@ -137,15 +137,20 @@ class Resource(object): - def determine_form(self, data=None): + def determine_form(self, input_data=None, return_data=None): """Optionally return a Django Form instance, which may be used for validation and/or rendered by an HTML/XHTML emitter. - The data argument will be non Null if the form is required to be bound to some deserialized - input data, or Null if the form is required to be unbound. + The input_data or return_data arguments can be used to bind the form either to the deserialized input, + or to a return object. """ if self.form: - return self.form(data) + if input_data: + return self.form(input_data) + elif return_data: + return self.form(return_data) + else: + return self.form() return None @@ -259,12 +264,13 @@ class Resource(object): if method in ('PUT', 'POST'): parser = self.determine_parser(request) data = parser(self, request).parse(request.raw_post_data) - form = self.determine_form(data) + form = self.determine_form(input_data=data) data = self.cleanup_request(data, form) (status, ret, headers) = func(data, request.META, *args, **kwargs) else: (status, ret, headers) = func(request.META, *args, **kwargs) + form = self.determine_form(return_data=ret) except ResourceException, exc: @@ -274,7 +280,7 @@ class Resource(object): if emitter is None: mimetype, emitter = self.emitters[0] - # Use a form unbound to any data if one has not yet been created + # Create an unbound form if one has not yet been created if form is None: form = self.determine_form() @@ -284,7 +290,6 @@ class Resource(object): # Serialize the response content ret = self.cleanup_response(ret) content = emitter(self, request, status, headers, form).emit(ret) - print content # Build the HTTP Response resp = HttpResponse(content, mimetype=mimetype, status=status) @@ -308,7 +313,7 @@ class ModelResource(Resource): fields = None form_fields = None - def determine_form(self, data=None): + def determine_form(self, input_data=None, return_data=None): """Return a form that may be used in validation and/or rendering an html emitter""" if self.form: return self.form @@ -317,12 +322,14 @@ class ModelResource(Resource): class NewModelForm(ModelForm): class Meta: model = self.model - fields = self.form_fields if self.form_fields else self.fields + fields = self.form_fields if self.form_fields else None #self.fields - if data is None: - return NewModelForm() + if input_data: + return NewModelForm(input_data) + elif return_data: + return NewModelForm(instance=return_data) else: - return NewModelForm(data) + return NewModelForm() else: return None @@ -359,6 +366,12 @@ class ModelResource(Resource): ret = _list(thing) elif isinstance(thing, dict): ret = _dict(thing) + elif isinstance(thing, int): + ret = thing + elif isinstance(thing, bool): + ret = thing + elif isinstance(thing, type(None)): + ret = thing elif isinstance(thing, decimal.Decimal): ret = str(thing) elif isinstance(thing, Model): @@ -417,7 +430,7 @@ class ModelResource(Resource): ret = { } #handler = self.in_typemapper(type(data), self.anonymous) # TRC handler = None # TRC - get_absolute_uri = False + get_absolute_url = False if handler or fields: v = lambda f: getattr(data, f.attname) @@ -444,12 +457,13 @@ class ModelResource(Resource): for field in get_fields.copy(): if exclude.match(field): get_fields.discard(field) - + + get_absolute_url = True + else: get_fields = set(fields) - - if 'absolute_uri' in get_fields: # MOVED (TRC) - get_absolute_uri = True + if 'absolute_url' in get_fields: # MOVED (TRC) + get_absolute_url = True met_fields = _method_fields(handler, get_fields) # TRC @@ -508,14 +522,37 @@ class ModelResource(Resource): # ret[maybe_field] = _any(handler_f(data)) else: + # Add absolute_url if it exists + get_absolute_url = True + + # Add all the fields for f in data._meta.fields: - ret[f.attname] = _any(getattr(data, f.attname)) + if f.attname != 'id': + ret[f.attname] = _any(getattr(data, f.attname)) - fields = dir(data.__class__) + ret.keys() - add_ons = [k for k in dir(data) if k not in fields] + # Add all the propertiess + klass = data.__class__ + for attr in dir(klass): + if not attr.startswith('_') and not attr in ('pk','id') and isinstance(getattr(klass, attr, None), property): + #if attr.endswith('_url') or attr.endswith('_uri'): + # ret[attr] = self.make_absolute(_any(getattr(data, attr))) + #else: + ret[attr] = _any(getattr(data, attr)) + #fields = dir(data.__class__) + ret.keys() + #add_ons = [k for k in dir(data) if k not in fields and not k.startswith('_')] + #print add_ons + ###print dir(data.__class__) + #from django.db.models import Model + #model_fields = dir(Model) + + #for attr in dir(data): + ## #if attr.startswith('_'): + ## # continue + # if (attr in fields) and not (attr in model_fields) and not attr.startswith('_'): + # print attr, type(getattr(data, attr, None)), attr in fields, attr in model_fields - for k in add_ons: - ret[k] = _any(getattr(data, k)) + #for k in add_ons: + # ret[k] = _any(getattr(data, k)) # TRC # resouce uri @@ -532,9 +569,13 @@ class ModelResource(Resource): # except: pass # absolute uri - if hasattr(data, 'get_absolute_url') and get_absolute_uri: - try: ret['absolute_uri'] = self.make_absolute(data.get_absolute_url()) + if hasattr(data, 'get_absolute_url') and get_absolute_url: + try: ret['absolute_url'] = self.make_absolute(data.get_absolute_url()) except: pass + + for key, val in ret.items(): + if key.endswith('_url') or key.endswith('_uri'): + ret[key] = self.make_absolute(val) return ret @@ -560,8 +601,9 @@ class ModelResource(Resource): return _any(data, self.fields) - def create(self, data, headers={}): - instance = self.model(**data) + def create(self, data, headers={}, *args, **kwargs): + all_kw_args = dict(data.items() + kwargs.items()) + instance = self.model(**all_kw_args) instance.save() headers = {} if hasattr(instance, 'get_absolute_url'): @@ -569,17 +611,37 @@ class ModelResource(Resource): return (201, instance, headers) def read(self, headers={}, *args, **kwargs): - instance = self.model.objects.get(**kwargs) + try: + instance = self.model.objects.get(**kwargs) + except self.model.DoesNotExist: + return (404, '', {}) + return (200, instance, {}) def update(self, data, headers={}, *args, **kwargs): - instance = self.model.objects.get(**kwargs) - for (key, val) in data.items(): - setattr(instance, key, val) + try: + instance = self.model.objects.get(**kwargs) + for (key, val) in data.items(): + setattr(instance, key, val) + except self.model.DoesNotExist: + instance = self.model(**data) + instance.save() + instance.save() return (200, instance, {}) def delete(self, headers={}, *args, **kwargs): instance = self.model.objects.get(**kwargs) instance.delete() - return (204, '', {}) \ No newline at end of file + return (204, '', {}) + + +class QueryModelResource(ModelResource): + allowed_methods = ('read',) + + def determine_form(self, input_data=None, return_data=None): + return None + + def read(self, headers={}, *args, **kwargs): + query = self.model.objects.all() + return (200, query, {}) diff --git a/src/rest/templates/emitter.html b/src/rest/templates/emitter.html index 8be41b7cf..ddc91fbf9 100644 --- a/src/rest/templates/emitter.html +++ b/src/rest/templates/emitter.html @@ -11,10 +11,10 @@

{{ resource_name }}

{{ resource_doc }}

-
{% autoescape off %}{{ status }} {{ reason }}
+    
{{ status }} {{ reason }}{% autoescape off %}
 {% for key, val in headers.items %}{{ key }}: {{ val|urlize_quoted_links }}
 {% endfor %}
-{{ content|urlize_quoted_links }}{% endautoescape %}    
+{{ content|urlize_quoted_links }}
{% endautoescape %} {% if 'read' in resource.allowed_operations %}
diff --git a/src/rest/templatetags/urlize_quoted_links.py b/src/rest/templatetags/urlize_quoted_links.py index cef179bf1..4e3ae6c85 100644 --- a/src/rest/templatetags/urlize_quoted_links.py +++ b/src/rest/templatetags/urlize_quoted_links.py @@ -33,7 +33,7 @@ html_gunk_re = re.compile(r'(?:
|<\/i>|<\/b>|<\/em>|(?:%s).*?[a-zA-Z].*?

\s*)+)' % '|'.join([re.escape(x) for x in DOTS]), re.DOTALL) trailing_empty_content_re = re.compile(r'(?:

(?: |\s|
)*?

\s*)+\Z') -def urlize_quoted_links(text, trim_url_limit=None, nofollow=False, autoescape=False): +def urlize_quoted_links(text, trim_url_limit=None, nofollow=False, autoescape=True): """ Converts any URLs in text into clickable links. @@ -90,6 +90,10 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=False, autoescape=Fa words[i] = escape(word) return u''.join(words) + +#urlize_quoted_links.needs_autoescape = True +urlize_quoted_links.is_safe = True + # Register urlize_quoted_links as a custom filter # http://docs.djangoproject.com/en/dev/howto/custom-template-tags/ register = template.Library() diff --git a/src/rest/templatetags/urlize_quoted_links.pyc b/src/rest/templatetags/urlize_quoted_links.pyc index b49e16b67..cc77f97fb 100644 Binary files a/src/rest/templatetags/urlize_quoted_links.pyc and b/src/rest/templatetags/urlize_quoted_links.pyc differ diff --git a/src/testapp/models.py b/src/testapp/models.py index 3960d0043..32d9a612e 100644 --- a/src/testapp/models.py +++ b/src/testapp/models.py @@ -1,63 +1,90 @@ from django.db import models from django.template.defaultfilters import slugify -from datetime import datetime import uuid def uuid_str(): return str(uuid.uuid1()) -class ExampleModel(models.Model): - num = models.IntegerField(default=2, choices=((1,'one'), (2, 'two'))) - hidden_num = models.IntegerField(verbose_name='Something', help_text='HELP') - text = models.TextField(blank=False) - another = models.CharField(max_length=10) +#class ExampleModel(models.Model): +# num = models.IntegerField(default=2, choices=((1,'one'), (2, 'two'))) +# hidden_num = models.IntegerField(verbose_name='Something', help_text='HELP') +# text = models.TextField(blank=False) +# another = models.CharField(max_length=10) -class ExampleContainer(models.Model): - """Container. Has a key, a name, and some internal data, and contains a set of items.""" - key = models.CharField(primary_key=True, default=uuid_str, max_length=36, editable=False) - name = models.CharField(max_length=256) - internal = models.IntegerField(default=0) +#class ExampleContainer(models.Model): +# """Container. Has a key, a name, and some internal data, and contains a set of items.""" +# key = models.CharField(primary_key=True, default=uuid_str, max_length=36, editable=False) +# name = models.CharField(max_length=256) +# internal = models.IntegerField(default=0) - @models.permalink - def get_absolute_url(self): - return ('testapp.views.ContainerInstance', [self.key]) +# @models.permalink +# def get_absolute_url(self): +# return ('testapp.views.ContainerInstance', [self.key]) -class ExampleItem(models.Model): - """Item. Belongs to a container and has an index number and a note. - Items are uniquely identified by their container and index number.""" - container = models.ForeignKey(ExampleContainer, related_name='items') - index = models.IntegerField() - note = models.CharField(max_length=1024) - unique_together = (container, index) +#class ExampleItem(models.Model): +# """Item. Belongs to a container and has an index number and a note. +# Items are uniquely identified by their container and index number.""" +# container = models.ForeignKey(ExampleContainer, related_name='items') +# index = models.IntegerField() +# note = models.CharField(max_length=1024) +# unique_together = (container, index) +RATING_CHOICES = ((0, 'Awful'), + (1, 'Poor'), + (2, 'OK'), + (3, 'Good'), + (4, 'Excellent')) + class BlogPost(models.Model): - slug = models.SlugField(editable=False, primary_key=True, default='blah') - title = models.CharField(max_length=128) - content = models.TextField() - when = models.DateTimeField(editable=False) + key = models.CharField(primary_key=True, max_length=64, default=uuid_str, editable=False) + title = models.CharField(max_length=128, help_text='The article title (Required)') + content = models.TextField(help_text='The article body (Required)') + created = models.DateTimeField(auto_now_add=True) + slug = models.SlugField(editable=False, default='') + + class Meta: + ordering = ('created',) @models.permalink def get_absolute_url(self): - return ('testapp.views.BlogPostInstance', (self.slug,)) + return ('testapp.views.BlogPostInstance', (self.key,)) + + @property + @models.permalink + def comments_url(self): + """Link to a resource which lists all comments for this blog post.""" + return ('testapp.views.CommentList', (self.key,)) + + @property + @models.permalink + def comment_url(self): + """Link to a resource which can create a comment for this blog post.""" + return ('testapp.views.CommentCreator', (self.key,)) + + def __unicode__(self): + return self.title def save(self, *args, **kwargs): self.slug = slugify(self.title) - self.when = datetime.now() super(self.__class__, self).save(*args, **kwargs) class Comment(models.Model): - blogpost = models.ForeignKey(BlogPost, related_name='comments') - name = models.CharField(max_length=128) - content = models.TextField() - when = models.DateTimeField(auto_now_add=True) + blogpost = models.ForeignKey(BlogPost, editable=False, related_name='comments') + username = models.CharField(max_length=128, help_text='Please enter a username (Required)') + comment = models.TextField(help_text='Enter your comment here (Required)') + rating = models.IntegerField(blank=True, null=True, choices=RATING_CHOICES, help_text='Please rate the blog post (Optional)') + created = models.DateTimeField(auto_now_add=True) @models.permalink def get_absolute_url(self): - return ('testapp.views.CommentInstance', (self.blogpost.slug, self.id)) - - def save(self): - self.index = self.blogpost.comments.count() \ No newline at end of file + return ('testapp.views.CommentInstance', (self.blogpost.key, self.id)) + + @property + @models.permalink + def blogpost_url(self): + return ('testapp.views.BlogPostInstance', (self.blogpost.key,)) + diff --git a/src/testapp/tests.py b/src/testapp/tests.py index ec1607ad1..e37c57c0b 100644 --- a/src/testapp/tests.py +++ b/src/testapp/tests.py @@ -1,8 +1,4 @@ -""" -This file demonstrates two different styles of tests (one doctest and one -unittest). These will both pass when you run "manage.py test". - -Replace these with more appropriate tests for your application. +"""Test a range of REST API usage of the example application. """ from django.test import TestCase @@ -13,134 +9,154 @@ import json class AcceptHeaderTests(TestCase): - def assert_accept_mimetype(self, mimetype, expect=None, expect_match=True): - """ - Assert that a request with given mimetype in the accept header, - gives a response with the appropriate content-type. - """ + """Test correct behaviour of the Accept header as specified by RFC 2616: + + http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1""" + + def assert_accept_mimetype(self, mimetype, expect=None): + """Assert that a request with given mimetype in the accept header, + gives a response with the appropriate content-type.""" if expect is None: expect = mimetype - resp = self.client.get(reverse(views.ReadOnlyResource), HTTP_ACCEPT=mimetype) + resp = self.client.get(reverse(views.RootResource), HTTP_ACCEPT=mimetype) - if expect_match: - self.assertEquals(resp['content-type'], expect) - else: - self.assertNotEquals(resp['content-type'], expect) + self.assertEquals(resp['content-type'], expect) - def test_accept_xml(self): - self.assert_accept_mimetype('application/xml') def test_accept_json(self): + """Ensure server responds with Content-Type of JSON when requested.""" self.assert_accept_mimetype('application/json') - def test_accept_xml_prefered_to_json(self): - self.assert_accept_mimetype('application/xml,q=0.9;application/json,q=0.1', expect='application/xml') + def test_accept_xml(self): + """Ensure server responds with Content-Type of XML when requested.""" + self.assert_accept_mimetype('application/xml') - def test_accept_json_prefered_to_xml(self): + def test_accept_json_when_prefered_to_xml(self): + """Ensure server responds with Content-Type of JSON when it is the client's prefered choice.""" self.assert_accept_mimetype('application/json,q=0.9;application/xml,q=0.1', expect='application/json') - def test_dont_accept_invalid(self): - self.assert_accept_mimetype('application/invalid', expect_match=False) + def test_accept_xml_when_prefered_to_json(self): + """Ensure server responds with Content-Type of XML when it is the client's prefered choice.""" + self.assert_accept_mimetype('application/xml,q=0.9;application/json,q=0.1', expect='application/xml') + + def test_default_json_prefered(self): + """Ensure server responds with JSON in preference to XML.""" + self.assert_accept_mimetype('application/json;application/xml', expect='application/json') + + def test_accept_generic_subtype_format(self): + """Ensure server responds with an appropriate type, when the subtype is left generic.""" + self.assert_accept_mimetype('text/*', expect='text/html') + + def test_accept_generic_type_format(self): + """Ensure server responds with an appropriate type, when the type and subtype are left generic.""" + self.assert_accept_mimetype('*/*', expect='application/json') def test_invalid_accept_header_returns_406(self): - resp = self.client.get(reverse(views.ReadOnlyResource), HTTP_ACCEPT='invalid/invalid') + """Ensure server returns a 406 (not acceptable) response if we set the Accept header to junk.""" + resp = self.client.get(reverse(views.RootResource), HTTP_ACCEPT='invalid/invalid') + self.assertNotEquals(resp['content-type'], 'invalid/invalid') self.assertEquals(resp.status_code, 406) - def test_prefer_specific(self): - self.fail("Test not implemented") + def test_prefer_specific_over_generic(self): # This test is broken right now + """More specific accept types have precedence over less specific types.""" + self.assert_accept_mimetype('application/xml;*/*', expect='application/xml') class AllowedMethodsTests(TestCase): - def test_reading_read_only_allowed(self): - resp = self.client.get(reverse(views.ReadOnlyResource)) + """Basic tests to check that only allowed operations may be performed on a Resource""" + + def test_reading_a_read_only_resource_is_allowed(self): + """GET requests on a read only resource should default to a 200 (OK) response""" + resp = self.client.get(reverse(views.RootResource)) self.assertEquals(resp.status_code, 200) - def test_writing_read_only_not_allowed(self): - resp = self.client.put(reverse(views.ReadOnlyResource), {}) + def test_writing_to_read_only_resource_is_not_allowed(self): + """PUT requests on a read only resource should default to a 405 (method not allowed) response""" + resp = self.client.put(reverse(views.RootResource), {}) self.assertEquals(resp.status_code, 405) - - def test_reading_write_only_not_allowed(self): - resp = self.client.get(reverse(views.WriteOnlyResource)) - self.assertEquals(resp.status_code, 405) - - def test_writing_write_only_allowed(self): - resp = self.client.put(reverse(views.WriteOnlyResource), {}) - self.assertEquals(resp.status_code, 200) - - -class EncodeDecodeTests(TestCase): - def setUp(self): - super(self.__class__, self).setUp() - self.input = {'a': 1, 'b': 'example'} - - def test_encode_form_decode_json(self): - content = self.input - resp = self.client.put(reverse(views.WriteOnlyResource), content) - output = json.loads(resp.content) - self.assertEquals(self.input, output) - - def test_encode_json_decode_json(self): - content = json.dumps(self.input) - resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json') - output = json.loads(resp.content) - self.assertEquals(self.input, output) - - #def test_encode_xml_decode_json(self): - # content = dict2xml(self.input) - # resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json', HTTP_ACCEPT='application/json') - # output = json.loads(resp.content) - # self.assertEquals(self.input, output) - - #def test_encode_form_decode_xml(self): - # content = self.input - # resp = self.client.put(reverse(views.WriteOnlyResource), content, HTTP_ACCEPT='application/xml') - # output = xml2dict(resp.content) - # self.assertEquals(self.input, output) - - #def test_encode_json_decode_xml(self): - # content = json.dumps(self.input) - # resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json', HTTP_ACCEPT='application/xml') - # output = xml2dict(resp.content) - # self.assertEquals(self.input, output) - - #def test_encode_xml_decode_xml(self): - # content = dict2xml(self.input) - # resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json', HTTP_ACCEPT='application/xml') - # output = xml2dict(resp.content) - # self.assertEquals(self.input, output) - -class ModelTests(TestCase): - def test_create_container(self): - content = json.dumps({'name': 'example'}) - resp = self.client.post(reverse(views.ContainerFactory), content, 'application/json') - output = json.loads(resp.content) - self.assertEquals(resp.status_code, 201) - self.assertEquals(output['name'], 'example') - self.assertEquals(set(output.keys()), set(('absolute_uri', 'name', 'key'))) - -class CreatedModelTests(TestCase): - def setUp(self): - content = json.dumps({'name': 'example'}) - resp = self.client.post(reverse(views.ContainerFactory), content, 'application/json', HTTP_ACCEPT='application/json') - self.container = json.loads(resp.content) - - def test_read_container(self): - resp = self.client.get(self.container["absolute_uri"]) - self.assertEquals(resp.status_code, 200) - container = json.loads(resp.content) - self.assertEquals(container, self.container) - - def test_delete_container(self): - resp = self.client.delete(self.container["absolute_uri"]) - self.assertEquals(resp.status_code, 204) - self.assertEquals(resp.content, '') - - def test_update_container(self): - self.container['name'] = 'new' - content = json.dumps(self.container) - resp = self.client.put(self.container["absolute_uri"], content, 'application/json') - self.assertEquals(resp.status_code, 200) - container = json.loads(resp.content) - self.assertEquals(container, self.container) +# +# def test_reading_write_only_not_allowed(self): +# resp = self.client.get(reverse(views.WriteOnlyResource)) +# self.assertEquals(resp.status_code, 405) +# +# def test_writing_write_only_allowed(self): +# resp = self.client.put(reverse(views.WriteOnlyResource), {}) +# self.assertEquals(resp.status_code, 200) +# +# +#class EncodeDecodeTests(TestCase): +# def setUp(self): +# super(self.__class__, self).setUp() +# self.input = {'a': 1, 'b': 'example'} +# +# def test_encode_form_decode_json(self): +# content = self.input +# resp = self.client.put(reverse(views.WriteOnlyResource), content) +# output = json.loads(resp.content) +# self.assertEquals(self.input, output) +# +# def test_encode_json_decode_json(self): +# content = json.dumps(self.input) +# resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json') +# output = json.loads(resp.content) +# self.assertEquals(self.input, output) +# +# #def test_encode_xml_decode_json(self): +# # content = dict2xml(self.input) +# # resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json', HTTP_ACCEPT='application/json') +# # output = json.loads(resp.content) +# # self.assertEquals(self.input, output) +# +# #def test_encode_form_decode_xml(self): +# # content = self.input +# # resp = self.client.put(reverse(views.WriteOnlyResource), content, HTTP_ACCEPT='application/xml') +# # output = xml2dict(resp.content) +# # self.assertEquals(self.input, output) +# +# #def test_encode_json_decode_xml(self): +# # content = json.dumps(self.input) +# # resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json', HTTP_ACCEPT='application/xml') +# # output = xml2dict(resp.content) +# # self.assertEquals(self.input, output) +# +# #def test_encode_xml_decode_xml(self): +# # content = dict2xml(self.input) +# # resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json', HTTP_ACCEPT='application/xml') +# # output = xml2dict(resp.content) +# # self.assertEquals(self.input, output) +# +#class ModelTests(TestCase): +# def test_create_container(self): +# content = json.dumps({'name': 'example'}) +# resp = self.client.post(reverse(views.ContainerFactory), content, 'application/json') +# output = json.loads(resp.content) +# self.assertEquals(resp.status_code, 201) +# self.assertEquals(output['name'], 'example') +# self.assertEquals(set(output.keys()), set(('absolute_uri', 'name', 'key'))) +# +#class CreatedModelTests(TestCase): +# def setUp(self): +# content = json.dumps({'name': 'example'}) +# resp = self.client.post(reverse(views.ContainerFactory), content, 'application/json', HTTP_ACCEPT='application/json') +# self.container = json.loads(resp.content) +# +# def test_read_container(self): +# resp = self.client.get(self.container["absolute_uri"]) +# self.assertEquals(resp.status_code, 200) +# container = json.loads(resp.content) +# self.assertEquals(container, self.container) +# +# def test_delete_container(self): +# resp = self.client.delete(self.container["absolute_uri"]) +# self.assertEquals(resp.status_code, 204) +# self.assertEquals(resp.content, '') +# +# def test_update_container(self): +# self.container['name'] = 'new' +# content = json.dumps(self.container) +# resp = self.client.put(self.container["absolute_uri"], content, 'application/json') +# self.assertEquals(resp.status_code, 200) +# container = json.loads(resp.content) +# self.assertEquals(container, self.container) diff --git a/src/testapp/urls.py b/src/testapp/urls.py index 6f87c698c..16ea9a2f2 100644 --- a/src/testapp/urls.py +++ b/src/testapp/urls.py @@ -2,13 +2,18 @@ from django.conf.urls.defaults import patterns urlpatterns = patterns('testapp.views', (r'^$', 'RootResource'), - (r'^read-only$', 'ReadOnlyResource'), - (r'^write-only$', 'WriteOnlyResource'), - (r'^read-write$', 'ReadWriteResource'), - (r'^model$', 'ModelFormResource'), - (r'^container$', 'ContainerFactory'), - (r'^container/((?P[^/]+))$', 'ContainerInstance'), + #(r'^read-only$', 'ReadOnlyResource'), + #(r'^write-only$', 'WriteOnlyResource'), + #(r'^read-write$', 'ReadWriteResource'), + #(r'^model$', 'ModelFormResource'), + #(r'^container$', 'ContainerFactory'), + #(r'^container/((?P[^/]+))$', 'ContainerInstance'), - (r'^blogpost/create$', 'BlogPostCreator'), - (r'^blogposts/(?P[^/]+)', 'BlogPostInstance'), + (r'^blog-posts/$', 'BlogPostList'), + (r'^blog-post/$', 'BlogPostCreator'), + (r'^blog-post/(?P[^/]+)/$', 'BlogPostInstance'), + + (r'^blog-post/(?P[^/]+)/comments/$', 'CommentList'), + (r'^blog-post/(?P[^/]+)/comment/$', 'CommentCreator'), + (r'^blog-post/(?P[^/]+)/comments/(?P[^/]+)/$', 'CommentInstance'), ) diff --git a/src/testapp/views.py b/src/testapp/views.py index 33e56bbd0..eca69cc33 100644 --- a/src/testapp/views.py +++ b/src/testapp/views.py @@ -1,78 +1,111 @@ -from rest.resource import Resource, ModelResource -from testapp.forms import ExampleForm -from testapp.models import ExampleModel, ExampleContainer, BlogPost, Comment +from rest.resource import Resource, ModelResource, QueryModelResource +from testapp.models import BlogPost, Comment class RootResource(Resource): - """This is my docstring - """ + """This is the top level resource for the API. + All the sub-resources are discoverable from here.""" allowed_operations = ('read',) def read(self, headers={}, *args, **kwargs): - return (200, {'read-only-api': self.reverse(ReadOnlyResource), - 'write-only-api': self.reverse(WriteOnlyResource), - 'read-write-api': self.reverse(ReadWriteResource), - 'model-api': self.reverse(ModelFormResource), - 'create-container': self.reverse(ContainerFactory), - 'blog-post-creator': self.reverse(BlogPostCreator)}, {}) + return (200, {'blog-posts': self.reverse(BlogPostList), + 'blog-post': self.reverse(BlogPostCreator)}, {}) -class ReadOnlyResource(Resource): - """This is my docstring - """ - allowed_operations = ('read',) +# Blog Post Resources - def read(self, headers={}, *args, **kwargs): - return (200, {'ExampleString': 'Example', - 'ExampleInt': 1, - 'ExampleDecimal': 1.0}, {}) +class BlogPostList(QueryModelResource): + """A resource which lists all existing blog posts.""" + allowed_operations = ('read', ) + model = BlogPost -class WriteOnlyResource(Resource): - """This is my docstring - """ - allowed_operations = ('update',) - - def update(self, data, headers={}, *args, **kwargs): - return (200, data, {}) - - -class ReadWriteResource(Resource): - allowed_operations = ('read', 'update', 'delete') - create_form = ExampleForm - update_form = ExampleForm - - -class ModelFormResource(ModelResource): - allowed_operations = ('read', 'update', 'delete') - model = ExampleModel - -# Nice things: form validation is applied to any input type -# html forms for output -# output always serialized nicely -class ContainerFactory(ModelResource): +class BlogPostCreator(ModelResource): + """A resource with which blog posts may be created.""" allowed_operations = ('create',) - model = ExampleContainer - fields = ('absolute_uri', 'name', 'key') - form_fields = ('name',) + model = BlogPost + fields = ('created', 'title', 'slug', 'content', 'absolute_url', 'comment_url', 'comments_url') -class ContainerInstance(ModelResource): +class BlogPostInstance(ModelResource): + """A resource which represents a single blog post.""" allowed_operations = ('read', 'update', 'delete') - model = ExampleContainer - fields = ('absolute_uri', 'name', 'key') - form_fields = ('name',) + model = BlogPost + fields = ('created', 'title', 'slug', 'content', 'absolute_url', 'comment_url', 'comments_url') + + +# Comment Resources + +class CommentList(QueryModelResource): + """A resource which lists all existing comments for a given blog post.""" + allowed_operations = ('read', ) + model = Comment + + +class CommentCreator(ModelResource): + """A resource with which blog comments may be created for a given blog post.""" + allowed_operations = ('create',) + model = Comment + fields = ('username', 'comment', 'created', 'rating', 'absolute_url', 'blogpost_url') + + +class CommentInstance(ModelResource): + """A resource which represents a single comment.""" + allowed_operations = ('read', 'update', 'delete') + model = Comment + fields = ('username', 'comment', 'created', 'rating', 'absolute_url', 'blogpost_url') + +# +#'read-only-api': self.reverse(ReadOnlyResource), +# 'write-only-api': self.reverse(WriteOnlyResource), +# 'read-write-api': self.reverse(ReadWriteResource), +# 'model-api': self.reverse(ModelFormResource), +# 'create-container': self.reverse(ContainerFactory), +# +#class ReadOnlyResource(Resource): +# """This is my docstring +# """ +# allowed_operations = ('read',) +# +# def read(self, headers={}, *args, **kwargs): +# return (200, {'ExampleString': 'Example', +# 'ExampleInt': 1, +# 'ExampleDecimal': 1.0}, {}) +# +# +#class WriteOnlyResource(Resource): +# """This is my docstring +# """ +# allowed_operations = ('update',) +# +# def update(self, data, headers={}, *args, **kwargs): +# return (200, data, {}) +# +# +#class ReadWriteResource(Resource): +# allowed_operations = ('read', 'update', 'delete') +# create_form = ExampleForm +# update_form = ExampleForm +# +# +#class ModelFormResource(ModelResource): +# allowed_operations = ('read', 'update', 'delete') +# model = ExampleModel +# +## Nice things: form validation is applied to any input type +## html forms for output +## output always serialized nicely +#class ContainerFactory(ModelResource): +# allowed_operations = ('create',) +# model = ExampleContainer +# fields = ('absolute_uri', 'name', 'key') +# form_fields = ('name',) +# +# +#class ContainerInstance(ModelResource): +# allowed_operations = ('read', 'update', 'delete') +# model = ExampleContainer +# fields = ('absolute_uri', 'name', 'key') +# form_fields = ('name',) ####################### - -class BlogPostCreator(ModelResource): - """A Resource with which blog posts may be created. - This is distinct from blog post instance so that it is discoverable by the client. - (ie the client doens't need to know how to form a blog post url in order to create a blog post)""" - allowed_operations = ('create',) - model = BlogPost - -class BlogPostInstance(ModelResource): - """Represents a single Blog Post.""" - allowed_operations = ('read', 'update', 'delete') - model = BlogPost \ No newline at end of file