Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Export Project to HydroShare #2570

Merged
merged 8 commits into from
Dec 28, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion deployment/ansible/group_vars/development
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ celery_processes_per_worker: 1
itsi_base_url: "https://learn.staging.concord.org/"
itsi_secret_key: "{{ lookup('env', 'MMW_ITSI_SECRET_KEY') }}"

hydroshare_base_url: "https://dev.hydroshare.org/"
hydroshare_base_url: "https://beta.hydroshare.org/"
hydroshare_secret_key: "{{ lookup('env', 'MMW_HYDROSHARE_SECRET_KEY') }}"

tilecache_bucket_name: "{{ lookup('env', 'MMW_TILECACHE_BUCKET') | default('', true) }}"
Expand Down
2 changes: 1 addition & 1 deletion deployment/ansible/group_vars/test
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ celery_processes_per_worker: 1
itsi_base_url: "https://learn.staging.concord.org/"
itsi_secret_key: "{{ lookup('env', 'MMW_ITSI_SECRET_KEY') }}"

hydroshare_base_url: "https://dev.hydroshare.org/"
hydroshare_base_url: "https://beta.hydroshare.org/"
hydroshare_secret_key: "{{ lookup('env', 'MMW_HYDROSHARE_SECRET_KEY') }}"

tilecache_bucket_name: "tile-cache.staging.app.wikiwatershed.org"
Expand Down
Empty file added src/mmw/apps/export/__init__.py
Empty file.
13 changes: 13 additions & 0 deletions src/mmw/apps/export/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
from __future__ import print_function
from __future__ import unicode_literals
from __future__ import division

from django.conf.urls import patterns, url

from apps.export.views import hydroshare

urlpatterns = patterns(
'',
url(r'^hydroshare/?$', hydroshare, name='hydroshare'),
)
142 changes: 142 additions & 0 deletions src/mmw/apps/export/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# -*- coding: utf-8 -*-
from __future__ import print_function
from __future__ import unicode_literals
from __future__ import division

import fiona
import json
import os
import StringIO

from django.conf import settings
from django.contrib.gis.geos import GEOSGeometry
from django.core.exceptions import ObjectDoesNotExist
from django.shortcuts import get_object_or_404
from django.utils.timezone import now

from rest_framework import decorators, status
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated

from apps.modeling.models import Project, HydroShareResource
from apps.modeling.serializers import HydroShareResourceSerializer
from apps.user.hydroshare import HydroShareService

hss = HydroShareService()
HYDROSHARE_BASE_URL = settings.HYDROSHARE['base_url']
SHAPEFILE_EXTENSIONS = ['cpg', 'dbf', 'prj', 'shp', 'shx']


@decorators.api_view(['GET', 'POST', 'DELETE'])
@decorators.permission_classes((IsAuthenticated, ))
def hydroshare(request):
# Get HydroShare client with user's credentials
try:
hs = hss.get_client(request.user)
except ObjectDoesNotExist:
return Response(
data={'errors': ['User not connected to HydroShare']},
status=status.HTTP_401_UNAUTHORIZED
)

project_id = request.GET.get('project')
if not project_id:
# No support for exporting without a project right now
return Response(
data={'errors': ['Cannot export to HydroShare without project']},
status=status.HTTP_400_BAD_REQUEST
)

params = request.data
project = get_object_or_404(Project, id=project_id,
user__id=request.user.id)

try:
hsresource = HydroShareResource.objects.get(project=project)
except HydroShareResource.DoesNotExist:
hsresource = None

# TODO POSTing to an existing export should update the resource
# Currently it is just paired with GET

# GET existing resource simply returns it
if hsresource and request.method in ['GET', 'POST']:
serializer = HydroShareResourceSerializer(hsresource)
return Response(serializer.data)

# DELETE existing resource removes it from MMW and HydroShare
if hsresource and request.method == 'DELETE':
hs.deleteResource(hsresource.resource)
hsresource.delete()
return Response(status=status.HTTP_204_NO_CONTENT)

# Cannot GET or DELETE non-existing resource
if not hsresource and request.method in ['GET', 'DELETE']:
return Response(status=status.HTTP_404_NOT_FOUND)

# Convert keywords from comma seperated string to tuple of values
keywords = params.get('keywords')
keywords = \
tuple(map(unicode.strip, keywords.split(','))) if keywords else tuple()

# POST new resource creates it in HydroShare
resource = hs.createResource(
'CompositeResource',
params.get('title', project.name),
abstract=params.get('abstract', ''),
keywords=('mmw', 'model-my-watershed') + keywords
)

# TODO Re-enable once hydroshare/hydroshare#2537 is fixed,
# and export all GeoJSON and Shapefiles in that folder

# aoi_folder = 'area-of-interest'
# hs.createResourceFolder(resource, pathname=aoi_folder)

# AoI GeoJSON
aoi_geojson = GEOSGeometry(project.area_of_interest).geojson
aoi_file = StringIO.StringIO()
aoi_file.write(aoi_geojson)
hs.addResourceFile(resource, aoi_file, 'area-of-interest.geojson')

# AoI Shapefile
aoi_json = json.loads(aoi_geojson)
crs = {'no_defs': True, 'proj': 'longlat',
'ellps': 'WGS84', 'datum': 'WGS84'}
schema = {'geometry': aoi_json['type'], 'properties': {}}
with fiona.open('/tmp/{}.shp'.format(resource), 'w',
driver='ESRI Shapefile',
crs=crs, schema=schema) as shapefile:
shapefile.write({'geometry': aoi_json, 'properties': {}})

for ext in SHAPEFILE_EXTENSIONS:
filename = '/tmp/{}.{}'.format(resource, ext)
with open(filename) as shapefile:
hs.addResourceFile(resource, shapefile,
'area-of-interest.{}'.format(ext))
os.remove(filename)

# Other files sent from the client
# Must be in {name: "string", contents: "string"} format
files = params.get('files', [])
for f in files:
fcontents = f.get('contents')
fname = f.get('name')
if fcontents and fname:
fio = StringIO.StringIO()
fio.write(fcontents)
hs.addResourceFile(resource, fio, fname)

# Link HydroShareResrouce to Project and save
hsresource = HydroShareResource.objects.create(
project=project,
resource=resource,
title=params.get('title', project.name),
autosync=params.get('autosync', True),
exported_at=now()
)
hsresource.save()

# Return newly created HydroShareResource
serializer = HydroShareResourceSerializer(hsresource)
return Response(serializer.data, status=status.HTTP_201_CREATED)
27 changes: 27 additions & 0 deletions src/mmw/apps/modeling/migrations/0025_hydroshareresource.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('modeling', '0024_fix_gwlfe_gis_data'),
]

operations = [
migrations.CreateModel(
name='HydroShareResource',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('resource', models.CharField(help_text='ID of Resource in HydroShare', max_length=63)),
('title', models.CharField(help_text='Title of Resource in HydroShare', max_length=255)),
('autosync', models.BooleanField(default=False, help_text='Whether to automatically push changes to HydroShare')),
('exported_at', models.DateTimeField(help_text='Most recent export date')),
('created_at', models.DateTimeField(auto_now_add=True)),
('modified_at', models.DateTimeField(auto_now=True)),
('project', models.OneToOneField(related_name='hydroshare', to='modeling.Project')),
],
),
]
31 changes: 31 additions & 0 deletions src/mmw/apps/modeling/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@

from django.contrib.gis.db import models
from django.contrib.auth.models import User
from django.conf import settings

HYDROSHARE_BASE_URL = settings.HYDROSHARE['base_url']


class Project(models.Model):
Expand Down Expand Up @@ -100,3 +103,31 @@ class Meta:

def __unicode__(self):
return self.name


class HydroShareResource(models.Model):
project = models.OneToOneField(Project, related_name='hydroshare')
resource = models.CharField(
max_length=63,
help_text='ID of Resource in HydroShare')
title = models.CharField(
max_length=255,
help_text='Title of Resource in HydroShare')
autosync = models.BooleanField(
default=False,
help_text='Whether to automatically push changes to HydroShare')
exported_at = models.DateTimeField(
help_text='Most recent export date')
created_at = models.DateTimeField(
auto_now=False,
auto_now_add=True)
modified_at = models.DateTimeField(
auto_now=True)

def _url(self):
return '{}resource/{}'.format(HYDROSHARE_BASE_URL, self.resource)

url = property(_url)

def __unicode__(self):
return '{} <{}>'.format(self.title, self.url)
11 changes: 10 additions & 1 deletion src/mmw/apps/modeling/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from rest_framework.exceptions import ValidationError
from rest_framework_gis import serializers as gis_serializers

from apps.modeling.models import Project, Scenario
from apps.modeling.models import Project, Scenario, HydroShareResource
from apps.modeling.validation import validate_aoi
from apps.modeling.calcs import get_layer_shape
from apps.user.serializers import UserSerializer
Expand Down Expand Up @@ -62,6 +62,14 @@ def to_internal_value(self, data):
return geometry.geojson


class HydroShareResourceSerializer(serializers.ModelSerializer):

class Meta:
model = HydroShareResource

url = serializers.ReadOnlyField()


class ScenarioSerializer(serializers.ModelSerializer):

class Meta:
Expand All @@ -84,6 +92,7 @@ class Meta:
user = UserSerializer(default=serializers.CurrentUserDefault())
gis_data = JsonField(required=False, allow_null=True)
scenarios = ScenarioSerializer(many=True, read_only=True)
hydroshare = HydroShareResourceSerializer(read_only=True)


class ProjectListingSerializer(gis_serializers.GeoModelSerializer):
Expand Down
3 changes: 2 additions & 1 deletion src/mmw/bundle.sh
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,8 @@ JS_DEPS=(backbone
turf-erase
turf-intersect
turf-kinks
underscore)
underscore
wellknown)

BROWSERIFY_EXT=""
BROWSERIFY_REQ=""
Expand Down
31 changes: 27 additions & 4 deletions src/mmw/js/src/analyze/models.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
"use strict";

var $ = require('jquery'),
lodash = require('lodash'),
_ = require('lodash'),
Backbone = require('../../shim/backbone'),
turfArea = require('turf-area'),
toWKT = require('wellknown').stringify,
settings = require('../core/settings'),
utils = require('../core/utils'),
coreModels = require('../core/models'),
turfArea = require('turf-area');
coreModels = require('../core/models');

var LayerModel = Backbone.Model.extend({});

Expand All @@ -23,7 +24,7 @@ var LayerCategoryCollection = Backbone.Collection.extend({
});

var AnalyzeTaskModel = coreModels.TaskModel.extend({
defaults: lodash.extend( {
defaults: _.extend( {
name: 'analysis',
displayName: 'Analysis',
area_of_interest: null,
Expand Down Expand Up @@ -67,6 +68,28 @@ var AnalyzeTaskModel = coreModels.TaskModel.extend({
}

return self.fetchAnalysisPromise || $.when();
},

getResultCSV: function() {
var toCSVString = function(x) {
if (_.isNull(x)) { return '""'; }
if (_.isObject(x) && _.has(x, 'type') && _.has(x, 'coordinates')) {
return '"' + toWKT(x) + '"';
}
return '"' + String(x) + '"';
};

if (this.get('status') === 'complete') {
var result = this.get('result'),
header = _.keys(result.survey.categories[0]).join(','),
values = _.map(result.survey.categories, function(c) {
return _(c).values().map(toCSVString).value().join(',');
}).join('\n');

return header + '\n' + values;
}

return "";
}
});

Expand Down
31 changes: 31 additions & 0 deletions src/mmw/js/src/core/modals/templates/hydroShareExportModal.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<div class="hydroshare-modal modal-dialog modal-dialog-sm">
<div class="modal-content">
<div class="modal-header">
<h2 class="light">Export to HydroShare</h2>
</div>
<div class="modal-body">
<p>
To publish this project on HydroShare, please confirm the title
and write an abstract.
</p>
<div>
<label class="modal-text-input-label" for="hydroshare-title">Title</label>
<input class="modal-text-input" name="hydroshare-title" type="text" id="hydroshare-title" required value="{{ name }}">
</div>
<div>
<label class="modal-text-input-label" for="hydroshare-abstract">Abstract</label>
<textarea class="modal-text-input" name="hydroshare-abstract" id="hydroshare-abstract" required rows="4"></textarea>
</div>
<div>
<label class="modal-text-input-label" for="hydroshare-keywords">Keywords <small>(optional)</small></label>
<input class="modal-text-input" name="hydroshare-keywords" type="text" id="hydroshare-keywords" placeholder="comma, separated, values">
</div>
</div>
<div class="modal-footer" style="text-align:right;">
<div class="footer-content">
<button type="button" class="btn btn-md btn-default" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-md btn-active">Export</button>
</div>
</div>
</div>
</div>
Loading