30
27
from zope.proxy import isProxy
31
28
from zope.publisher.interfaces import NotFound
32
29
from zope.schema import ValidationError, getFields
33
from zope.schema.interfaces import IDatetime, IObject
30
from zope.schema.interfaces import ConstraintNotSatisfied, IChoice, IObject
34
31
from zope.security.proxy import removeSecurityProxy
36
32
from canonical.lazr.enum import BaseItem
38
34
# XXX leonardr 2008-01-25 bug=185958:
42
38
from canonical.launchpad.webapp.interfaces import ICanonicalUrlData
43
39
from canonical.lazr.interfaces import (
44
40
ICollection, ICollectionField, ICollectionResource, IEntry,
45
IEntryResource, IHTTPResource, IJSONPublishable, IResourceGETOperation,
46
IResourcePOSTOperation, IScopedCollection, IServiceRootResource)
41
IEntryResource, IFieldDeserializer, IHTTPResource, IJSONPublishable,
42
IResourceGETOperation, IResourcePOSTOperation, IScopedCollection,
44
from canonical.launchpad.webapp.vocabulary import SQLObjectVocabularyBase
47
45
from canonical.lazr.rest.schema import URLDereferencingMixin
120
118
IResourcePOSTOperation)
121
119
return len(adapters) > 0
121
def toWADL(self, template_name):
122
"""Represent this resource as a WADL application.
124
The WADL document describes the capabilities of this resource.
126
template = LazrPageTemplateFile('../templates/' + template_name)
127
namespace = template.pt_getContext()
128
namespace['context'] = self
129
return template.pt_render(namespace)
131
def getPreferredSupportedContentType(self):
132
"""Of the content types we serve, which would the client prefer?
134
The web service supports WADL and JSON representations. The
135
default is JSON. This method determines whether the client
136
would rather have WADL or JSON.
138
content_types = self.getPreferredContentTypes()
140
wadl_pos = content_types.index(self.WADL_TYPE)
142
wadl_pos = float("infinity")
144
json_pos = content_types.index(self.JSON_TYPE)
146
json_pos = float("infinity")
147
if wadl_pos < json_pos:
148
return self.WADL_TYPE
149
return self.JSON_TYPE
123
151
def getPreferredContentTypes(self):
124
152
"""Find which content types the client prefers to receive."""
125
153
return self._parseAcceptStyleHeader(self.request.get('HTTP_ACCEPT'))
156
def _fieldValueIsObject(self, field):
157
"""Does the given field expect a data model object as its value?
159
Obviously an IObject field is expected to have a data model
160
object as its value. But an IChoice field might also have a
161
vocabulary drawn from the set of data model objects.
163
if IObject.providedBy(field):
165
if IChoice.providedBy(field):
166
# Find out whether the field's vocabulary is made of
167
# database objects (which correspond to resources that
168
# need to be linked to) or regular objects (which can
169
# be serialized to JSON).
170
field = field.bind(self.context)
171
return isinstance(field.vocabulary, SQLObjectVocabularyBase)
127
174
def _parseAcceptStyleHeader(self, value):
128
175
"""Parse an HTTP header from the Accept-* family.
351
398
data['self_link'] = canonical_url(self.context)
352
399
for name, field in getFields(self.entry.schema).items():
353
400
value = getattr(self.entry, name)
354
402
if ICollectionField.providedBy(field):
355
403
# The field is a collection; include a link to the
356
404
# collection resource.
357
405
if value is not None:
358
406
key = name + '_collection_link'
359
407
data[key] = "%s/%s" % (data['self_link'], name)
360
elif IObject.providedBy(field):
408
elif self._fieldValueIsObject(field):
361
409
# The field is an entry; include a link to the
362
410
# entry resource.
363
411
if value is not None:
411
459
# No custom operation was specified. Implement a standard
412
460
# GET, which serves a JSON or WADL representation of the
414
content_types = self.getPreferredContentTypes()
416
wadl_pos = content_types.index(self.WADL_TYPE)
418
wadl_pos = float("infinity")
420
json_pos = content_types.index(self.JSON_TYPE)
422
json_pos = float("infinity")
424
# If the client's desire for WADL outranks its desire for
425
# JSON, serve WADL. Otherwise, serve JSON.
426
if wadl_pos < json_pos:
462
if self.getPreferredSupportedContentType() == self.WADL_TYPE:
427
463
result = self.toWADL().encode("utf-8")
428
464
self.request.response.setHeader(
429
465
'Content-Type', self.WADL_TYPE)
497
530
validated_changeset = {}
498
532
for repr_name, value in changeset.items():
499
533
if repr_name == 'self_link':
500
534
# The self link isn't part of the schema, so it's
501
535
# handled separately.
502
if value == canonical_url(self.context):
505
self.request.response.setStatus(400)
506
return ("You tried to modify the read-only attribute "
536
if value != canonical_url(self.context):
537
errors.append("self_link: You tried to modify "
538
"a read-only attribute.")
509
541
change_this_field = True
532
564
# (Of course, you also can't change
533
565
# 'foo_collection_link', but that's taken care of
535
self.request.response.setStatus(400)
536
return ("You tried to modify the nonexistent attribute '%s'"
567
errors.append("%s: You tried to modify a nonexistent "
568
"attribute." % repr_name)
539
571
# Around this point the specific value provided by the client
540
# becomes relevant, so we pre-process it if necessary.
572
# becomes relevant, so we deserialize it.
573
element = element.bind(self.context)
574
deserializer = getMultiAdapter((element, self.request),
577
value = deserializer.deserialize(value)
578
except (ValueError, ValidationError), e:
579
errors.append("%s: %s" % (repr_name, e))
541
582
if (IObject.providedBy(element)
542
583
and not ICollectionField.providedBy(element)):
543
# 'value' is the URL to an object. Dereference the URL
544
# to find the actual object.
546
value = self.dereference_url(value)
548
self.request.response.setStatus(400)
549
return ("Your value for the attribute '%s' wasn't "
550
"the URL to any object published by this web "
551
"service." % repr_name)
552
underlying_object = removeSecurityProxy(value)
553
value = underlying_object.context
554
# The URL points to an object, but is it an object of the
584
# TODO leonardr 2008-15-04
585
# blueprint=api-wadl-description: This should be moved
586
# into the ObjectLookupFieldDeserializer, once we make
587
# it possible for Vocabulary fields to specify a
588
# schema class the way IObject fields can.
556
589
if not element.schema.providedBy(value):
557
self.request.response.setStatus(400)
558
return ("Your value for the attribute '%s' doesn't "
559
"point to the right kind of object." % repr_name)
560
elif IDatetime.providedBy(element):
562
value = DateTimeParser().parse(value)
563
(year, month, day, hours, minutes, secondsAndMicroseconds,
565
seconds = int(secondsAndMicroseconds)
567
round((secondsAndMicroseconds - seconds) * 1000000))
568
if timezone not in ['Z', '+0000', '-0000']:
569
self.request.response.setStatus(400)
570
return ("You set the attribute '%s' to a time "
573
value = datetime(year, month, day, hours, minutes,
574
seconds, microseconds, pytz.utc)
575
except (DateError, DateTimeError, SyntaxError):
576
self.request.response.setStatus(400)
577
return ("You set the attribute '%s' to a value "
578
"that doesn't look like a date." % repr_name)
590
errors.append("%s: Your value points to the "
591
"wrong kind of object" % repr_name)
580
594
# The current value of the attribute also becomes
581
595
# relevant, so we obtain that. If the attribute designates
597
611
if ICollectionField.providedBy(element):
598
612
change_this_field = False
599
613
if value != current_value:
600
self.request.response.setStatus(400)
601
return ("You tried to modify the collection link '%s'"
614
errors.append("%s: You tried to modify a collection "
615
"attribute." % repr_name)
604
618
if element.readonly:
605
619
change_this_field = False
606
620
if value != current_value:
607
self.request.response.setStatus(400)
608
return ("You tried to modify the read-only attribute '%s'"
621
errors.append("%s: You tried to modify a read-only "
622
"attribute." % repr_name)
611
625
if change_this_field is True and value != current_value:
612
626
if not IObject.providedBy(element):
614
628
# Do any field-specific validation.
615
field = element.bind(self.context)
616
field.validate(value)
617
except ValidationError, e:
618
self.request.response.setStatus(400)
629
element.validate(value)
630
except ConstraintNotSatisfied, e:
631
# Try to get a string error message out of
632
# the exception; otherwise use a generic message
633
# instead of whatever object the raise site
634
# thought would be a good idea.
635
if (len(e.args) > 0 and
636
isinstance(e.args[0], basestring)):
639
error = "Constraint not satisfied."
640
errors.append("%s: %s" % (repr_name, error))
642
except (ValueError, ValidationError), e:
621
645
error = "Validation error"
646
errors.append("%s: %s" % (repr_name, error))
623
648
validated_changeset[name] = value
650
# If there were errors, display them and send a status of 400.
652
self.request.response.setStatus(400)
653
self.request.response.setHeader('Content-type', 'text/plain')
654
return "\n".join(errors)
625
656
# Store the entry's current URL so we can see if it changes.
626
657
original_url = canonical_url(self.context)
627
658
# Make the changes.
657
688
# just needs this string served to the client.
660
# No custom operation was specified. Implement a standard GET,
661
# which retrieves the items in the collection.
691
# No custom operation was specified. Implement a standard
692
# GET, which serves a JSON or WADL representation of the
662
694
entries = self.collection.find()
663
695
if entries is None:
664
696
raise NotFound(self, self.collection_name)
698
if self.getPreferredSupportedContentType() == self.WADL_TYPE:
699
result = self.toWADL().encode("utf-8")
700
self.request.response.setHeader(
701
'Content-Type', self.WADL_TYPE)
665
703
result = self.batch(entries, self.request)
667
705
self.request.response.setHeader('Content-type', self.JSON_TYPE)
668
706
return simplejson.dumps(result, cls=ResourceJSONEncoder)
709
"""Represent this resource as a WADL application.
711
The WADL document describes the capabilities of this resource.
713
return super(CollectionResource, self).toWADL('wadl-collection.pt')
671
716
class ServiceRootResource:
672
717
"""A resource that responds to GET by describing the service."""