Package madrona :: Package features :: Module models
[hide private]

Source Code for Module madrona.features.models

  1  from django.contrib.gis.db import models 
  2  from django.contrib.auth.models import User, Group 
  3  from django.conf import settings 
  4  from django.contrib.contenttypes.models import ContentType 
  5  from django.contrib.contenttypes import generic 
  6  from django.http import HttpResponse 
  7  from django.utils.html import escape 
  8  from madrona.features.managers import ShareableGeoManager 
  9  from madrona.features.forms import FeatureForm 
 10  from madrona.features import get_model_options 
 11  from madrona.common.utils import asKml, clean_geometry, ensure_clean 
 12  from madrona.common.utils import get_logger, get_class, enable_sharing 
 13  from madrona.manipulators.manipulators import manipulatorsDict, NullManipulator 
 14  import re 
 15  import mapnik 
 16   
 17  logger = get_logger() 
18 19 -class Feature(models.Model):
20 """Model used for representing user-generated features 21 22 ====================== ============================================== 23 Attribute Description 24 ====================== ============================================== 25 ``user`` Creator 26 27 ``name`` Name of the object 28 29 ``date_created`` When it was created 30 31 ``date_modified`` When it was last updated. 32 ====================== ============================================== 33 """ 34 user = models.ForeignKey(User, related_name="%(app_label)s_%(class)s_related") 35 name = models.CharField(verbose_name="Name", max_length="255") 36 date_created = models.DateTimeField(auto_now_add=True, 37 verbose_name="Date Created") 38 date_modified = models.DateTimeField(auto_now=True, 39 verbose_name="Date Modified") 40 sharing_groups = models.ManyToManyField(Group,editable=False,blank=True, 41 null=True,verbose_name="Share with the following groups", 42 related_name="%(app_label)s_%(class)s_related") 43 content_type = models.ForeignKey(ContentType, blank=True, null=True, 44 related_name="%(app_label)s_%(class)s_related") 45 object_id = models.PositiveIntegerField(blank=True,null=True) 46 collection = generic.GenericForeignKey('content_type', 'object_id') 47 48 objects = ShareableGeoManager() 49
50 - def __unicode__(self):
51 return u"%s_%s" % (self.model_uid(), self.pk)
52
53 - def __repr__(self):
54 return u"%s_%s" % (self.model_uid(), self.pk)
55
56 - class Meta:
57 abstract = True
58 59 ''' 60 Note on keyword args rerun and form: These are extracted from kwargs so that they will not cause an unexpected 61 keyword argument error during call to super.save. (They are used in the Analysis model save method, but become 62 superfluous here.) 63 '''
64 - def save(self, rerun=True, form=None, *args, **kwargs):
65 super(Feature, self).save(*args, **kwargs) # Call the "real" save() method 66 if form is not None: 67 form.save_m2m()
68 69 @models.permalink
70 - def get_absolute_url(self):
71 return ('%s_resource' % (self.get_options().slug, ), (), { 72 'uid': self.uid 73 })
74 75 @classmethod
76 - def get_options(klass):
77 """ 78 Returns model class Options object 79 """ 80 return get_model_options(klass.__name__)
81 82 @classmethod
83 - def css(klass):
84 """ 85 Specifies the CSS for representing features in kmltree, specifically the icon 86 Works one of two ways: 87 1. Use the icon_url Option and this default css() classmethod 88 2. Override the css() classmethod for more complex cases 89 """ 90 url = klass.get_options().icon_url 91 if url: 92 if not url.startswith("/") and not url.startswith("http://"): 93 url = settings.MEDIA_URL + url 94 return """ li.%s > .icon { 95 background: url("%s") no-repeat scroll 0 0 transparent ! important; 96 display:inline ! important; 97 } 98 99 div.%s > .goog-menuitem-content { 100 background: url("%s") no-repeat scroll 0 0 transparent !important; 101 display: block !important; 102 padding-left: 22px; 103 position: relative; 104 left: -22px; 105 height: 16px; 106 } """ % (klass.model_uid(), url, klass.model_uid(), url)
107 108 @property
109 - def options(self):
110 return get_model_options(self.__class__.__name__)
111 112 @classmethod
113 - def model_uid(klass):
114 """ 115 class method providing the uid for the model class. 116 """ 117 ct = ContentType.objects.get_for_model(klass) 118 return "%s_%s" % (ct.app_label, ct.model)
119 120 @property
121 - def hash(self):
122 """ 123 For caching. This string represents a hash of all 124 attributes that may influence reporting results. 125 i.e. if this property changes, reports for the feature get rerun. 126 """ 127 important = "%s%s" % (self.date_modified, self.uid) 128 return important.__hash__()
129 130 @property
131 - def uid(self):
132 """ 133 Unique identifier for this feature. 134 """ 135 if not self.pk: 136 raise Exception( 137 'Trying to get uid for feature class that is not yet saved!') 138 return "%s_%s" % (self.model_uid(), self.pk, )
139 140 @property
141 - def kml_safe(self):
142 """ 143 A safety valve for kmlapp... 144 If one feature's .kml property fails, 145 it won't bring down the entire request. 146 This property is never to be overridden! 147 """ 148 try: 149 return self.kml 150 except Exception as e: 151 try: 152 logger.error("%s .kml property is failing: \n%s\n" % (self.uid,e.message)) 153 except: 154 # just in case logging or the uid property are fubar 155 print ".kml is failing on something" 156 # Create a fallback KML placemark so it doesn't just disappear 157 return """ 158 <Placemark id="%s"> 159 <visibility>0</visibility> 160 <name>%s (KML Error)</name> 161 <description>Error Details ... %s ... If the problem persists, please contact us.</description> 162 </Placemark> 163 """ % (self.uid, self.name, e.message)
164
165 - def add_to_collection(self, collection):
166 """ 167 Add feature to specified FeatureCollection 168 """ 169 assert issubclass(collection.__class__, FeatureCollection) 170 assert self.__class__ in collection.get_options().get_valid_children() 171 assert self.user == collection.user 172 self.collection = collection 173 self.save(rerun=False)
174
175 - def remove_from_collection(self):
176 """ 177 Remove feature from FeatureCollection 178 """ 179 collection = self.collection 180 self.collection = None 181 self.save(rerun=False) 182 if collection: 183 collection.save(rerun=True)
184
185 - def share_with(self, groups, append=False):
186 """ 187 Share this feature with the specified group/groups. 188 Owner must be a member of the group/groups. 189 Group must have 'can_share' permissions else an Exception is raised 190 """ 191 if not append: 192 # Don't append to existing groups; Wipe the slate clean 193 # Note that this is the default behavior 194 self.sharing_groups.clear() 195 196 if groups is None or groups == []: 197 # Nothing to do here 198 return True 199 200 if isinstance(groups,Group): 201 # Only a single group was provided, make a 1-item list 202 groups = [groups] 203 204 for group in groups: 205 assert isinstance(group, Group) 206 # Check that the group to be shared with has appropos permissions 207 assert group in self.user.groups.all() 208 try: 209 gp = group.permissions.get(codename='can_share_features') 210 except: 211 raise Exception("The group you are trying to share with " 212 "does not have can_share permission") 213 214 self.sharing_groups.add(group) 215 216 self.save(rerun=False) 217 return True
218
219 - def is_viewable(self, user):
220 """ 221 Is this feauture viewable by the specified user? 222 Either needs to own it or have it shared with them. 223 returns : Viewable(boolean), HttpResponse 224 """ 225 # First, is the user logged in? 226 if user.is_anonymous() or not user.is_authenticated(): 227 try: 228 obj = self.__class__.objects.shared_with_user(user).get(pk=self.pk) 229 return True, HttpResponse("Object shared with public, viewable by anonymous user", status=202) 230 except self.__class__.DoesNotExist: 231 # Unless the object is publicly shared, we won't give away anything 232 return False, HttpResponse('You must be logged in', status=401) 233 234 # Does the user own it? 235 if self.user == user: 236 return True, HttpResponse("Object owned by user",status=202) 237 238 # Next see if its shared with the user 239 try: 240 # Instead having the sharing logic here, use the shared_with_user 241 # We need it to return querysets so no sense repeating that logic 242 obj = self.__class__.objects.shared_with_user(user).get(pk=self.pk) 243 return True, HttpResponse("Object shared with user", status=202) 244 except self.__class__.DoesNotExist: 245 return False, HttpResponse("Access denied", status=403) 246 247 return False, HttpResponse("Server Error in feature.is_viewable()", status=500)
248
249 - def copy(self, user=None):
250 """ 251 Returns a copy of this feature, setting the user to the specified 252 owner. Copies many-to-many relations 253 """ 254 # Took this code almost verbatim from the mpa model code. 255 # TODO: Test if this method is robust, and evaluate alternatives like 256 # that described in django ticket 4027 257 # http://code.djangoproject.com/ticket/4027 258 the_feature = self 259 260 # Make an inventory of all many-to-many fields in the original feature 261 m2m = {} 262 for f in the_feature._meta.many_to_many: 263 m2m[f.name] = the_feature.__getattribute__(f.name).all() 264 265 # The black magic voodoo way, 266 # makes a copy but relies on this strange implementation detail of 267 # setting the pk & id to null 268 # An alternate, more explicit way, can be seen at: 269 # http://blog.elsdoerfer.name/2008/09/09/making-a-copy-of-a-model-instance 270 the_feature.pk = None 271 the_feature.id = None 272 the_feature.save(rerun=False) 273 274 the_feature.name = the_feature.name + " (copy)" 275 276 # Restore the many-to-many fields 277 for fname in m2m.keys(): 278 for obj in m2m[fname]: 279 the_feature.__getattribute__(fname).add(obj) 280 281 # Reassign User 282 the_feature.user = user 283 284 # Clear everything else 285 the_feature.sharing_groups.clear() 286 the_feature.remove_from_collection() 287 288 the_feature.save(rerun=False) 289 return the_feature
290
291 -class SpatialFeature(Feature):
292 """ 293 Abstract Model used for representing user-generated geometry features. 294 Inherits from Feature and adds geometry-related methods/properties 295 common to all geometry types. 296 297 ====================== ============================================== 298 Attribute Description 299 ====================== ============================================== 300 ``user`` Creator 301 302 ``name`` Name of the object 303 304 ``date_created`` When it was created 305 306 ``date_modified`` When it was last updated. 307 308 ``manipulators`` List of manipulators to be applied when geom 309 is saved. 310 ====================== ============================================== 311 """ 312 manipulators = models.TextField(verbose_name="Manipulator List", null=True, 313 blank=True, help_text='csv list of manipulators to be applied') 314
315 - class Meta(Feature.Meta):
316 abstract = True
317
318 - def save(self, *args, **kwargs):
319 self.apply_manipulators() 320 if self.geometry_final: 321 self.geometry_final = clean_geometry(self.geometry_final) 322 super(SpatialFeature, self).save(*args, **kwargs) # Call the "real" save() method
323 324 @property
325 - def geom_kml(self):
326 """ 327 Basic KML representation of the feature geometry 328 """ 329 return asKml(self.geometry_final, uid=self.uid)
330 331 @classmethod
332 - def mapnik_style(self):
333 """ 334 Mapnik style object containing rules for symbolizing features in staticmap 335 """ 336 return None 337 style = mapnik.Style() 338 return style
339 340 @property
341 - def kml(self):
342 """ 343 Fully-styled KML placemark representation of the feature. 344 The Feature's kml property MUST 345 - return a string containing a valid KML placemark element 346 - the placemark must have id= [the feature's uid] 347 - if it references any style URLs, the corresponding Style element(s) 348 must be provided by the feature's .kml_style property 349 """ 350 return """ 351 <Placemark id="%s"> 352 <visibility>1</visibility> 353 <name>%s</name> 354 <styleUrl>#%s-default</styleUrl> 355 <ExtendedData> 356 <Data name="name"><value>%s</value></Data> 357 <Data name="user"><value>%s</value></Data> 358 <Data name="modified"><value>%s</value></Data> 359 </ExtendedData> 360 %s 361 </Placemark> 362 """ % (self.uid, self.name, self.model_uid(), 363 self.name, self.user, self.date_modified, 364 self.geom_kml)
365 366 @property
367 - def kml_style(self):
368 """ 369 Must return a string with one or more KML Style elements 370 whose id's may be referenced by relative URL 371 from within the feature's .kml string 372 In any given KML document, each *unique* kml_style string will get included 373 so don't worry if you have 10 million features with "blah-default" style... 374 only one will appear in the final document and all the placemarks can refer 375 to it. BEST TO TREAT THIS LIKE A CLASS METHOD - no instance specific vars. 376 """ 377 378 return """ 379 <Style id="%s-default"> 380 <IconStyle> 381 <color>ffffffff</color> 382 <colorMode>normal</colorMode> 383 <scale>0.9</scale> 384 <Icon> <href>http://maps.google.com/mapfiles/kml/paddle/wht-blank.png</href> </Icon> 385 </IconStyle> 386 <BalloonStyle> 387 <bgColor>ffeeeeee</bgColor> 388 <text> <![CDATA[ 389 <font color="#1A3752"><strong>$[name]</strong></font><br /> 390 <font size=1>Created by $[user] on $[modified]</font> 391 ]]> </text> 392 </BalloonStyle> 393 <LabelStyle> 394 <color>ffffffff</color> 395 <scale>0.8</scale> 396 </LabelStyle> 397 <PolyStyle> 398 <color>778B1A55</color> 399 </PolyStyle> 400 <LineStyle> 401 <color>ffffffff</color> 402 </LineStyle> 403 </Style> 404 """ % (self.model_uid())
405 406 @property
407 - def active_manipulators(self):
408 """ 409 This method contains all the logic to determine which manipulators get applied to a feature 410 411 If self.manipulators doesnt exist or is null or blank, 412 apply the required manipulators (or the NullManipulator if none are required) 413 414 If there is a self.manipulators string and there are optional manipulators contained in it, 415 apply the required manipulators PLUS the specified optional manipulators 416 """ 417 active = [] 418 try: 419 manipulator_list = self.manipulators.split(',') 420 if len(manipulator_list) == 1 and manipulator_list[0] == '': 421 # list is blank 422 manipulator_list = [] 423 except AttributeError: 424 manipulator_list = [] 425 426 required = self.options.manipulators 427 try: 428 optional = self.options.optional_manipulators 429 except AttributeError: 430 optional = [] 431 432 # Always include the required manipulators in the active list 433 active.extend(required) 434 435 if len(manipulator_list) < 1: 436 if not required or len(required) < 1: 437 manipulator_list = ['NullManipulator'] 438 else: 439 return active 440 441 # include all valid manipulators from the self.manipulators list 442 for manipulator in manipulator_list: 443 manipClass = manipulatorsDict.get(manipulator) 444 if manipClass and (manipClass in optional or manipClass == NullManipulator): 445 active.append(manipClass) 446 447 return active
448
449 - def apply_manipulators(self, force=False):
450 if force or (self.geometry_orig and not self.geometry_final): 451 logger.debug("applying manipulators to %r" % self) 452 target_shape = self.geometry_orig.transform(settings.GEOMETRY_CLIENT_SRID, clone=True).wkt 453 logger.debug("active manipulators: %r" % self.active_manipulators) 454 result = False 455 for manipulator in self.active_manipulators: 456 m = manipulator(target_shape) 457 result = m.manipulate() 458 target_shape = result['clipped_shape'].wkt 459 if not result: 460 raise Exception("No result returned - maybe manipulators did not run?") 461 geo = result['clipped_shape'] 462 geo.transform(settings.GEOMETRY_DB_SRID) 463 ensure_clean(geo, settings.GEOMETRY_DB_SRID) 464 if geo: 465 self.geometry_final = geo 466 else: 467 raise Exception('Could not pre-process geometry')
468
469 -class PolygonFeature(SpatialFeature):
470 """ 471 Model used for representing user-generated polygon features. Inherits from SpatialFeature. 472 473 ====================== ============================================== 474 Attribute Description 475 ====================== ============================================== 476 ``user`` Creator 477 478 ``name`` Name of the object 479 480 ``date_created`` When it was created 481 482 ``date_modified`` When it was last updated. 483 484 ``manipulators`` List of manipulators to be applied when geom 485 is saved. 486 487 ``geometry_original`` Original geometry as input by the user. 488 489 ``geometry_final`` Geometry after manipulators are applied. 490 ====================== ============================================== 491 """ 492 geometry_orig = models.PolygonField(srid=settings.GEOMETRY_DB_SRID, 493 null=True, blank=True, verbose_name="Original Polygon Geometry") 494 geometry_final = models.PolygonField(srid=settings.GEOMETRY_DB_SRID, 495 null=True, blank=True, verbose_name="Final Polygon Geometry") 496 497 @property
498 - def centroid_kml(self):
499 """ 500 KML geometry representation of the centroid of the polygon 501 """ 502 geom = self.geometry_final.point_on_surface.transform(settings.GEOMETRY_CLIENT_SRID, clone=True) 503 return geom.kml
504 505 @classmethod
506 - def mapnik_style(self):
507 polygon_style = mapnik.Style() 508 ps = mapnik.PolygonSymbolizer(mapnik.Color('#ffffff')) 509 ps.fill_opacity = 0.5 510 ls = mapnik.LineSymbolizer(mapnik.Color('#555555'),0.75) 511 ls.stroke_opacity = 0.5 512 r = mapnik.Rule() 513 r.symbols.append(ps) 514 r.symbols.append(ls) 515 polygon_style.rules.append(r) 516 return polygon_style
517
518 - class Meta(Feature.Meta):
519 abstract = True
520
521 -class LineFeature(SpatialFeature):
522 """ 523 Model used for representing user-generated linestring features. Inherits from SpatialFeature. 524 525 ====================== ============================================== 526 Attribute Description 527 ====================== ============================================== 528 ``user`` Creator 529 530 ``name`` Name of the object 531 532 ``date_created`` When it was created 533 534 ``date_modified`` When it was last updated. 535 536 ``manipulators`` List of manipulators to be applied when geom 537 is saved. 538 539 ``geometry_original`` Original geometry as input by the user. 540 541 ``geometry_final`` Geometry after manipulators are applied. 542 ====================== ============================================== 543 """ 544 geometry_orig = models.LineStringField(srid=settings.GEOMETRY_DB_SRID, 545 null=True, blank=True, verbose_name="Original LineString Geometry") 546 geometry_final = models.LineStringField(srid=settings.GEOMETRY_DB_SRID, 547 null=True, blank=True, verbose_name="Final LineString Geometry") 548 549 @classmethod
550 - def mapnik_style(self):
551 line_style = mapnik.Style() 552 ls = mapnik.LineSymbolizer(mapnik.Color('#444444'),1.5) 553 ls.stroke_opacity = 0.5 554 r = mapnik.Rule() 555 r.symbols.append(ls) 556 line_style.rules.append(r) 557 return line_style
558
559 - class Meta(Feature.Meta):
560 abstract = True
561
562 -class PointFeature(SpatialFeature):
563 """ 564 Model used for representing user-generated point features. Inherits from SpatialFeature. 565 566 ====================== ============================================== 567 Attribute Description 568 ====================== ============================================== 569 ``user`` Creator 570 571 ``name`` Name of the object 572 573 ``date_created`` When it was created 574 575 ``date_modified`` When it was last updated. 576 577 ``manipulators`` List of manipulators to be applied when geom 578 is saved. 579 580 ``geometry_original`` Original geometry as input by the user. 581 582 ``geometry_final`` Geometry after manipulators are applied. 583 ====================== ============================================== 584 """ 585 geometry_orig = models.PointField(srid=settings.GEOMETRY_DB_SRID, 586 null=True, blank=True, verbose_name="Original Point Geometry") 587 geometry_final = models.PointField(srid=settings.GEOMETRY_DB_SRID, 588 null=True, blank=True, verbose_name="Final Point Geometry") 589 590 @classmethod
591 - def mapnik_style(self):
592 point_style = mapnik.Style() 593 r = mapnik.Rule() 594 r.symbols.append(mapnik.PointSymbolizer()) 595 point_style.rules.append(r) 596 return point_style
597
598 - class Meta(Feature.Meta):
599 abstract = True
600
601 -class FeatureCollection(Feature):
602 """ 603 A Folder/Collection of Features 604 """
605 - class Meta:
606 abstract = True
607
608 - def add(self, f):
609 """Adds a specified Feature to the Collection""" 610 f.add_to_collection(self)
611
612 - def remove(self, f):
613 """Removes a specified Feature from the Collection""" 614 if f.collection == self: 615 f.remove_from_collection() 616 self.save() # This updates the date_modified field of the collection 617 else: 618 raise Exception('Feature `%s` is not in Collection `%s`' % (f.name, self.name))
619
620 - def save(self, rerun=True, *args, **kwargs):
621 super(FeatureCollection, self).save(*args, **kwargs) # Call the "real" save() method
622 623 @property
624 - def kml(self):
625 features = self.feature_set() 626 kmls = [x.kml for x in features] 627 return """ 628 <Folder id="%s"> 629 <name>%s</name> 630 <visibility>0</visibility> 631 <open>0</open> 632 %s 633 </Folder> 634 """ % (self.uid, self.name, ''.join(kmls))
635 636 @property
637 - def kml_style(self):
638 return """ 639 <Style id="%(model_uid)s-default"> 640 </Style> 641 """ % {'model_uid': self.model_uid()}
642 643 @property
644 - def kml_style_id(self):
645 return "%s-default" % self.model_uid()
646
647 - def feature_set(self, recurse=False, feature_classes=None):
648 """ 649 Returns a list of Features belonging to the Collection 650 Optionally recurse into all child containers 651 or limit/filter for a list of feature classes 652 """ 653 feature_set = [] 654 655 # If a single Feature is provided, make it into 1-item list 656 if issubclass(feature_classes.__class__, Feature): 657 feature_classes = [feature_classes] 658 659 for model_class in self.get_options().get_valid_children(): 660 if recurse and issubclass(model_class, FeatureCollection): 661 collections = model_class.objects.filter( 662 content_type=ContentType.objects.get_for_model(self), 663 object_id=self.pk 664 ) 665 for collection in collections: 666 feature_list = collection.feature_set(recurse, feature_classes) 667 if len(feature_list) > 0: 668 feature_set.extend(feature_list) 669 670 if feature_classes and model_class not in feature_classes: 671 continue 672 673 feature_list = list( 674 model_class.objects.filter( 675 content_type=ContentType.objects.get_for_model(self), 676 object_id=self.pk 677 ) 678 ) 679 680 if len(feature_list) > 0: 681 feature_set.extend(feature_list) 682 683 return feature_set
684
685 - def copy(self, user=None):
686 """ 687 Returns a copy of this feature collection, setting the user to the specified 688 owner. Recursively copies all children. 689 """ 690 original_feature_set = self.feature_set(recurse=False) 691 692 the_collection = self 693 694 # Make an inventory of all many-to-many fields in the original feature 695 m2m = {} 696 for f in the_collection._meta.many_to_many: 697 m2m[f.name] = the_collection.__getattribute__(f.name).all() 698 699 # makes a copy but relies on this strange implementation detail of 700 # setting the pk & id to null 701 the_collection.pk = None 702 the_collection.id = None 703 the_collection.save() 704 705 the_collection.name = the_collection.name + " (copy)" 706 707 # Restore the many-to-many fields 708 for fname in m2m.keys(): 709 for obj in m2m[fname]: 710 the_collection.__getattribute__(fname).add(obj) 711 712 # Reassign User 713 the_collection.user = user 714 715 # Clear everything else 716 the_collection.sharing_groups.clear() 717 the_collection.remove_from_collection() 718 the_collection.save() 719 720 for child in original_feature_set: 721 new_child = child.copy(user) 722 new_child.add_to_collection(the_collection) 723 724 the_collection.save() 725 return the_collection
726
727 - def delete(self, *args, **kwargs):
728 """ 729 Delete all features in the set 730 """ 731 for feature in self.feature_set(recurse=False): 732 feature.delete() 733 super(FeatureCollection, self).delete(*args, **kwargs)
734