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

Source Code for Module madrona.features.views

  1  from django.http import HttpResponse, HttpResponseForbidden 
  2  from django.shortcuts import get_object_or_404, render_to_response 
  3  from django.template import RequestContext, Context 
  4  from django.template import loader, TemplateDoesNotExist 
  5  from django.conf import settings 
  6  from django.contrib.contenttypes.models import ContentType 
  7  from django.contrib.auth.models import Group 
  8  from madrona.features.models import Feature 
  9  from madrona.features import user_sharing_groups 
 10  from madrona.common.utils import get_logger 
 11  from madrona.common import default_mimetypes as mimetypes 
 12  from madrona.features import workspace_json, get_feature_by_uid 
 13  from django.template.defaultfilters import slugify 
 14  from madrona.features.models import SpatialFeature, Feature, FeatureCollection 
 15  from django.core.urlresolvers import reverse 
 16  from django.views.decorators.cache import cache_page 
 17  from django.utils import simplejson 
 18  logger = get_logger() 
19 20 -def get_object_for_editing(request, uid, target_klass=None):
21 """ 22 Return the specified instance by uid for editing. 23 If a target_klass is provided, uid will be checked for consistency. 24 If the request has no logged-in user, a 401 Response will be returned. If 25 the item is not found, a 404 Response will be returned. If the user is 26 not authorized to edit the item (not the owner or a staff user), a 403 Not 27 Authorized Response will be returned. 28 29 usage: 30 31 instance = get_object_for_editing(request, 'mlpa_mpa_12', target_klass=Mpa) 32 if isinstance(instance, HttpResponse): 33 return instance 34 35 """ 36 if target_klass and not target_klass.model_uid() in uid: 37 return HttpResponse("Target class %s doesn't match the provided uid %s" % 38 (target_klass, uid), 39 status=401) 40 try: 41 instance = get_feature_by_uid(uid) 42 except ValueError: 43 return HttpResponse("Uid not valid: %s" % uid, status=401) 44 except: 45 return HttpResponse("Feature not found - %s" % uid, status=404) 46 47 if not request.user.is_authenticated(): 48 return HttpResponse('You must be logged in.', status=401) 49 # Check that user owns the object or is staff 50 if not request.user.is_staff and request.user != instance.user: 51 return HttpResponseForbidden( 52 'You do not have permission to modify this object.') 53 return instance
54
55 -def get_object_for_viewing(request, uid, target_klass=None):
56 """ 57 Return the specified instance by uid for viewing. 58 If a target_klass is provided, uid will be checked for consistency. 59 If the request has no authenticated user, a 401 Response will be returned. 60 If the item is not found, a 404 Response will be returned. If the user is 61 not authorized to view the item (not the owner or part of a group the item 62 is shared with), a 403 Not Authorized Response will be returned. 63 64 usage: 65 66 instance = get_object_for_viewing(request, 'mlpa_mpa_12', target_klass=Mpa) 67 if isinstance(instance, HttpResponse): 68 return instance 69 70 """ 71 if target_klass and not target_klass.model_uid() in uid: 72 return HttpResponse("Target class %s doesn't match the provided uid %s" % 73 (target_klass, uid), 74 status=401) 75 try: 76 instance = get_feature_by_uid(uid) 77 except ValueError: 78 return HttpResponse("Uid not valid: %s" % uid, status=401) 79 except: 80 return HttpResponse("Feature not found - %s" % uid, status=404) 81 82 viewable, response = instance.is_viewable(request.user) 83 if viewable: 84 return instance 85 else: 86 return response
87 155
156 -def delete(request, model=None, uid=None):
157 """ 158 When calling, provide the request object, reference to the resource 159 class, and the primary key of the object to delete. 160 161 Possible response codes: 162 163 200: delete operation successful 164 401: login required 165 403: user does not have permission (not admin user or doesn't own object) 166 404: resource for deletion could not be found 167 5xx: server error 168 """ 169 if model is None: 170 return HttpResponse('Model not specified in feature urls', status=500) 171 if request.method == 'DELETE': 172 if model is None or uid is None: 173 raise Exception('delete view not configured properly.') 174 instance = get_object_for_editing(request, uid, target_klass=model) 175 if isinstance(instance, HttpResponse): 176 # get_object_for_editing is trying to return a 404, 401, or 403 177 return instance 178 instance.delete() 179 return HttpResponse('{"status": 200}') 180 else: 181 return HttpResponse('DELETE http method must be used to delete', 182 status=405)
183
184 -def multi_delete(request, instances):
185 """ 186 Generic view to delete multiple instances 187 """ 188 deleted = [] 189 if request.method == 'DELETE': 190 for instance in instances: 191 uid = instance.uid 192 instance.delete() 193 deleted.append(uid) 194 return HttpResponse('{"status": 200}') 195 else: 196 return HttpResponse('DELETE http method must be used to delete', 197 status=405)
198
199 200 -def create(request, model, action):
201 """ 202 When calling, provide the request object and a ModelForm class 203 204 POST: Create a new instance from filled out ModelForm 205 206 201: Created. Response body includes representation of resource 207 400: Validation error. Response body includes form. Form should 208 be displayed back to the user for correction. 209 401: Not logged in. 210 5xx: Server error. 211 """ 212 config = model.get_options() 213 form_class = config.get_form_class() 214 if not request.user.is_authenticated(): 215 return HttpResponse('You must be logged in.', status=401) 216 title = 'New %s' % (config.slug, ) 217 if request.method == 'POST': 218 values = request.POST.copy() 219 values.__setitem__('user', request.user.pk) 220 if request.FILES: 221 form = form_class(values, request.FILES, label_suffix='') 222 else: 223 form = form_class(values, label_suffix='') 224 if form.is_valid(): 225 m = form.save(commit=False) 226 ''' 227 Note on the following 3 lines: 228 We need to call form.save_m2m after save but before run, this is accomplished in the Feature model save method 229 ''' 230 kwargs = {} 231 kwargs['form'] = form 232 m.save(**kwargs) 233 234 return to_response( 235 status=201, 236 location=m.get_absolute_url(), 237 select=m.uid, 238 show=m.uid 239 ) 240 else: 241 context = config.form_context 242 user = request.user 243 context.update({ 244 'form': form, 245 'title': title, 246 'action': action, 247 'is_ajax': request.is_ajax(), 248 'MEDIA_URL': settings.MEDIA_URL, 249 'is_spatial': issubclass(model, SpatialFeature), 250 'is_collection': issubclass(model, FeatureCollection), 251 'user': user, 252 }) 253 context = decorate_with_manipulators(context, form_class) 254 c = RequestContext(request, context) 255 t = loader.get_template(config.form_template) 256 return HttpResponse(t.render(c), status=400) 257 else: 258 return HttpResponse('Invalid http method', status=405)
259
260 -def create_form(request, model, action=None):
261 """ 262 Serves a form for creating new objects 263 264 GET only 265 """ 266 config = model.get_options() 267 form_class = config.get_form_class() 268 if action is None: 269 raise Exception('create_form view is not configured properly.') 270 if not request.user.is_authenticated(): 271 return HttpResponse('You must be logged in.', status=401) 272 title = 'New %s' % (config.verbose_name) 273 user = request.user 274 context = config.form_context 275 if request.method == 'GET': 276 context.update({ 277 'form': form_class(label_suffix=''), 278 'title': title, 279 'action': action, 280 'is_ajax': request.is_ajax(), 281 'MEDIA_URL': settings.MEDIA_URL, 282 'is_spatial': issubclass(model, SpatialFeature), 283 'is_collection': issubclass(model, FeatureCollection), 284 'user': user, 285 }) 286 context = decorate_with_manipulators(context, form_class) 287 return render_to_response(config.form_template, context) 288 else: 289 return HttpResponse('Invalid http method', status=405)
290
291 -def update_form(request, model, uid):
292 """ 293 Returns a form for editing features 294 """ 295 instance = get_object_for_editing(request, uid, target_klass=model) 296 if isinstance(instance, HttpResponse): 297 # get_object_for_editing is trying to return a 404, 401, or 403 298 return instance 299 try: 300 instance.get_absolute_url() 301 except: 302 raise Exception( 303 'Model to be edited must have get_absolute_url defined.') 304 try: 305 instance.name 306 except: 307 raise Exception('Model to be edited must have a name attribute.') 308 309 user = request.user 310 config = model.get_options() 311 if request.method == 'GET': 312 form_class = config.get_form_class() 313 form = form_class(instance=instance, label_suffix='') 314 context = config.form_context 315 context.update({ 316 'form': form, 317 'title': "Edit '%s'" % (instance.name, ), 318 'action': instance.get_absolute_url(), 319 'is_ajax': request.is_ajax(), 320 'MEDIA_URL': settings.MEDIA_URL, 321 'is_spatial': issubclass(model, SpatialFeature), 322 'is_collection': issubclass(model, FeatureCollection), 323 'user': user, 324 }) 325 context = decorate_with_manipulators(context, form_class) 326 return render_to_response(config.form_template, context) 327 else: 328 return HttpResponse('Invalid http method', status=405)
329
330 -def update(request, model, uid):
331 """ 332 When calling, provide the request object, a model class, and the 333 primary key of the instance to be updated. 334 335 POST: Update instance. 336 337 possible response codes: 338 339 200: OK. Object updated and in response body. 340 400: Form validation error. Present form back to user. 341 401: Not logged in. 342 403: Forbidden. User is not staff or does not own object. 343 404: Instance for uid not found. 344 5xx: Server error. 345 """ 346 config = model.get_options() 347 instance = get_object_for_editing(request, uid, target_klass=model) 348 if isinstance(instance, HttpResponse): 349 # get_object_for_editing is trying to return a 404, 401, or 403 350 return instance 351 try: 352 instance.get_absolute_url() 353 except: 354 raise Exception('Model must have get_absolute_url defined.') 355 try: 356 instance.name 357 except: 358 raise Exception('Model to be edited must have a name attribute.') 359 360 if request.method == 'POST': 361 values = request.POST.copy() 362 # Even if request.user is different (ie request.user is staff) 363 # user is still set to the original owner to prevent staff from 364 # 'stealing' 365 values.__setitem__('user', instance.user.pk) 366 form_class = config.get_form_class() 367 if request.FILES: 368 form = form_class( 369 values, request.FILES, instance=instance, label_suffix='') 370 else: 371 form = form_class(values, instance=instance, label_suffix='') 372 if form.is_valid(): 373 m = form.save(commit=False) 374 kwargs = {} 375 kwargs['form'] = form 376 m.save(**kwargs) 377 378 return to_response( 379 status=200, 380 select=m.uid, 381 show=m.uid, 382 parent=m.collection, 383 ) 384 else: 385 context = config.form_context 386 context.update({ 387 'form': form, 388 'title': "Edit '%s'" % (instance.name, ), 389 'action': instance.get_absolute_url(), 390 'is_ajax': request.is_ajax(), 391 'MEDIA_URL': settings.MEDIA_URL, 392 'is_spatial': issubclass(model, SpatialFeature), 393 'is_collection': issubclass(model, FeatureCollection), 394 }) 395 context = decorate_with_manipulators(context, form_class) 396 c = RequestContext(request, context) 397 t = loader.get_template(config.form_template) 398 return HttpResponse(t.render(c), status=400) 399 else: 400 return HttpResponse("""Invalid http method. 401 Yes we know, PUT is supposed to be used rather than POST, 402 but it was much easier to implement as POST :)""", status=405)
403
404 405 -def resource(request, model=None, uid=None):
406 """ 407 Provides a resource for a django model that can be utilized by the 408 madrona.features client module. 409 410 Implements actions for the following http actions: 411 412 POST: Update an object 413 DELETE: Delete it 414 GET: Provide a page representing the model. For MPAs, this is the 415 MPA attributes screen. The madrona client will display this 416 page in the sidebar whenever the object is brought into focus. 417 418 To implement GET, this view needs to be passed a view function 419 that returns an HttpResponse or a template can be specified 420 that will be passed the instance and an optional extra_context 421 422 Uses madrona.features.views.update and madrona.feature.views.delete 423 """ 424 if model is None: 425 return HttpResponse('Model not specified in feature urls', status=500) 426 config = model.get_options() 427 if request.method == 'DELETE': 428 return delete(request, model, uid) 429 elif request.method == 'GET': 430 instance = get_object_for_viewing(request, uid, target_klass=model) 431 if isinstance(instance, HttpResponse): 432 # Object is not viewable so we return httpresponse 433 # should contain the appropriate error code 434 return instance 435 436 t = config.get_show_template() 437 context = config.show_context 438 context.update({ 439 'instance': instance, 440 'MEDIA_URL': settings.MEDIA_URL, 441 'is_ajax': request.is_ajax(), 442 'template': t.name, 443 }) 444 445 return HttpResponse(t.render(RequestContext(request, context))) 446 elif request.method == 'POST': 447 return update(request, model, uid)
448
449 -def form_resources(request, model=None, uid=None):
450 if model is None: 451 return HttpResponse('Model not specified in feature urls', status=500) 452 if request.method == 'POST': 453 if uid is None: 454 return create(request, model, request.build_absolute_uri()) 455 else: 456 return HttpResponse('Invalid http method', status=405) 457 elif request.method == 'GET': 458 if uid is None: 459 # Get the create form 460 return create_form( 461 request, 462 model, 463 action=request.build_absolute_uri()) 464 else: 465 # get the update form 466 return update_form(request, model, uid) 467 else: 468 return HttpResponse('Invalid http method', status=405)
469 470 from madrona.manipulators.manipulators import get_manipulators_for_model
471 472 # TODO: Refactor this so that it is part of Feature.Options.edit_context 473 -def decorate_with_manipulators(extra_context, form_class):
474 try: 475 extra_context['json'] = simplejson.dumps(get_manipulators_for_model(form_class.Meta.model)) 476 except: 477 extra_context['json'] = False 478 return extra_context
479
480 -def copy(request, instances):
481 """ 482 Generic view that can be used to copy any feature classes. Supports 483 requests referencing multiple instances. 484 485 To copy, this view will call the copy() method with the request's user as 486 it's sole argument. The Feature base class has a generic copy method, but 487 developers can override it. A poorly implemented copy method that does not 488 return the copied instance will raise an exception here. 489 490 This view returns a space-delimited list of the Feature uid's for 491 selection in the user-interface after this operation via the 492 X-Madrona-Select response header. 493 """ 494 copies = [] 495 # setting this here because somehow the copies and instances vars get 496 # confused 497 untoggle = ' '.join([i.uid for i in instances]) 498 for instance in instances: 499 copy = instance.copy(request.user) 500 if not copy or not isinstance(copy, Feature): 501 raise Exception('copy method on feature class %s did not return \ 502 Feature instance.' % (instance.__class__.__name__, )) 503 copies.append(copy) 504 return to_response( 505 status=200, 506 select=copies, 507 untoggle=untoggle, 508 ) 509 return response
510
511 -def kml(request, instances):
512 return kml_core(request, instances, kmz=False)
513
514 -def kmz(request, instances):
515 return kml_core(request, instances, kmz=True)
516
517 -def kml_core(request, instances, kmz):
518 """ 519 Generic view for KML representation of feature classes. 520 Can be overridden in options but this provided a default. 521 """ 522 from madrona.kmlapp.views import get_styles, create_kmz 523 from django.template.loader import get_template 524 from madrona.common import default_mimetypes as mimetypes 525 from madrona.features.models import FeatureCollection 526 527 user = request.user 528 try: 529 session_key = request.COOKIES['sessionid'] 530 except: 531 session_key = 0 532 533 # Get features, collection from instances 534 features = [] 535 collections = [] 536 537 # If there is only a single instance with a kml_full property, 538 # just return the contents verbatim 539 if len(instances) == 1: 540 from madrona.common.utils import is_text 541 filename = slugify(instances[0].name) 542 try: 543 kml = instances[0].kml_full 544 response = HttpResponse() 545 if is_text(kml) and kmz: 546 # kml_full is text, but they want as KMZ 547 kmz = create_kmz(kml, 'mm/doc.kml') 548 response['Content-Type'] = mimetypes.KMZ 549 response['Content-Disposition'] = 'attachment; filename=%s.kmz' % filename 550 response.write(kmz) 551 return response 552 elif is_text(kml) and not kmz: 553 # kml_full is text, they just want kml 554 response['Content-Type'] = mimetypes.KML 555 response['Content-Disposition'] = 'attachment; filename=%s.kml' % filename 556 response.write(kml) 557 return response 558 else: 559 # If the kml_full returns binary, always return kmz 560 # even if they asked for kml 561 response['Content-Type'] = mimetypes.KMZ 562 response['Content-Disposition'] = 'attachment; filename=%s.kmz' % filename 563 response.write(kml) # actually its kmz but whatevs 564 return response 565 except AttributeError: 566 pass 567 568 for instance in instances: 569 viewable, response = instance.is_viewable(user) 570 if not viewable: 571 return viewable, response 572 573 if isinstance(instance, FeatureCollection): 574 collections.append(instance) 575 else: 576 features.append(instance) 577 578 styles = get_styles(features,collections,links=False) 579 580 t = get_template('kmlapp/myshapes.kml') 581 context = Context({ 582 'user': user, 583 'features': features, 584 'collections': collections, 585 'use_network_links': False, 586 'request_path': request.path, 587 'styles': styles, 588 'session_key': session_key, 589 'shareuser': None, 590 'sharegroup': None, 591 'feature_id': None, 592 }) 593 kml = t.render(context) 594 response = HttpResponse() 595 filename = '_'.join([slugify(i.name) for i in instances]) 596 597 if kmz: 598 kmz = create_kmz(kml, 'mm/doc.kml') 599 response['Content-Type'] = mimetypes.KMZ 600 response['Content-Disposition'] = 'attachment; filename=%s.kmz' % filename 601 response.write(kmz) 602 else: 603 response['Content-Type'] = mimetypes.KML 604 response['Content-Disposition'] = 'attachment; filename=%s.kml' % filename 605 response.write(kml) 606 response.write('\n') 607 return response
608
609 -def share_form(request,model=None, uid=None):
610 """ 611 Generic view for showing the sharing form for an object 612 613 POST: Update the sharing status of an object 614 GET: Provide an html form for selecting groups 615 to which the feature will be shared. 616 """ 617 if model is None: 618 return HttpResponse('Model not specified in feature urls', status=500) 619 if uid is None: 620 return HttpResponse('Instance UID not specified', status=500) 621 622 obj = get_object_for_editing(request, uid, target_klass=model) 623 624 if isinstance(obj, HttpResponse): 625 return obj 626 if not isinstance(obj, Feature): 627 return HttpResponse('Instance is not a Feature', status=500) 628 629 obj_type_verbose = obj._meta.verbose_name 630 631 if request.method == 'GET': 632 # Display the form 633 # Which groups is this object already shared to? 634 already_shared_groups = obj.sharing_groups.all() 635 636 # Get a list of user's groups that have sharing permissions 637 groups = user_sharing_groups(request.user) 638 639 return render_to_response('sharing/share_form.html', {'groups': groups, 640 'already_shared_groups': already_shared_groups, 'obj': obj, 641 'obj_type_verbose': obj_type_verbose, 'user':request.user, 642 'MEDIA_URL': settings.MEDIA_URL, 643 'action': request.build_absolute_uri()}) 644 645 elif request.method == 'POST': 646 group_ids = [int(x) for x in request.POST.getlist('sharing_groups')] 647 groups = Group.objects.filter(pk__in=group_ids) 648 649 try: 650 obj.share_with(groups) 651 return to_response( 652 status=200, 653 select=obj, 654 parent=obj.collection, 655 ) 656 except Exception as e: 657 return HttpResponse( 658 'Unable to share objects with those specified groups: %r.' % e, 659 status=500) 660 661 else: 662 return HttpResponse("Received unexpected " + request.method + 663 " request.", status=400)
664
665 -def manage_collection(request, action, uids, collection_model, collection_uid):
666 config = collection_model.get_options() 667 collection_instance = get_object_for_editing(request, collection_uid, 668 target_klass=collection_model) 669 if isinstance(collection_instance, HttpResponse): 670 return instance 671 672 if request.method == 'POST': 673 uids = uids.split(',') 674 instances = [] 675 for uid in uids: 676 inst = get_object_for_editing(request, uid) 677 678 if isinstance(inst, HttpResponse): 679 return inst 680 else: 681 instances.append(inst) 682 683 if action == 'remove': 684 for instance in instances: 685 instance.remove_from_collection() 686 elif action == 'add': 687 for instance in instances: 688 instance.add_to_collection(collection_instance) 689 else: 690 return HttpResponse("Invalid action %s." % action, status=500) 691 692 return to_response( 693 status=200, 694 select=instances, 695 parent=collection_instance, 696 ) 697 else: 698 return HttpResponse("Invalid http method.", status=405)
699
700 @cache_page(60 * 60) 701 -def workspace(request, username, is_owner):
702 user = request.user 703 if request.method == 'GET': 704 if user.is_anonymous() and is_owner: 705 return HttpResponse("Anonymous user can't access workspace as owner", status=403) 706 res = HttpResponse(workspace_json(user, is_owner), status=200) 707 res['Content-Type'] = mimetypes.JSON 708 return res 709 else: 710 return HttpResponse("Invalid http method.", status=405)
711
712 @cache_page(60 * 60) 713 -def feature_tree_css(request):
714 from madrona.features import registered_models 715 if request.method == 'GET': 716 styles = [] 717 for model in registered_models: 718 try: 719 css = model.css() 720 if css: 721 styles.append(css) 722 except: 723 logger.ERROR("Something is wrong with %s.css() class method" % model) 724 pass 725 726 res = HttpResponse('\n'.join(styles), status=200) 727 res['Content-Type'] = 'text/css' 728 return res 729 else: 730 return HttpResponse("Invalid http method.", status=405)
731
732 -def to_response(status=200, select=None, show=None, parent=None, 733 untoggle=None, location=None):
734 """Will return an appropriately structured response that the client can 735 interpret to carry out the following actions: 736 737 select 738 Accepts a list of features. Tells the client to select these 739 features in the user interface after an editing operation 740 741 show 742 Accepts a single feature. Client will show that feature's 743 attribute window in the sidebar 744 745 untoggle 746 Accepts a list of features. Useful for toggling the visibility of 747 original features that are being copied so there are not multiple 748 overlapping copies on the map 749 750 parent 751 Gives a hint to the client that the edited feature falls within a 752 particular FeatureCollection. Without this hint the client may not 753 in all cases be able to perform select and show behaviors. 754 755 These behaviors are intended to be specified to the client using 756 X-Madrona- style headers in the response. Unfortunately, we have to post 757 some forms via an iframe in order to upload files. This makes it 758 impossible to get the response headers on the client end. This function 759 therefor currently also returns all headers in a json structure in the 760 response body. 761 """ 762 headers = { 763 "status": status, 764 "Location": location, 765 "X-Madrona-Select": to_csv(select), 766 "X-Madrona-Parent-Hint": to_csv(parent), 767 "X-Madrona-Show": to_csv(show), 768 "X-Madrona-UnToggle": to_csv(untoggle), 769 } 770 headers = dict((k,v) for k,v in headers.items() if v != '' and v != None) 771 response = HttpResponse(simplejson.dumps(headers), status=status) 772 for k,v in headers.items(): 773 if k != 'status' and k != 'Location': 774 response[k] = v 775 return response
776
777 -def to_csv(features):
778 if not features or isinstance(features, unicode): 779 return features 780 elif isinstance(features, Feature): 781 return features.uid 782 elif len(features) != 0: 783 return ' '.join([f.uid for f in features]) 784 else: 785 return features
786
787 -def has_features(user):
788 """ 789 Util function to determine if a user owns any features 790 """ 791 from madrona.features import registered_models 792 for model in registered_models: 793 try: 794 if len(model.objects.filter(user=user)) > 0: 795 return True 796 except: 797 pass 798 return False
799 918