Package madrona :: Package features
[hide private]

Source Code for Package madrona.features

  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() 
19 20 -class FeatureConfigurationError(Exception):
21 pass
22
23 -class FeatureOptions:
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 """
31 - def __init__(self, model):
32 33 # Import down here to avoid circular reference 34 from madrona.features.models import Feature, FeatureCollection 35 36 # call this here to ensure that permsissions get created 37 #enable_sharing() 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 # Add a copy method unless disabled 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 # Add a multi-share generic link 130 # TODO when the share_form view takes multiple instances 131 # we can make sharing a generic link 132 #self.links.insert(0, edit('Share', 133 # 'madrona.features.views.share_form', 134 # select='multiple single', 135 # method='POST', 136 # edits_original=True, 137 #)) 138 139 # Add a multi-delete generic link 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 # Add a staticmap generic link 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 # Add a geojson generic link 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 # Test that manipulator is compatible with this Feature Class 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 #logger.debug("Added required manipulator %s" % m) 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 # Test that manipulator is compatible with this Feature Class 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 #logger.debug("Added optional manipulator %s" % m) 216 self.optional_manipulators.append(manip) 217 218 self.enable_kml = True 219 """ 220 Enable kml visualization of features. Defaults to True. 221 """ 222 # Add a kml link by default 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
235 - def get_show_template(self):
236 """ 237 Returns the template used to render this Feature Class' attributes 238 """ 239 # Grab a template specified in the Options object, or use the default 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 # If a template has not been created, use a stub that displays 246 # some documentation on how to override the default template 247 t = loader.get_template('features/show.html') 248 return t
249 259
260 - def get_valid_children(self):
261 if not self.valid_children: 262 raise FeatureConfigurationError( 263 "%r is not a properly configured FeatureCollection" % (self._model)) 264 265 valid_child_classes = [] 266 for vc in self.valid_children: 267 try: 268 vc_class = get_class(vc) 269 except: 270 raise FeatureConfigurationError( 271 "Error trying to import module %s" % vc) 272 273 from madrona.features.models import Feature 274 if not issubclass(vc_class, Feature): 275 raise FeatureConfigurationError( 276 "%r is not a Feature; can't be a child" % vc) 277 278 valid_child_classes.append(vc_class) 279 280 return valid_child_classes
281
282 - def get_potential_parents(self):
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
309 - def get_form_class(self):
310 """ 311 Returns the form class for this Feature Class. 312 """ 313 try: 314 klass = get_class(self.form) 315 except Exception, e: 316 raise FeatureConfigurationError( 317 "Feature class %s is not configured with a valid form class. \ 318 Could not import %s.\n%s" % (self._model.__name__, self.form, e)) 319 320 if not issubclass(klass, FeatureForm): 321 raise FeatureConfigurationError( 322 "Feature class %s's form is not a subclass of \ 323 madrona.features.forms.FeatureForm." % (self._model.__name__, )) 324 325 return klass
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
383 - def json(self):
384 return json.dumps(self.dict())
385
386 - def get_create_form(self):
387 """ 388 Returns the path to a form for creating new instances of this model 389 """ 390 return reverse('%s_create_form' % (self.slug, ))
391
392 - def get_update_form(self, pk):
393 """ 394 Given a primary key, returns the path to a form for updating a Feature 395 Class 396 """ 397 return reverse('%s_update_form' % (self.slug, ), args=['%s_%d' % (self._model.model_uid(), pk)])
398
399 - def get_share_form(self, pk):
400 """ 401 Given a primary key, returns path to a form for sharing a Feature inst 402 """ 403 return reverse('%s_share_form' % (self.slug, ), args=['%s_%d' % (self._model.model_uid(), pk)])
404
405 - def get_resource(self, pk):
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 632 650
651 -def alternate(*args, **kwargs):
652 return create_link('alternate', *args, **kwargs)
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
662 -def edit_form(*args, **kwargs):
663 if 'method' not in kwargs.keys(): 664 kwargs['method'] = 'GET' 665 return create_link('edit', *args, **kwargs)
666
667 -def register(model):
668 options = FeatureOptions(model) 669 logger.debug('registering Feature %s' % (model.__name__,)) 670 if model not in registered_models: 671 registered_models.append(model) 672 registered_model_options[model.__name__] = options 673 for link in options.links: 674 if link not in registered_links: 675 registered_links.append(link) 676 return model
677
678 -def get_model_options(model_name):
679 return registered_model_options[model_name]
680
681 -def workspace_json(user, is_owner, models=None):
682 workspace = { 683 'feature-classes': [], 684 'generic-links': [] 685 } 686 if not models: 687 # Workspace doc gets ALL feature classes and registered links 688 for model in registered_models: 689 workspace['feature-classes'].append(model.get_options().dict(user, is_owner)) 690 for link in registered_links: 691 if link.generic and link.can_user_view(user, is_owner) \ 692 and not (user.is_anonymous() and link.rel == 'edit'): 693 workspace['generic-links'].append(link.dict(user, is_owner)) 694 else: 695 # Workspace doc only reflects specified feature class models 696 for model in models: 697 workspace['feature-classes'].append(model.get_options().dict(user, is_owner)) 698 for link in registered_links: 699 # See if the generic links are relavent to this list 700 if link.generic and \ 701 [i for i in args if i in link.models] and \ 702 link.can_user_view(user, is_owner) and \ 703 not (user.is_anonymous() and link.rel == 'edit'): 704 workspace['generic-links'].append(link.dict(user, is_owner)) 705 return json.dumps(workspace, indent=2)
706
707 -def get_collection_models():
708 """ 709 Utility function returning models for 710 registered and valid FeatureCollections 711 """ 712 from madrona.features.models import FeatureCollection 713 registered_collections = [] 714 for model in registered_models: 715 if issubclass(model,FeatureCollection): 716 opts = model.get_options() 717 try: 718 assert len(opts.get_valid_children()) > 0 719 registered_collections.append(model) 720 except: 721 pass 722 return registered_collections
723
724 -def get_feature_models():
725 """ 726 Utility function returning models for 727 registered and valid Features excluding Collections 728 """ 729 from madrona.features.models import Feature, FeatureCollection 730 registered_features = [] 731 for model in registered_models: 732 if issubclass(model,Feature) and not issubclass(model,FeatureCollection): 733 registered_features.append(model) 734 return registered_features
735
736 -def user_sharing_groups(user):
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
749 -def groups_users_sharing_with(user, include_public=False):
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 # Unless overridden, public shares don't show up here 761 if group.name in settings.SHARING_TO_PUBLIC_GROUPS and not include_public: 762 continue 763 # User has to be staff to see these 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
784 -def get_model_by_uid(muid):
785 for model in registered_models: 786 if model.model_uid() == muid: 787 return model 788 raise Exception("No model with model_uid == `%s`" % muid)
789
790 -def get_feature_by_uid(uid):
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