1 from django.conf.urls.defaults import *
2 from madrona.common.utils import get_logger, get_class, enable_sharing
3 from django.template.defaultfilters import slugify
4 from django.template import loader, TemplateDoesNotExist
5 from madrona.features.forms import FeatureForm
6 from django.core.urlresolvers import reverse
7 from django.contrib.contenttypes.models import ContentType
8 from django.db.models.signals import post_save, class_prepared
9 from django.dispatch import receiver
10 from django.contrib.auth.models import Permission, Group
11 from django.conf import settings
12 from django.db.utils import DatabaseError
13 import json
14
15 registered_models = []
16 registered_model_options = {}
17 registered_links = []
18 logger = get_logger()
22
24 """
25 Represents properties of Feature Classes derived from both defaults and
26 developer-specified options within the Options inner-class. These
27 properties drive the features of the spatial content managment system,
28 such as CRUD operations, copy, sharing, etc.
29
30 """
32
33
34 from madrona.features.models import Feature, FeatureCollection
35
36
37
38
39 if not issubclass(model, Feature):
40 raise FeatureConfigurationError('Is not a subclass of \
41 madrona.features.models.Feature')
42
43 self._model = model
44 name = model.__name__
45
46 if not getattr(model, 'Options', False):
47 raise FeatureConfigurationError(
48 'Have not defined Options inner-class on registered feature \
49 class %s' % (name, ))
50
51 self._options = model.Options
52
53 if not hasattr(self._options, 'form'):
54 raise FeatureConfigurationError(
55 "Feature class %s is not configured with a form class. \
56 To specify, add a `form` property to its Options inner-class." % (name,))
57
58 if not isinstance(self._options.form, str):
59 raise FeatureConfigurationError(
60 "Feature class %s is configured with a form property that is \
61 not a string path." % (name,))
62
63 self.form = self._options.form
64 """
65 Path to FeatureForm used to edit this class.
66 """
67
68 self.slug = slugify(name)
69 """
70 Name used in the url path to this feature as well as part of
71 the Feature's uid
72 """
73
74 self.verbose_name = getattr(self._options, 'verbose_name', name)
75 """
76 Name specified or derived from the feature class name used
77 in the user interface for representing this feature class.
78 """
79
80 self.form_template = getattr(self._options, 'form_template',
81 'features/form.html')
82 """
83 Location of the template that should be used to render forms
84 when editing or creating new instances of this feature class.
85 """
86
87 self.form_context = getattr(self._options, 'form_context', {})
88 """
89 Context to merge with default context items when rendering
90 templates to create or modify features of this class.
91 """
92
93 self.show_context = getattr(self._options, 'show_context', {})
94 """
95 Context to merge with default context items when rendering
96 templates to view information about instances of this feature class.
97 """
98
99 self.icon_url = getattr(self._options, 'icon_url', None)
100 """
101 Optional; URL to 16x16 icon to use in kmltree
102 Use full URL or relative to MEDIA_URL
103 """
104
105 self.links = []
106 """
107 Links associated with this class.
108 """
109
110 opts_links = getattr(self._options, 'links', False)
111 if opts_links:
112 self.links.extend(opts_links)
113
114 self.enable_copy = getattr(self._options, 'disable_copy', True)
115 """
116 Enable copying features. Uses the feature class' copy() method.
117 Defaults to True.
118 """
119
120
121 if self.enable_copy:
122 self.links.insert(0, edit('Copy',
123 'madrona.features.views.copy',
124 select='multiple single',
125 edits_original=False))
126
127 confirm = "Are you sure you want to delete this feature and it's contents?"
128
129
130
131
132
133
134
135
136
137
138
139
140 self.links.insert(0, edit('Delete',
141 'madrona.features.views.multi_delete',
142 select='multiple single',
143 method='DELETE',
144 edits_original=True,
145 confirm=confirm,
146 ))
147
148
149 export_png = getattr(self._options, 'export_png', True)
150 if export_png:
151 self.links.insert(0, alternate('PNG Image',
152 'madrona.staticmap.views.staticmap_link',
153 select='multiple single',
154 method='GET',
155 ))
156
157
158 export_geojson = getattr(self._options, 'export_geojson', True)
159 if export_geojson:
160 self.links.insert(0, alternate('GeoJSON',
161 'madrona.features.views.geojson_link',
162 select='multiple single',
163 method='GET',
164 ))
165
166 self.valid_children = getattr(self._options, 'valid_children', None)
167 """
168 valid child classes for the feature container
169 """
170 if self.valid_children and not issubclass(self._model, FeatureCollection):
171 raise FeatureConfigurationError("valid_children Option only \
172 for FeatureCollection classes" % m)
173
174 self.manipulators = []
175 """
176 Required manipulators applied to user input geometries
177 """
178 manipulators = getattr(self._options, 'manipulators', [])
179 for m in manipulators:
180 try:
181 manip = get_class(m)
182 except:
183 raise FeatureConfigurationError("Error trying to import module %s" % m)
184
185
186 geom_field = self._model.geometry_final._field.__class__.__name__
187 if geom_field not in manip.Options.supported_geom_fields:
188 raise FeatureConfigurationError("%s does not support %s geometry types (only %r)" %
189 (m, geom_field, manip.Options.supported_geom_fields))
190
191
192 self.manipulators.append(manip)
193
194 self.optional_manipulators = []
195 """
196 Optional manipulators that may be applied to user input geometries
197 """
198 optional_manipulators = getattr(self._options, 'optional_manipulators', [])
199 for m in optional_manipulators:
200 try:
201 manip = get_class(m)
202 except:
203 raise FeatureConfigurationError("Error trying to import module %s" % m)
204
205
206 geom_field = self._model.geometry_final._field.__class__.__name__
207 try:
208 if geom_field not in manip.Options.supported_geom_fields:
209 raise FeatureConfigurationError("%s does not support %s geometry types (only %r)" %
210 (m, geom_field, manip.Options.supported_geom_fields))
211 except AttributeError:
212 raise FeatureConfigurationError("%s is not set up properly; must have "
213 "Options.supported_geom_fields list." % m)
214
215
216 self.optional_manipulators.append(manip)
217
218 self.enable_kml = True
219 """
220 Enable kml visualization of features. Defaults to True.
221 """
222
223 if self.enable_kml:
224 self.links.insert(0,alternate('KML',
225 'madrona.features.views.kml',
226 select='multiple single'))
227 self.links.insert(0,alternate('KMZ',
228 'madrona.features.views.kmz',
229 select='multiple single'))
230
231 for link in self.links:
232 if self._model not in link.models:
233 link.models.append(self._model)
234
236 """
237 Returns the template used to render this Feature Class' attributes
238 """
239
240 template = getattr(self._options, 'show_template',
241 '%s/show.html' % (self.slug, ))
242 try:
243 t = loader.get_template(template)
244 except TemplateDoesNotExist:
245
246
247 t = loader.get_template('features/show.html')
248 return t
249
251 """
252 Returns the FeatureLink with the specified name
253 """
254 try:
255 link = [x for x in self.links if x.title == linkname][0]
256 return link
257 except:
258 raise Exception("%r has no link named %s" % (self._model, linkname))
259
281
283 """
284 It's not sufficient to look if this model is a valid_child of another
285 FeatureCollection; that collection could contain other collections
286 that contain this model.
287
288 Ex: Folder (only valid child is Array)
289 Array (only valid child is MPA)
290 Therefore, Folder is also a potential_parent of MPA
291 """
292 potential_parents = []
293 direct_parents = []
294 collection_models = get_collection_models()
295 for model in collection_models:
296 opts = model.get_options()
297 valid_children = opts.get_valid_children()
298
299 if self._model in valid_children:
300 direct_parents.append(model)
301 potential_parents.append(model)
302
303 for direct_parent in direct_parents:
304 if direct_parent != self._model:
305 potential_parents.extend(direct_parent.get_options().get_potential_parents())
306
307 return potential_parents
308
326
327 - def dict(self,user,is_owner):
328 """
329 Returns a json representation of this feature class configuration
330 that can be used to specify client behavior
331 """
332 placeholder = "%s_%d" % (self._model.model_uid(), 14)
333 link_rels = {
334 'id': self._model.model_uid(),
335 'title': self.verbose_name,
336 'link-relations': {
337 'self': {
338 'uri-template': reverse("%s_resource" % (self.slug, ),
339 args=[placeholder]).replace(placeholder, '{uid}'),
340 'title': settings.TITLES['self'],
341 },
342 }
343 }
344
345 if is_owner:
346 lr = link_rels['link-relations']
347 lr['create'] = {
348 'uri-template': reverse("%s_create_form" % (self.slug, ))
349 }
350
351 lr['edit'] = [
352 {'title': 'Edit',
353 'uri-template': reverse("%s_update_form" % (self.slug, ),
354 args=[placeholder]).replace(placeholder, '{uid}')
355 },
356 {'title': 'Share',
357 'uri-template': reverse("%s_share_form" % (self.slug, ),
358 args=[placeholder]).replace(placeholder, '{uid}')
359 }]
360
361 for link in self.links:
362 if not link.generic and link.can_user_view(user, is_owner):
363 if link.rel not in link_rels['link-relations'].keys():
364 if not (user.is_anonymous() and link.rel == 'edit'):
365 link_rels['link-relations'][link.rel] = []
366 link_rels['link-relations'][link.rel].append(link.dict(user,is_owner))
367
368 if self._model in get_collection_models() and is_owner:
369 link_rels['collection'] = {
370 'classes': [x.model_uid() for x in self.get_valid_children()],
371 'remove': {
372 'uri-template': reverse("%s_remove_features" % (self.slug, ),
373 kwargs={'collection_uid':14,'uids':'xx'}).replace('14', '{collection_uid}').replace('xx','{uid+}')
374 },
375 'add': {
376 'uri-template': reverse("%s_add_features" % (self.slug, ),
377 kwargs={'collection_uid':14,'uids':'xx'}).replace('14', '{collection_uid}').replace('xx','{uid+}')
378 }
379
380 }
381 return link_rels
382
385
391
398
404
406 """
407 Returns the primary url for a feature. This url supports GET, POST,
408 and DELETE operations.
409 """
410 return reverse('%s_resource' % (self.slug, ), args=['%s_%d' % (self._model.model_uid(), pk)])
411
413 - def __init__(self, rel, title, view, method='GET', select='single',
414 type=None, slug=None, generic=False, models=None, extra_kwargs={},
415 confirm=False, edits_original=None, must_own=False,
416 limit_to_groups=None):
417
418 self.rel = rel
419 """Type of link - alternate, related, edit, or edit_form.
420 """
421
422 try:
423 self.view = get_class(view)
424 """
425 View function handling requests to this link.
426 """
427 except Exception as err:
428 msg = 'Link "%s" configured with invalid path to view %s' % (title, view)
429 msg += '\n%s\n' % str(err)
430 if "cannot import" in str(err):
431 msg += "(Possible cause: importing Features at the top level in views.py can cause"
432 msg += " circular dependencies; Try to import Features within the view function)"
433
434 raise FeatureConfigurationError(msg)
435
436 self.title = title
437 """
438 Human-readable title for the link to be shown in the user interface.
439 """
440
441 self.method = method
442 """
443 For rel=edit links, identifies whether a form should be requested or
444 that url should just be POST'ed to.
445 """
446
447 self.type = type
448 """
449 MIME type of this link, useful for alternate links. May in the future
450 be used to automatically assign an icon in the dropdown Export menu.
451 """
452
453 self.slug = slug
454 """
455 Part of this link's path.
456 """
457
458 self.select = select
459 """
460 Determines whether this link accepts requests with single or multiple
461 instances of a feature class. Valid values are "single", "multiple",
462 "single multiple", and "multiple single".
463 """
464
465 self.extra_kwargs = extra_kwargs
466 """
467 Extra keyword arguments to pass to the view.
468 """
469
470 self.generic = generic
471 """
472 Whether this view can be applied to multiple feature classes.
473 """
474
475 self.models = models
476 """
477 List of feature classes that a this view can be applied to, if it is
478 generic.
479 """
480
481 self.confirm = confirm
482 """
483 Confirmation message to show the user before POSTing to rel=edit link
484 """
485
486 self.edits_original = edits_original
487 """
488 Set to false for editing links that create a copy of the original.
489 This will allow users who do not own the instance(s) but can view them
490 perform the action.
491 """
492
493 self.must_own = must_own
494 if self.edits_original:
495 self.must_own = True
496 """
497 Whether this link should be accessible to non-owners.
498 Default link behavior is False; i.e. Link can be used for shared features
499 as well as for user-owned features.
500 If edits_original is true, this implies must_own = True as well.
501 """
502
503 self.limit_to_groups = limit_to_groups
504 """
505 Allows you to specify groups (a list of group names)
506 that should have access to the link.
507 Default is None; i.e. All users have link access regardless of group membership
508 """
509
510 if self.models is None:
511 self.models = []
512
513
514 if self.title is '':
515 raise FeatureConfigurationError('Link title is empty')
516 valid_options = ('single', 'multiple', 'single multiple',
517 'multiple single')
518
519 if self.select not in valid_options:
520 raise FeatureConfigurationError(
521 'Link specified with invalid select option "%s"' % (
522 self.select, ))
523
524 if self.slug is None:
525 self.slug = slugify(title)
526
527 self._validate_view(self.view)
528
530 """
531 Ensures view has a compatible signature to be able to hook into the
532 features app url registration facilities
533
534 For single-select views
535 must accept a second argument named instance
536 For multiple-select views
537 must accept a second argument named instances
538
539 Must also ensure that if the extra_kwargs option is specified, the
540 view can handle them
541 """
542
543 if self.select is 'single':
544 args = view.__code__.co_varnames
545 if len(args) < 2 or args[1] != 'instance':
546 raise FeatureConfigurationError('Link "%s" not configured \
547 with a valid view. View must take a second argument named instance.' % (
548 self.title, ))
549 else:
550
551 args = view.__code__.co_varnames
552 if len(args) < 2 or args[1] != 'instances':
553 raise FeatureConfigurationError('Link "%s" not configured \
554 with a valid view. View must take a second argument named instances.' % (
555 self.title, ))
556
558 """
559 Returns True/False depending on whether user can view the link.
560 """
561 if self.limit_to_groups:
562
563 user_groupnames = [x.name for x in user.groups.all()]
564 match = False
565 for groupname in self.limit_to_groups:
566 if groupname in user_groupnames:
567 match = True
568 break
569 if not match:
570 return False
571
572 if self.must_own and not is_owner:
573 return False
574
575 return True
576
577 @property
579 """
580 Links are registered with named-urls. This function will return
581 that name so that it can be used in calls to reverse().
582 """
583 return "%s-%s" % (self.parent_slug, self.slug)
584
585 @property
587 """
588 Returns either the slug of the only model this view applies to, or
589 'generic'
590 """
591 if len(self.models) == 1:
592 return self.models[0].get_options().slug
593 else:
594 return 'generic-links'
595
597 """Can be used to get the url for this link.
598
599 In the case of select=single links, just pass in a single instance. In
600 the case of select=multiple links, pass in an array.
601 """
602 if not isinstance(instances,tuple) and not isinstance(instances,list):
603 instances = [instances]
604 uids = ','.join([instance.uid for instance in instances])
605 return reverse(self.url_name, kwargs={'uids': uids})
606
609
612
613 - def dict(self,user,is_owner):
614 d = {
615 'rel': self.rel,
616 'title': self.title,
617 'select': self.select,
618 'uri-template': reverse(self.url_name,
619 kwargs={'uids': 'idplaceholder'}).replace(
620 'idplaceholder', '{uid+}')
621 }
622 if self.rel == 'edit':
623 d['method'] = self.method
624 if len(self.models) > 1:
625 d['models'] = [m.model_uid() for m in self.models]
626 if self.confirm:
627 d['confirm'] = self.confirm
628 return d
629
632
634 nargs = [rel]
635 nargs.extend(args)
636 link = Link(*nargs, **kwargs)
637 must_match = ('rel', 'title', 'view', 'extra_kwargs', 'method', 'slug',
638 'select', 'must_own')
639 for registered_link in registered_links:
640 matches = True
641 for key in must_match:
642 if getattr(link, key) != getattr(registered_link, key):
643 matches = False
644 break
645 if matches:
646 registered_link.generic = True
647 return registered_link
648 registered_links.append(link)
649 return link
650
653
656
657 -def edit(*args, **kwargs):
658 if 'method' not in kwargs.keys():
659 kwargs['method'] = 'POST'
660 return create_link('edit', *args, **kwargs)
661
666
677
680
706
723
735
737 """
738 Returns a list of groups that user is member of and
739 and group must have sharing permissions
740 """
741 try:
742 p = Permission.objects.get(codename='can_share_features')
743 except Permission.DoesNotExist:
744 return None
745
746 groups = user.groups.filter(permissions=p).distinct()
747 return groups
748
750 """
751 Get a dict of groups and users that are currently sharing items with a given user
752 If spatial_only is True, only models which inherit from the Feature class will be reflected here
753 returns something like {'our_group': {'group': <Group our_group>, 'users': [<user1>, <user2>,...]}, ... }
754 """
755 groups_sharing = {}
756
757 for model_class in registered_models:
758 shared_objects = model_class.objects.shared_with_user(user)
759 for group in user.groups.all():
760
761 if group.name in settings.SHARING_TO_PUBLIC_GROUPS and not include_public:
762 continue
763
764 if group.name in settings.SHARING_TO_STAFF_GROUPS and not user.is_staff:
765 continue
766 group_objects = shared_objects.filter(sharing_groups=group)
767 user_list = []
768 for gobj in group_objects:
769 if gobj.user not in user_list and gobj.user != user:
770 user_list.append(gobj.user)
771
772 if len(user_list) > 0:
773 if group.name in groups_sharing.keys():
774 for user in user_list:
775 if user not in groups_sharing[group.name]['users']:
776 groups_sharing[group.name]['users'].append(user)
777 else:
778 groups_sharing[group.name] = {'group':group, 'users': user_list}
779 if len(groups_sharing.keys()) > 0:
780 return groups_sharing
781 else:
782 return None
783
789
791 applabel, modelname, id = uid.split('_')
792 id = int(id)
793 model = get_model_by_uid("%s_%s" % (applabel,modelname))
794 instance = model.objects.get(pk=int(id))
795 return instance
796