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()
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
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
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
91 """
92 Handles all requests to views setup via features.register using Link
93 objects.
94
95 Assuming a valid request, this generic view will call the view specified
96 by the link including an instance or instances argument containing the
97 relavent Feature(s).
98
99 If the incoming request is invalid, any one of the following errors may be
100 returned:
101
102 401: login required
103 403: user does not have permission (not admin user or doesn't own object
104 to be edited)
105 404: feature(s) could not be found
106 400: requested for feature classes not supported by this view
107 5xx: server error
108 """
109 if link is None:
110 raise Exception('handle_link configured without link kwarg!')
111 uids = uids.split(',')
112
113 if len(uids) > 1 and link.select is 'single':
114
115 return HttpResponse(
116 'Not Supported Error: Requested %s for multiple instances' % (
117 link.title, ), status=400)
118 singles = ('single', 'multiple single', 'single multiple')
119 if len(uids) is 1 and link.select not in singles:
120
121 return HttpResponse(
122 'Not Supported Error: Requested %s for single instance' % (
123 link.title, ), status=400)
124 instances = []
125 for uid in uids:
126 if link.rel == 'edit':
127 if link.method.lower() == 'post' and request.method == 'GET':
128 resp = HttpResponse('Invalid Method', status=405)
129 resp['Allow'] = 'POST'
130 return resp
131 if link.edits_original is False:
132
133 inst = get_object_for_viewing(request, uid)
134 else:
135 inst = get_object_for_editing(request, uid)
136 else:
137 inst = get_object_for_viewing(request, uid)
138
139 if isinstance(inst, HttpResponse):
140 return inst
141 else:
142 instances.append(inst)
143 for instance in instances:
144 if link.generic and instance.__class__ not in link.models:
145 return HttpResponse(
146 'Not Supported Error: Requested for "%s" feature class. This \
147 generic link only supports requests for feature classes %s' % (
148 instance.__class__.__name__,
149 ', '.join([m.__name__ for m in link.models])), status=400)
150
151 if link.select is 'single':
152 return link.view(request, instances[0], **link.extra_kwargs)
153 else:
154 return link.view(request, instances, **link.extra_kwargs)
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
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
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
290
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
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
363
364
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
433
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
469
470 from madrona.manipulators.manipulators import get_manipulators_for_model
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
496
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):
513
514 -def kmz(request, instances):
516
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
534 features = []
535 collections = []
536
537
538
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
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
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
560
561 response['Content-Type'] = mimetypes.KMZ
562 response['Content-Disposition'] = 'attachment; filename=%s.kmz' % filename
563 response.write(kml)
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
664
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
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
786
799
801 """
802 Generic view for GeoJSON representation of feature classes.
803 Can be overridden but this is provided a default.
804
805 To override, feature class needs a geojson object that returns
806 a geojson feature string (no trailing comma)::
807
808 { "type": "Feature",
809 "geometry": {"type": "Point", "coordinates": [102.0, 0.5]},
810 "properties": {"prop0": "value0"}
811 }
812
813 GeoJSON Feature collections *cannot* be nested within other feature collections
814 http://lists.geojson.org/pipermail/geojson-geojson.org/2008-October/000464.html
815 Thus collections can be treated using one of the following strategies:
816
817 ``flat``: (DEFAULT) The collection is "flattened" to contain all the
818 features in a single featurecollection (lossy)
819
820 ``nest_feature_set``: the collection is represented as an empty geometry with a special
821 feature_set property; a list of UIDs to fetch
822 (requires a client with knowledge of this convention)
823
824 Pass by URL GET parameter like ?strategy=nest_feature_set
825 """
826 from madrona.common import default_mimetypes as mimetypes
827 from madrona.common.jsonutils import get_properties_json, get_feature_json, srid_to_urn, srid_to_proj
828 from madrona.features.models import FeatureCollection, SpatialFeature, Feature
829 from django.contrib.gis.gdal import DataSource
830 import tempfile
831 import os
832 import json
833
834 strategy = request.GET.get('strategy', default='flat')
835 strategy = strategy.lower()
836
837 if settings.GEOJSON_SRID:
838 srid_setting = settings.GEOJSON_SRID
839 else:
840 srid_setting = settings.GEOMETRY_DB_SRID
841 srid = int(request.GET.get('srid', default=srid_setting))
842 if srid <= 32766:
843 crs = srid_to_urn(srid)
844 else:
845 crs = srid_to_proj(srid)
846
847 if settings.GEOJSON_DOWNLOAD:
848 download = 'noattach' not in request.GET
849 else:
850 download = 'attach' in request.GET
851
852 feature_jsons = []
853 for i in instances:
854 gj = None
855 try:
856 gj = i.geojson(srid)
857 except AttributeError:
858 pass
859
860 if gj is None:
861 props = get_properties_json(i)
862 if issubclass(i.__class__, FeatureCollection):
863 if strategy == 'nest_feature_set':
864
865 props['feature_set'] = [x.uid for x in i.feature_set()]
866 gj = get_feature_json('null', json.dumps(props))
867 else:
868 feats = [f for f in i.feature_set(recurse=True)
869 if not isinstance(f, FeatureCollection)]
870 gjs = []
871 for f in feats:
872 try:
873 geom = x.geometry_final.transform(srid, clone=True).json
874 except:
875 geom = 'null'
876 props = get_properties_json(f)
877 gjs.append(get_feature_json(geom, json.dumps(props)))
878 gj = ', \n'.join(gjs)
879
880 else:
881 try:
882
883 geom = i.geometry_final.transform(srid, clone=True).json
884 except:
885 geom = 'null'
886 gj = get_feature_json(geom, json.dumps(props))
887
888 if gj is not None:
889 feature_jsons.append(gj)
890
891 geojson = """{
892 "type": "FeatureCollection",
893 "crs": { "type": "name", "properties": {"name": "%s"}},
894 "features": [
895 %s
896 ]
897 }""" % (crs, ', \n'.join(feature_jsons),)
898
899
900
901
902
903
904
905
906
907
908
909
910
911 response = HttpResponse()
912 response['Content-Type'] = mimetypes.JSON
913 if download:
914 filename = '_'.join([slugify(i.name) for i in instances])[:40]
915 response['Content-Disposition'] = 'attachment; filename=%s.geojson' % filename
916 response.write(geojson)
917 return response
918