summaryrefslogtreecommitdiffstats
path: root/google-appengine/google/appengine/api/datastore_entities.py
blob: 93ffdb5f08b53cbf8f0e7c2d84f5d17ed0daba36 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
#!/usr/bin/env python
#
# Copyright 2007 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

"""Classes for common kinds, including Contact, Message, and Event.

Most of these kinds are based on the gd namespace "kinds" from GData:

  http://code.google.com/apis/gdata/common-elements.html
"""





import types
import urlparse
from xml.sax import saxutils
from google.appengine.datastore import datastore_pb
from google.appengine.api import datastore
from google.appengine.api import datastore_errors
from google.appengine.api import datastore_types

class GdKind(datastore.Entity):
  """ A base class for gd namespace kinds.

  This class contains common logic for all gd namespace kinds. For example,
  this class translates datastore (app id, kind, key) tuples to tag:
  URIs appropriate for use in <key> tags.
  """

  HEADER = u"""<entry xmlns:gd='http://schemas.google.com/g/2005'>
  <category scheme='http://schemas.google.com/g/2005#kind'
            term='http://schemas.google.com/g/2005#%s' />"""
  FOOTER = u"""
</entry>"""

  _kind_properties = set()
  _contact_properties = set()

  def __init__(self, kind, title, kind_properties, contact_properties=[]):
    """ Ctor.

    title is the name of this particular entity, e.g. Bob Jones or Mom's
    Birthday Party.

    kind_properties is a list of property names that should be included in
    this entity's XML encoding as first-class XML elements, instead of
    <property> elements. 'title' and 'content' are added to kind_properties
    automatically, and may not appear in contact_properties.

    contact_properties is a list of property names that are Keys that point to
    Contact entities, and should be included in this entity's XML encoding as
    <gd:who> elements. If a property name is included in both kind_properties
    and contact_properties, it is treated as a Contact property.

    Args:
    kind: string
    title: string
    kind_properties: list of strings
    contact_properties: list of string
    """
    datastore.Entity.__init__(self, kind)

    if not isinstance(title, types.StringTypes):
      raise datastore_errors.BadValueError(
        'Expected a string for title; received %s (a %s).' %
        (title, datastore_types.typename(title)))
    self['title'] = title
    self['content'] = ''

    self._contact_properties = set(contact_properties)
    assert not self._contact_properties.intersection(self.keys())

    self._kind_properties = set(kind_properties) - self._contact_properties
    self._kind_properties.add('title')
    self._kind_properties.add('content')

  def _KindPropertiesToXml(self):
    """ Convert the properties that are part of this gd kind to XML. For
    testability, the XML elements in the output are sorted alphabetically
    by property name.

    Returns:
    string  # the XML representation of the gd kind properties
    """
    properties = self._kind_properties.intersection(set(self.keys()))

    xml = u''
    for prop in sorted(properties):
      prop_xml = saxutils.quoteattr(prop)[1:-1]

      value = self[prop]
      has_toxml = (hasattr(value, 'ToXml') or
                   isinstance(value, list) and hasattr(value[0], 'ToXml'))

      for val in self._XmlEscapeValues(prop):
        if has_toxml:
          xml += '\n  %s' % val
        else:
          xml += '\n  <%s>%s</%s>' % (prop_xml, val, prop_xml)

    return xml


  def _ContactPropertiesToXml(self):
    """ Convert this kind's Contact properties kind to XML. For testability,
    the XML elements in the output are sorted alphabetically by property name.

    Returns:
    string  # the XML representation of the Contact properties
    """
    properties = self._contact_properties.intersection(set(self.keys()))

    xml = u''
    for prop in sorted(properties):
      values = self[prop]
      if not isinstance(values, list):
        values = [values]

      for value in values:
        assert isinstance(value, datastore_types.Key)
        xml += """
  <gd:who rel="http://schemas.google.com/g/2005#%s.%s>
    <gd:entryLink href="%s" />
  </gd:who>""" % (self.kind().lower(), prop, value.ToTagUri())

    return xml


  def _LeftoverPropertiesToXml(self):
    """ Convert all of this entity's properties that *aren't* part of this gd
    kind to XML.

    Returns:
    string  # the XML representation of the leftover properties
    """
    leftovers = set(self.keys())
    leftovers -= self._kind_properties
    leftovers -= self._contact_properties
    if leftovers:
      return u'\n  ' + '\n  '.join(self._PropertiesToXml(leftovers))
    else:
      return u''

  def ToXml(self):
    """ Returns an XML representation of this entity, as a string.
    """
    xml = GdKind.HEADER % self.kind().lower()
    xml += self._KindPropertiesToXml()
    xml += self._ContactPropertiesToXml()
    xml += self._LeftoverPropertiesToXml()
    xml += GdKind.FOOTER
    return xml


class Message(GdKind):
  """A message, such as an email, a discussion group posting, or a comment.

  Includes the message title, contents, participants, and other properties.

  This is the gd Message kind. See:
  http://code.google.com/apis/gdata/common-elements.html#gdMessageKind

  These properties are meaningful. They are all optional.

  property name  property type    meaning
  -------------------------------------
  title          string         message subject
  content        string         message body
  from           Contact*       sender
  to             Contact*       primary recipient
  cc             Contact*       CC recipient
  bcc            Contact*       BCC recipient
  reply-to       Contact*       intended recipient of replies
  link           Link*          attachment
  category       Category*      tag or label associated with this message
  geoPt          GeoPt*         geographic location the message was posted from
  rating         Rating*        message rating, as defined by the application

  * means this property may be repeated.

  The Contact properties should be Keys of Contact entities. They are
  represented in the XML encoding as linked <gd:who> elements.
  """
  KIND_PROPERTIES = ['title', 'content', 'link', 'category', 'geoPt', 'rating']
  CONTACT_PROPERTIES = ['from', 'to', 'cc', 'bcc', 'reply-to']

  def __init__(self, title, kind='Message'):
    GdKind.__init__(self, kind, title, Message.KIND_PROPERTIES,
                    Message.CONTACT_PROPERTIES)


class Event(GdKind):
  """A calendar event.

  Includes the event title, description, location, organizer, start and end
  time, and other details.

  This is the gd Event kind. See:
  http://code.google.com/apis/gdata/common-elements.html#gdEventKind

  These properties are meaningful. They are all optional.

  property name  property type    meaning
  -------------------------------------
  title          string         event name
  content        string         event description
  author         string         the organizer's name
  where          string*        human-readable location (not a GeoPt)
  startTime      timestamp      start time
  endTime        timestamp      end time
  eventStatus    string         one of the Event.Status values
  link           Link*          page with more information
  category       Category*      tag or label associated with this event
  attendee       Contact*       attendees and other related people

  * means this property may be repeated.

  The Contact properties should be Keys of Contact entities. They are
  represented in the XML encoding as linked <gd:who> elements.
  """
  KIND_PROPERTIES = ['title', 'content', 'author', 'where', 'startTime',
                     'endTime', 'eventStatus', 'link', 'category']
  CONTACT_PROPERTIES = ['attendee']

  class Status:
    CONFIRMED = 'confirmed'
    TENTATIVE = 'tentative'
    CANCELED = 'canceled'

  def __init__(self, title, kind='Event'):
    GdKind.__init__(self, kind, title, Event.KIND_PROPERTIES,
                    Event.CONTACT_PROPERTIES)

  def ToXml(self):
    """ Override GdKind.ToXml() to special-case author, gd:where, gd:when, and
    gd:eventStatus.
    """
    xml = GdKind.HEADER % self.kind().lower()

    self._kind_properties = set(Contact.KIND_PROPERTIES)
    xml += self._KindPropertiesToXml()

    if 'author' in self:
      xml += """
  <author><name>%s</name></author>""" % self['author']

    if 'eventStatus' in self:
      xml += """
  <gd:eventStatus value="http://schemas.google.com/g/2005#event.%s" />""" % (
    self['eventStatus'])

    if 'where' in self:
      lines = ['<gd:where valueString="%s" />' % val
               for val in self._XmlEscapeValues('where')]
      xml += '\n  ' + '\n  '.join(lines)

    iso_format = '%Y-%m-%dT%H:%M:%S'
    xml += '\n  <gd:when'
    for key in ['startTime', 'endTime']:
      if key in self:
        xml += ' %s="%s"' % (key, self[key].isoformat())
    xml += ' />'

    self._kind_properties.update(['author', 'where', 'startTime', 'endTime',
                                  'eventStatus'])
    xml += self._ContactPropertiesToXml()
    xml += self._LeftoverPropertiesToXml()
    xml += GdKind.FOOTER
    return xml


class Contact(GdKind):
  """A contact: a person, a venue such as a club or a restaurant, or an
  organization.

  This is the gd Contact kind. See:
  http://code.google.com/apis/gdata/common-elements.html#gdContactKind

  Most of the information about the contact is in the <gd:contactSection>
  element; see the reference section for that element for details.

  These properties are meaningful. They are all optional.

  property name  property type    meaning
  -------------------------------------
  title          string         contact's name
  content        string         notes
  email          Email*         email address
  geoPt          GeoPt*         geographic location
  im             IM*            IM address
  phoneNumber    Phonenumber*   phone number
  postalAddress  PostalAddress* mailing address
  link           Link*          link to more information
  category       Category*      tag or label associated with this contact

  * means this property may be repeated.
  """
  CONTACT_SECTION_HEADER = """
  <gd:contactSection>"""
  CONTACT_SECTION_FOOTER = """
  </gd:contactSection>"""

  KIND_PROPERTIES = ['title', 'content', 'link', 'category']

  CONTACT_SECTION_PROPERTIES = ['email', 'geoPt', 'im', 'phoneNumber',
                                'postalAddress']

  def __init__(self, title, kind='Contact'):
    GdKind.__init__(self, kind, title, Contact.KIND_PROPERTIES)

  def ToXml(self):
    """ Override GdKind.ToXml() to put some properties inside a
    gd:contactSection.
    """
    xml = GdKind.HEADER % self.kind().lower()

    self._kind_properties = set(Contact.KIND_PROPERTIES)
    xml += self._KindPropertiesToXml()

    xml += Contact.CONTACT_SECTION_HEADER
    self._kind_properties = set(Contact.CONTACT_SECTION_PROPERTIES)
    xml += self._KindPropertiesToXml()
    xml += Contact.CONTACT_SECTION_FOOTER

    self._kind_properties.update(Contact.KIND_PROPERTIES)
    xml += self._LeftoverPropertiesToXml()
    xml += GdKind.FOOTER
    return xml