| 1 | from zope.traversing.browser.interfaces import IAbsoluteURL |
|---|
| 2 | from zope.dublincore.interfaces import ICMFDublinCore |
|---|
| 3 | |
|---|
| 4 | try: |
|---|
| 5 | from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile |
|---|
| 6 | raise Exception, "Five's ViewPageTemplateFile doesn't work with named templating" |
|---|
| 7 | except: |
|---|
| 8 | from zope.app.pagetemplate import ViewPageTemplateFile |
|---|
| 9 | |
|---|
| 10 | from zope.interface import implements |
|---|
| 11 | from zope.publisher.browser import BrowserPage |
|---|
| 12 | from zope.formlib.namedtemplate import NamedTemplate |
|---|
| 13 | from zope.formlib.namedtemplate import NamedTemplateImplementation |
|---|
| 14 | from zope.component import getMultiAdapter, queryAdapter |
|---|
| 15 | import zope.security.proxy |
|---|
| 16 | |
|---|
| 17 | from zgeo.geographer.interfaces import IGeoreferenced |
|---|
| 18 | from zgeo.atom.interfaces import IAtomBase, IEntry, ILink |
|---|
| 19 | from zgeo.atom.interfaces import IFeed, ISubscriptionFeed, ISearchFeed |
|---|
| 20 | from zgeo.atom.interfaces import IWriteAtomMetadata, IAtomPublishable |
|---|
| 21 | from zgeo.atom.interfaces import IAtomPubPOSTable |
|---|
| 22 | from zgeo.atom.link import Link |
|---|
| 23 | import zope.datetime |
|---|
| 24 | |
|---|
| 25 | # zgeo.spatialindex is required to use a bbox parameter with paging search |
|---|
| 26 | # feeds |
|---|
| 27 | try: |
|---|
| 28 | from zgeo.spatialindex.site import get_catalog |
|---|
| 29 | except ImportError: |
|---|
| 30 | def noner(arg): |
|---|
| 31 | return None |
|---|
| 32 | get_catalog = noner |
|---|
| 33 | |
|---|
| 34 | def coords_to_georss(geom): |
|---|
| 35 | gtype = geom.type |
|---|
| 36 | if gtype == 'Point': |
|---|
| 37 | coords = (geom.coordinates,) |
|---|
| 38 | elif gtype == 'Polygon': |
|---|
| 39 | coords = geom.coordinates[0] |
|---|
| 40 | else: |
|---|
| 41 | coords = geom.coordinates |
|---|
| 42 | tuples = ('%f %f' % (c[1], c[0]) for c in coords) |
|---|
| 43 | return ' '.join(tuples) |
|---|
| 44 | |
|---|
| 45 | def rfc3339(date): |
|---|
| 46 | ts = zope.datetime.time(date) |
|---|
| 47 | return zope.datetime.iso8601_date(ts) |
|---|
| 48 | |
|---|
| 49 | def absoluteURL(ob, request): |
|---|
| 50 | return getMultiAdapter((ob, request), IAbsoluteURL)() |
|---|
| 51 | |
|---|
| 52 | |
|---|
| 53 | class NullGeometry(object): |
|---|
| 54 | type = None |
|---|
| 55 | coordinates = None |
|---|
| 56 | |
|---|
| 57 | |
|---|
| 58 | class NullGeoItem(object): |
|---|
| 59 | id = None |
|---|
| 60 | properties = None |
|---|
| 61 | |
|---|
| 62 | def __init__(self): |
|---|
| 63 | self.geometry = NullGeometry() |
|---|
| 64 | |
|---|
| 65 | |
|---|
| 66 | class AtomBase(BrowserPage): |
|---|
| 67 | |
|---|
| 68 | """Not to be instantiated. |
|---|
| 69 | """ |
|---|
| 70 | implements(IAtomBase) |
|---|
| 71 | |
|---|
| 72 | @property |
|---|
| 73 | def id(self): |
|---|
| 74 | context = zope.security.proxy.removeSecurityProxy(self.context) |
|---|
| 75 | atom = IWriteAtomMetadata(context) |
|---|
| 76 | if atom.id is None: |
|---|
| 77 | return atom.setId() |
|---|
| 78 | return atom.id |
|---|
| 79 | |
|---|
| 80 | @property |
|---|
| 81 | def title(self): |
|---|
| 82 | return self.dc.Title() |
|---|
| 83 | |
|---|
| 84 | @property |
|---|
| 85 | def updated(self): |
|---|
| 86 | return rfc3339(self.dc.ModificationDate()) |
|---|
| 87 | |
|---|
| 88 | |
|---|
| 89 | @property |
|---|
| 90 | def author(self): |
|---|
| 91 | return { |
|---|
| 92 | 'name': self.dc.Creator() |
|---|
| 93 | , |
|---|
| 94 | 'uri': '', |
|---|
| 95 | 'email': '' |
|---|
| 96 | } |
|---|
| 97 | |
|---|
| 98 | @property |
|---|
| 99 | def links(self): |
|---|
| 100 | """Override this.""" |
|---|
| 101 | raise NotImplementedError |
|---|
| 102 | |
|---|
| 103 | |
|---|
| 104 | class LinkEntry(AtomBase): |
|---|
| 105 | |
|---|
| 106 | implements(IEntry) |
|---|
| 107 | |
|---|
| 108 | __name__ = 'atom-entry' |
|---|
| 109 | template = NamedTemplate('template-atom-entry') |
|---|
| 110 | |
|---|
| 111 | def __init__(self, context, request): |
|---|
| 112 | self.context = context |
|---|
| 113 | self.request = request |
|---|
| 114 | self.dc = zope.security.proxy.removeSecurityProxy( |
|---|
| 115 | ICMFDublinCore(self.context) |
|---|
| 116 | ) |
|---|
| 117 | try: |
|---|
| 118 | self.geom = IGeoreferenced(self.context) |
|---|
| 119 | except: |
|---|
| 120 | self.geom = NullGeometry() |
|---|
| 121 | |
|---|
| 122 | @property |
|---|
| 123 | def published(self): |
|---|
| 124 | return rfc3339(self.dc.CreationDate()) |
|---|
| 125 | |
|---|
| 126 | @property |
|---|
| 127 | def summary(self): |
|---|
| 128 | return self.dc.Description() |
|---|
| 129 | |
|---|
| 130 | @property |
|---|
| 131 | def links(self): |
|---|
| 132 | items = { |
|---|
| 133 | 'alternate': Link( |
|---|
| 134 | absoluteURL(self.context, self.request), |
|---|
| 135 | rel='alternate', |
|---|
| 136 | type='text/html') |
|---|
| 137 | } |
|---|
| 138 | if IAtomPublishable.providedBy(self.context): |
|---|
| 139 | items['edit'] = Link( |
|---|
| 140 | "%s/atom-entry" % absoluteURL(self.context, self.request), |
|---|
| 141 | rel='edit', |
|---|
| 142 | type='application/atom+xml;type=entry') |
|---|
| 143 | return items |
|---|
| 144 | |
|---|
| 145 | @property |
|---|
| 146 | def hasPoint(self): |
|---|
| 147 | return int(self.geom.type == 'Point') |
|---|
| 148 | |
|---|
| 149 | @property |
|---|
| 150 | def hasLineString(self): |
|---|
| 151 | return int(self.geom.type == 'LineString') |
|---|
| 152 | |
|---|
| 153 | @property |
|---|
| 154 | def hasPolygon(self): |
|---|
| 155 | return int(self.geom.type == 'Polygon') |
|---|
| 156 | |
|---|
| 157 | @property |
|---|
| 158 | def coords_georss(self): |
|---|
| 159 | return coords_to_georss(self.geom) |
|---|
| 160 | |
|---|
| 161 | def __call__(self): |
|---|
| 162 | return self.template().encode('utf-8') |
|---|
| 163 | |
|---|
| 164 | |
|---|
| 165 | class FeedBase(AtomBase): |
|---|
| 166 | |
|---|
| 167 | implements(IFeed) |
|---|
| 168 | |
|---|
| 169 | def __init__(self, context, request): |
|---|
| 170 | self.context = context |
|---|
| 171 | self.request = request |
|---|
| 172 | self.dc = zope.security.proxy.removeSecurityProxy( |
|---|
| 173 | ICMFDublinCore(self.context) |
|---|
| 174 | ) |
|---|
| 175 | |
|---|
| 176 | @property |
|---|
| 177 | def links(self): |
|---|
| 178 | raise NotImplementedError |
|---|
| 179 | |
|---|
| 180 | @property |
|---|
| 181 | def entries(self): |
|---|
| 182 | context = zope.security.proxy.removeSecurityProxy(self.context) |
|---|
| 183 | for item in context.values(): |
|---|
| 184 | yield LinkEntry(item, self.request) |
|---|
| 185 | |
|---|
| 186 | def collection_href(self): |
|---|
| 187 | if IAtomPubPOSTable.providedBy(self.context): |
|---|
| 188 | return '%s/atompub-collection' % absoluteURL( |
|---|
| 189 | self.context, self.request) |
|---|
| 190 | return None |
|---|
| 191 | |
|---|
| 192 | |
|---|
| 193 | class SubscriptionFeed(FeedBase): |
|---|
| 194 | |
|---|
| 195 | implements(ISubscriptionFeed) |
|---|
| 196 | |
|---|
| 197 | __name__ = 'atom-subscription-feed' |
|---|
| 198 | template = NamedTemplate('template-atom-subscription-feed') |
|---|
| 199 | |
|---|
| 200 | @property |
|---|
| 201 | def links(self): |
|---|
| 202 | return { |
|---|
| 203 | 'alternate': Link( |
|---|
| 204 | absoluteURL(self.context, self.request), |
|---|
| 205 | rel='alternate', |
|---|
| 206 | type='text/html' |
|---|
| 207 | ), |
|---|
| 208 | 'self': Link( |
|---|
| 209 | self.request.getURL(), |
|---|
| 210 | rel='self', |
|---|
| 211 | type='application/atom+xml' |
|---|
| 212 | ), |
|---|
| 213 | 'previous-archive': Link('None', rel='previous-archive') |
|---|
| 214 | } |
|---|
| 215 | |
|---|
| 216 | def __call__(self): |
|---|
| 217 | return self.template().encode('utf-8') |
|---|
| 218 | |
|---|
| 219 | |
|---|
| 220 | class SearchFeed(FeedBase): |
|---|
| 221 | |
|---|
| 222 | implements(ISearchFeed) |
|---|
| 223 | |
|---|
| 224 | __name__ = 'atom-search-feed' |
|---|
| 225 | template = NamedTemplate('template-atom-search-feed') |
|---|
| 226 | page_size = 20 |
|---|
| 227 | |
|---|
| 228 | def __init__(self, context, request): |
|---|
| 229 | self.context = context |
|---|
| 230 | self.request = request |
|---|
| 231 | self.dc = ICMFDublinCore(self.context) |
|---|
| 232 | self.catalog = None |
|---|
| 233 | self.bounds = None |
|---|
| 234 | self.page = 1 |
|---|
| 235 | self.num_results = 0 |
|---|
| 236 | self.num_pages = 0 |
|---|
| 237 | self.results = [] |
|---|
| 238 | |
|---|
| 239 | def parse_bbox(self, bbox=None): |
|---|
| 240 | if bbox is None: |
|---|
| 241 | b = self.request.form.get('bbox') |
|---|
| 242 | if b is None: |
|---|
| 243 | return None |
|---|
| 244 | else: |
|---|
| 245 | b = bbox |
|---|
| 246 | return tuple(float(x) for x in b.split(',')) |
|---|
| 247 | |
|---|
| 248 | def _first_link(self): |
|---|
| 249 | url = "%s/@@%s" % ( |
|---|
| 250 | absoluteURL(self.context, self.request), self.__name__ |
|---|
| 251 | ) |
|---|
| 252 | if self.bounds: |
|---|
| 253 | url = "%s?bbox=%f,%f,%f,%f" % ((url,) + self.bounds) |
|---|
| 254 | return url |
|---|
| 255 | |
|---|
| 256 | def _last_link(self): |
|---|
| 257 | url = "%s/@@%s?page=-1" % ( |
|---|
| 258 | absoluteURL(self.context, self.request), self.__name__ |
|---|
| 259 | ) |
|---|
| 260 | if self.bounds: |
|---|
| 261 | url = "%s&bbox=%f,%f,%f,%f" % ((url,) + self.bounds) |
|---|
| 262 | return url |
|---|
| 263 | |
|---|
| 264 | def _previous_link(self): |
|---|
| 265 | if self.page == 1: |
|---|
| 266 | return 'None' |
|---|
| 267 | url = "%s/@@%s?page=%d" % ( |
|---|
| 268 | absoluteURL(self.context, self.request), |
|---|
| 269 | self.__name__, |
|---|
| 270 | self.page - 1 |
|---|
| 271 | ) |
|---|
| 272 | if self.bounds: |
|---|
| 273 | url = "%s&bbox=%f,%f,%f,%f" % ((url,) + self.bounds) |
|---|
| 274 | return url |
|---|
| 275 | |
|---|
| 276 | def _next_link(self): |
|---|
| 277 | if self.page == -1 or self.page >= self.num_pages: |
|---|
| 278 | return 'None' |
|---|
| 279 | url = "%s/@@%s?page=%d" % ( |
|---|
| 280 | absoluteURL(self.context, self.request), |
|---|
| 281 | self.__name__, |
|---|
| 282 | self.page + 1 |
|---|
| 283 | ) |
|---|
| 284 | if self.bounds: |
|---|
| 285 | url = "%s&bbox=%f,%f,%f,%f" % ((url,) + self.bounds) |
|---|
| 286 | return url |
|---|
| 287 | |
|---|
| 288 | def update(self): |
|---|
| 289 | self.bounds = self.parse_bbox() |
|---|
| 290 | if self.bounds is not None: |
|---|
| 291 | self.catalog = get_catalog(self.context) |
|---|
| 292 | if self.catalog is None: |
|---|
| 293 | raise Exception, "Spatial search is not supported in the absence of a spatial catalog" |
|---|
| 294 | results = self.catalog.searchResults(bounds=self.bounds) |
|---|
| 295 | num_results = len(results) |
|---|
| 296 | self.results = results |
|---|
| 297 | else: |
|---|
| 298 | results = list(self.context.values()) |
|---|
| 299 | num_results = len(results) |
|---|
| 300 | self.results = results |
|---|
| 301 | |
|---|
| 302 | self.num_pages = num_results/self.page_size + num_results%self.page_size |
|---|
| 303 | self.num_results = num_results |
|---|
| 304 | |
|---|
| 305 | page = int(self.request.form.get('page', 1)) |
|---|
| 306 | if page > 1 and page > self.num_pages: |
|---|
| 307 | raise Exception, "Page number exceeds number of pages" |
|---|
| 308 | elif page < 0 and -page > self.num_pages: |
|---|
| 309 | raise Exception, "Page number exceeds number of pages" |
|---|
| 310 | else: |
|---|
| 311 | self.page = page |
|---|
| 312 | |
|---|
| 313 | @property |
|---|
| 314 | def entries(self): |
|---|
| 315 | if self.page >= 0: |
|---|
| 316 | begin = (self.page-1) * self.page_size |
|---|
| 317 | end = begin + self.page_size |
|---|
| 318 | else: |
|---|
| 319 | begin = self.num_results + (self.page * self.page_size) |
|---|
| 320 | end = begin + self.page_size |
|---|
| 321 | if end > self.num_results: end = self.num_results |
|---|
| 322 | for result in list(self.results)[begin:end]: |
|---|
| 323 | yield LinkEntry(result, self.request) |
|---|
| 324 | |
|---|
| 325 | @property |
|---|
| 326 | def links(self): |
|---|
| 327 | return { |
|---|
| 328 | 'alternate': Link( |
|---|
| 329 | absoluteURL(self.context, self.request), |
|---|
| 330 | rel='alternate', |
|---|
| 331 | type='text/html' |
|---|
| 332 | ), |
|---|
| 333 | 'self': Link( |
|---|
| 334 | '%s/@@%s' % (absoluteURL(self.context, self.request), self.__name__), |
|---|
| 335 | rel='self', |
|---|
| 336 | type='application/atom+xml' |
|---|
| 337 | ), |
|---|
| 338 | 'first': Link(self._first_link(), rel='first'), |
|---|
| 339 | 'last': Link(self._last_link(), rel='last'), |
|---|
| 340 | 'previous': Link(self._previous_link(), rel='previous'), |
|---|
| 341 | 'next': Link(self._next_link(), rel='next'), |
|---|
| 342 | } |
|---|
| 343 | |
|---|
| 344 | def __call__(self): |
|---|
| 345 | self.update() |
|---|
| 346 | return self.template().encode('utf-8') |
|---|
| 347 | |
|---|
| 348 | |
|---|
| 349 | # Named template implementations |
|---|
| 350 | |
|---|
| 351 | entry_template = NamedTemplateImplementation( |
|---|
| 352 | ViewPageTemplateFile('entry.pt') |
|---|
| 353 | ) |
|---|
| 354 | |
|---|
| 355 | subscription_feed_template = NamedTemplateImplementation( |
|---|
| 356 | ViewPageTemplateFile('subscription_feed.pt') |
|---|
| 357 | ) |
|---|
| 358 | |
|---|
| 359 | search_feed_template = NamedTemplateImplementation( |
|---|
| 360 | ViewPageTemplateFile('search_feed.pt') |
|---|
| 361 | ) |
|---|
| 362 | |
|---|