Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

new uploader using geoserver importer extension

  • Loading branch information...
commit f77cb43425f92866c12490b9a4a1c5284bc16aea 1 parent 3d318be
@ischneider ischneider authored
Showing with 4,206 additions and 25 deletions.
  1. 0  geonode/geoserver/__init__.py
  2. +1 −1  geonode/{gs_helpers.py → geoserver/helpers.py}
  3. 0  geonode/geoserver/uploader/__init__.py
  4. +319 −0 geonode/geoserver/uploader/api.py
  5. +226 −0 geonode/geoserver/uploader/uploader.py
  6. +21 −0 geonode/geoserver/uploader/utils.py
  7. +2 −2 geonode/layers/forms.py
  8. +1 −1  geonode/layers/models.py
  9. +27 −9 geonode/layers/utils.py
  10. +22 −0 geonode/local_settings.py.sample
  11. +1 −0  geonode/settings.py
  12. +421 −0 geonode/static/geonode/js/upload/ext/layer_upload.js
  13. +20 −6 geonode/static/geonode/js/upload/ext/upload_common.js
  14. +3 −4 geonode/tests/integration.py
  15. 0  geonode/upload/__init__.py
  16. +36 −0 geonode/upload/admin.py
  17. +172 −0 geonode/upload/files.py
  18. +130 −0 geonode/upload/forms.py
  19. +138 −0 geonode/upload/models.py
  20. +21 −0 geonode/upload/signals.py
  21. +1 −0  geonode/upload/templates/upload/base.html
  22. +93 −0 geonode/upload/templates/upload/layer_upload.html
  23. +46 −0 geonode/upload/templates/upload/layer_upload_crs.html
  24. +69 −0 geonode/upload/templates/upload/layer_upload_csv.html
  25. +218 −0 geonode/upload/templates/upload/layer_upload_time.html
  26. +18 −0 geonode/upload/templates/upload/no_upload.html
  27. +7 −0 geonode/upload/templates/upload/upload_error.html
  28. +18 −0 geonode/upload/templates/upload/uploadfile_confirm_delete.html
  29. +73 −0 geonode/upload/templates/upload/uploadfile_form.html
  30. 0  geonode/upload/templatetags/__init__.py
  31. +68 −0 geonode/upload/templatetags/upload_tags.py
  32. +57 −0 geonode/upload/tests.py
  33. +34 −0 geonode/upload/tests/README.rst
  34. 0  geonode/upload/tests/__init__.py
  35. +578 −0 geonode/upload/tests/integration.py
  36. +5 −0 geonode/upload/tests/test_settings.py
  37. +594 −0 geonode/upload/upload.py
  38. +28 −0 geonode/upload/urls.py
  39. +147 −0 geonode/upload/utils.py
  40. +570 −0 geonode/upload/views.py
  41. +3 −0  geonode/urls.py
  42. +7 −2 geonode/utils.py
  43. +9 −0 scripts/misc/create_dbs.sh
  44. +2 −0  setup.py
View
0  geonode/geoserver/__init__.py
No changes.
View
2  geonode/gs_helpers.py → geonode/geoserver/helpers.py
@@ -30,7 +30,7 @@
from geoserver.catalog import Catalog, FailedRequestError
-logger = logging.getLogger("geonode.maps.gs_helpers")
+logger = logging.getLogger(__name__)
_punc = re.compile(r"[\.:]") #regex for punctuation that confuses restconfig
_foregrounds = ["#ffbbbb", "#bbffbb", "#bbbbff", "#ffffbb", "#bbffff", "#ffbbff"]
View
0  geonode/geoserver/uploader/__init__.py
No changes.
View
319 geonode/geoserver/uploader/api.py
@@ -0,0 +1,319 @@
+import os
+import logging
+import json
+import pprint
+
+STATE_PENDING = "PENDING"
+STATE_READY = "READY"
+STATE_RUNNING = "RUNNING"
+STATE_INCOMPLETE = "INCOMPLETE"
+STATE_COMPLETE = "COMPLETE"
+
+_logger = logging.getLogger(__name__)
+
+def parse_response(args):
+ headers, response = args
+ try:
+ resp = json.loads(response)
+ except ValueError,ex:
+ _logger.warn('invalid JSON response: %s',response)
+ raise ex
+ if "import" in resp:
+ return Session(json=resp['import'])
+ elif "task" in resp:
+ return Task(resp['task'])
+ elif "imports" in resp:
+ return [ Session(json=j) for j in resp['imports'] ]
+ raise Exception("Unknown response %s" % resp)
+
+class _UploadBase(object):
+ _uploader = None
+ def __init__(self,json,parent=None):
+ self._parent = parent
+ if parent == self:
+ raise Exception('bogus')
+ self._bind_json(json)
+ def _bind(self,json):
+ for k in json:
+ v = json[k]
+ if not isinstance(v,dict):
+ setattr(self,k,v)
+ def _build(self,json,clazz):
+ return [ clazz(j,self) for j in json ]
+ def _getuploader(self):
+ comp = self
+ while comp:
+ if comp._uploader:
+ return comp._uploader
+ comp = comp._parent
+ def _url(self,spec,*parts):
+ return self._getuploader().client.url( spec % parts )
+ def _client(self):
+ return self._getuploader().client
+ def __repr__(self):
+ # @todo fix this
+ def _fields(obj):
+ fields = filter( lambda kp: kp[0][0] != '_',vars(obj).items())
+ fields.sort(key=lambda f: f[0])
+ return map(lambda f: isinstance(f[1],_UploadBase) and (f[0],_fields(f[1])) or f, fields)
+ repr = pprint.pformat(_fields(self),indent=2)
+ return "%s : %s" % (self.__class__.__name__,repr)
+
+class Task(_UploadBase):
+ def _bind_json(self,json):
+ self._bind(json)
+ self.source = Source(json['source'],self)
+ self.target = None
+ if 'target' in json:
+ self.target = Target(json['target'],self)
+ self.items = self._build(json['items'],Item)
+ def set_target(self,store_name,workspace):
+ data = { 'task' : {
+ 'target' : {
+ 'dataStore' : {
+ 'name' : store_name,
+ 'workspace' : {
+ 'name' : workspace
+ }
+ }
+ }
+ }}
+ self._client().put_json(self.href,json.dumps(data))
+ def set_update_mode(self,update_mode):
+ data = { 'task' : {
+ 'updateMode' : update_mode
+ }}
+ self._client().put_json(self.href,json.dumps(data))
+ def set_charset(self,charset):
+ data = { 'task' : {
+ 'source' : {
+ 'charset' : charset
+ }
+ }}
+ self._client().put_json(self.href,json.dumps(data))
+ def _add_url_part(self,parts):
+ parts.append('tasks/%s' % self.id)
+
+class Workspace(_UploadBase):
+ def _bind_json(self,json):
+ self._bind(json)
+
+class Source(_UploadBase):
+ def _bind_json(self,json):
+ self._bind(json)
+ # @todo more
+
+class Target(_UploadBase):
+
+ # this allows compatibility with the gsconfig datastore object
+ resource_type = "featureType"
+
+ def _bind_json(self,json):
+ key,val = json.items()[0]
+ self.target_type = key
+ self._bind(val)
+ self.workspace = Workspace(val['workspace'])
+ # @todo more
+
+class Item(_UploadBase):
+ def _bind_json(self,json):
+ self._bind(json)
+ # @todo iws - why is layer nested in another layer
+ self.layer = Layer(json['layer']['layer'],self)
+ resource = json['resource']
+ if 'featureType' in resource:
+ self.resource = FeatureType(resource['featureType'],self)
+ elif 'coverage' in resource:
+ self.resource = Coverage(resource['coverage'], self)
+ else:
+ raise Exception('not handling resource %s' % resource)
+ self.transformChain = json.get('transformChain',[])
+ def set_transforms(self,transforms):
+ """Set the transforms of this Item. transforms is a list of dicts"""
+ self._transforms = transforms
+ def add_transforms(self,transforms):
+ if not hasattr(self, '_transforms') and 'transforms' in self.transformChain:
+ self._transforms = list(self.transformChain['transforms'])
+ self._transforms.extend(transforms)
+ def get_progress(self):
+ """Get a json object representing progress of this item"""
+ if self.progress:
+ client = self._client()
+ headers, response = client._request(self.progress)
+ try:
+ return json.loads(response)
+ except ValueError,ex:
+ _logger.warn('invalid JSON response: %s',response)
+ raise ex
+ else:
+ raise Exception("Item does not have a progress endpoint")
+ def save(self):
+ """@todo,@hack This really only saves transforms and will overwrite existing"""
+ data = {
+ "item" : {
+ "transformChain" : {
+ "type" : "VectorTransformChain", #@todo sniff for existing
+ "transforms" : self._transforms
+ }
+ }
+ }
+ self._client().put_json(self.href,json.dumps(data))
+
+class Layer(_UploadBase):
+ def _bind_json(self,json):
+ self.layer_type = json.pop('type')
+ self._bind(json)
+
+class FeatureType(_UploadBase):
+ resource_type = "featureType"
+
+ def _bind_json(self,json):
+ self._bind(json)
+ attributes = json['attributes']['attribute'] # why extra
+ self.attributes = self._build(attributes,Attribute)
+ self.nativeCRS = None
+ if 'nativeCRS' in json:
+ self.nativeCRS = json['nativeCRS']
+ # if nativeCRS is missing, this will be a dict, otherwise a string
+ if isinstance(self.nativeCRS, dict):
+ self.nativeCRS = self.nativeCRS['$']
+
+ def set_srs(self,srs):
+ """@todo,@hack This immediately changes srs"""
+ item = self._parent
+ data = {
+ "item" : {
+ "id" : item.id,
+ "resource" : {
+ "featureType" : {
+ "srs" : srs
+ }
+ }
+ }
+ }
+ self._client().put_json(item.href,json.dumps(data))
+ self.srs = srs
+
+ def add_meta_data_entry(self,key,mtype,**kw):
+ if not hasattr(self,'metadata'):
+ self.metadata = []
+ self.metadata.append((key,mtype,kw))
+
+ def add_time_dimension_info(self,att_name,end_att_name,presentation,amt,period):
+
+ kw = {
+ 'enabled' : True,
+ 'attribute' : att_name,
+ 'presentation' : presentation
+ }
+ if end_att_name:
+ kw['endAttribute'] = end_att_name
+ if amt and period:
+ mult = {
+ 'seconds': 1,
+ 'minutes': 60,
+ 'hours': 3600,
+ 'days': 86400,
+ 'months': 2628000000, # this is the number geoserver computes for 1 month
+ 'years': 31536000000
+ }
+ kw['resolution'] = int(amt) * mult[period] * 1000 #yay millis
+ self.add_meta_data_entry('time','dimensionInfo',**kw)
+
+ def save(self):
+ """@todo,@hack This really only saves meta_data additions and will overwrite existing"""
+ item = self._parent
+ entry = []
+ for m in self.metadata:
+ entry.append({
+ "@key" : m[0],
+ m[1] : m[2]
+ })
+ data = {
+ "item" : {
+ "id" : item.id,
+ "resource" : {
+ "featureType" : {
+ "metadata" : {
+ "entry": entry
+ }
+ }
+ }
+ }
+
+ }
+ self._client().put_json(item.href, json.dumps(data))
+
+
+class Coverage(_UploadBase):
+ resource_type = "coverage"
+
+ def _bind_json(self, json):
+ # TODO
+ self._bind(json)
+
+
+class Attribute(_UploadBase):
+ def _bind_json(self, json):
+ self._bind(json)
+
+
+class Session(_UploadBase):
+ def __init__(self, json=None):
+ self.tasks = []
+ if json:
+ self._bind(json)
+ if 'tasks' in json:
+ self.tasks = self._build(json['tasks'], Task)
+
+ def reload(self):
+ '''return a reloaded version of this session'''
+ return self._uploader.get_session(self.id)
+
+ def upload_task(self, files, use_url=False):
+ """create a task with the provided files
+ files - collection of files to upload or zip file
+ use_url - if true, post a URL to the uploader
+ """
+ # @todo getting the task response updates the session tasks, but
+ # neglects to retreive the overall session status field
+ fname = os.path.basename(files[0])
+ _,ext = os.path.splitext(fname)
+ if use_url:
+ if ext == '.zip':
+ upload_url = files[0]
+ else:
+ upload_url = os.path.dirname(files[0])
+ url = self._url("imports/%s/tasks" % self.id)
+ upload_url = "file://%s" % os.path.abspath(upload_url)
+ resp = self._client().post_upload_url(url, upload_url)
+ elif ext == '.zip':
+ url = self._url("imports/%s/tasks/%s" % (self.id,fname))
+ resp = self._client().put_zip(url, files[0])
+ else:
+ url = self._url("imports/%s/tasks" % self.id)
+ resp = self._client().post_multipart(url, files)
+ task = parse_response( resp )
+ task._parent = self
+ if not isinstance(task,Task):
+ raise Exception("expected Task, got %s" % task)
+ self.tasks.append(task)
+
+ def commit(self, async=False):
+ """complete upload"""
+ #@todo check status if we don't have it already
+ url = self._url("imports/%s",self.id)
+ if async:
+ url = url + "?async"
+ resp, content = self._client().post(url)
+ if resp['status'] != '204':
+ raise Exception("expected 204 response code, got %s" % resp['status'],content)
+
+ def delete(self):
+ """Delete the upload"""
+ url = self._url("imports/%s",self.id)
+ resp, content = self._client().delete(url)
+ if resp['status'] != '204':
+ raise Exception('expected 204 response code, got %s' % resp['status'],content)
+
+
View
226 geonode/geoserver/uploader/uploader.py
@@ -0,0 +1,226 @@
+import httplib2
+import logging
+import os
+import pprint
+import json
+import mimetypes
+
+from urlparse import urlparse
+from urllib import urlencode
+
+from geonode.geoserver.uploader.api import parse_response
+from geonode.geoserver.uploader.utils import shp_files
+
+_logger = logging.getLogger(__name__)
+
+class Uploader(object):
+
+ def __init__(self, url, username="admin", password="geoserver"):
+ self.client = _Client(url,username,password)
+
+ def _call(self,fun,*args):
+ robj = fun(*args)
+ if isinstance(robj, list):
+ for i in robj:
+ i._uploader = self
+ else:
+ robj._uploader = self
+ return robj
+
+ def get_sessions(self):
+ return self._call(self.client.get_imports)
+
+ def get_session(self,id):
+ """Get an existing session by id.
+ """
+ return self._call(self.client.get_import,id)
+
+ def start_import(self, import_id=None):
+ """Create a new import session.
+ import_id - optional id to specify
+ returns an uploader.api.Session object
+ """
+ session = self._call(self.client.start_import, import_id)
+ if import_id: assert session.id >= import_id
+ return session
+
+ def upload(self, fpath, use_url=False, import_id=None):
+ """Try a complete import - create a session and upload the provided file.
+ fpath can be a path to a zip file or the 'main' file if a shapefile or a tiff
+ returns a uploader.api.Session object
+
+ use_url - if True, will post a URL to geoserver, not the file itself
+ for now, this only works with actual files, not remote urls
+ import_id - if provided, PUT to the endpoint to create the specified id
+ """
+ files = [ fpath ]
+ if fpath.lower().endswith(".shp"):
+ files = shp_files(fpath)
+
+ session = self.start_import(import_id)
+ session.upload_task(files, use_url)
+ return session
+
+ # pickle protocol - client object cannot be serialized
+ # this allows api objects to be seamlessly pickled and loaded without restarting
+ # the connection more explicitly but this will have consequences if other state is stored
+ # in the uploader or client objects
+ def __getstate__(self):
+ cl = self.client
+ return {'url':cl.service_url,'username':cl.username,'password':cl.password}
+ def __setstate__(self,state):
+ self.client = _Client(state['url'],state['username'],state['password'])
+
+class BadRequest(Exception):
+ pass
+
+class RequestFailed(Exception):
+ pass
+
+class NotFound(Exception):
+ pass
+
+class _Client(object):
+ """Lower level http client"""
+
+ # @todo some sanity on return values, either parsed object or resp,content tuple
+
+ def __init__(self, url, username, password):
+ self.service_url = url
+ if self.service_url.endswith("/"):
+ self.service_url = self.service_url.strip("/")
+ self.http = httplib2.Http()
+ self.username = username
+ self.password = password
+ self.http.add_credentials(self.username, self.password)
+ netloc = urlparse(url).netloc
+ self.http.authorizations.append(
+ httplib2.BasicAuthentication(
+ (username, password),
+ netloc,
+ url,
+ {},
+ None,
+ None,
+ self.http
+ ))
+
+ def url(self,path):
+ return "%s/%s" % (self.service_url,path)
+
+ def post(self, url):
+ return self._request(url, "POST")
+
+ def delete(self, url):
+ return self._request(url, "DELETE")
+
+ def put_json(self, url, data):
+ return self._request(url, "PUT", data, {
+ "Content-type" : "application/json"
+ })
+
+ def _parse_errors(self, content):
+ try:
+ resp = json.loads(content)
+ except ValueError:
+ return content
+ return resp['errors']
+
+ def _request(self, url, method="GET", data=None, headers={}):
+ _logger.info("%s request to %s:\n%s",method,url,data)
+ resp, content = self.http.request(url,method,data,headers)
+ _debug(resp, content)
+ if resp.status == 404:
+ raise NotFound()
+ if resp.status < 200 or resp.status > 299:
+ if resp.status == 400:
+ raise BadRequest(*self._parse_errors(content))
+ raise RequestFailed(resp.status,content)
+ return resp, content
+
+ def post_upload_url(self, url, upload_url):
+ data = urlencode({
+ 'url' : upload_url
+ })
+ return self._request(url, "POST", data, {
+ # importer very picky
+ 'Content-type' : "application/x-www-form-urlencoded"
+ })
+
+ def put_zip(self,url,payload):
+ message = open(payload)
+ with message:
+ return self._request(url,"PUT",message,{
+ "Content-type": "application/zip",
+ })
+
+ def get_import(self,i):
+ return parse_response(self._request(self.url("imports/%s" % i)))
+
+ def get_imports(self):
+ return parse_response(self._request(self.url("imports")))
+
+ def start_import(self, import_id=None):
+ method = 'POST'
+ if import_id is not None:
+ url = self.url("imports/%s" % import_id)
+ method = 'PUT'
+ else:
+ url = self.url("imports")
+ return parse_response(self._request(url, method))
+
+ def post_multipart(self,url,files,fields=[]):
+ """
+ fields is a sequence of (name, value) elements for regular form fields.
+ files is a sequence of name or (name,filename) or (name, filename, value)
+ elements for data to be uploaded as files
+
+ """
+ BOUNDARY = '----------ThIs_Is_tHe_bouNdaRY_$'
+ CRLF = '\r\n'
+ L = []
+ _logger.info("post_multipart %s %s %s",url,files,fields)
+ for (key, value) in fields:
+ L.append('--' + BOUNDARY)
+ L.append('Content-Disposition: form-data; name="%s"' % str(key))
+ L.append('')
+ L.append(str(value))
+ for fpair in files:
+ if isinstance(fpair,basestring):
+ fpair = (fpair,fpair)
+ key = fpair[0]
+ if len(fpair) == 2:
+ filename = os.path.basename(fpair[1])
+ fp = open(fpair[1])
+ value = fp.read()
+ fp.close()
+ else:
+ filename, value = fpair[1:]
+ L.append('--' + BOUNDARY)
+ L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (str(key), str(filename)))
+ L.append('Content-Type: %s' % _get_content_type(filename))
+ L.append('')
+ L.append(value)
+ L.append('--' + BOUNDARY + '--')
+ L.append('')
+ return self._request(
+ url, 'POST', CRLF.join(L), {
+ 'Content-Type' : 'multipart/form-data; boundary=%s' % BOUNDARY
+ }
+ )
+
+
+def _get_content_type(filename):
+ return mimetypes.guess_type(filename)[0] or 'application/octet-stream'
+
+def _debug(resp, content):
+ if _logger.isEnabledFor(logging.DEBUG):
+ _logger.debug("response : %s",pprint.pformat(resp))
+ if "content-type" in resp and resp['content-type'] == 'application/json':
+ try:
+ content = json.loads(content)
+ content = json.dumps(content,indent=2)
+ except ValueError:
+ pass
+
+ _logger.debug("content : %s",content)
View
21 geonode/geoserver/uploader/utils.py
@@ -0,0 +1,21 @@
+from zipfile import ZipFile
+import tempfile
+from os import path
+
+_shp_exts = ["dbf","prj","shx"]
+_shp_exts = _shp_exts + map(lambda s: s.upper(), _shp_exts)
+
+def shp_files(fpath):
+ basename, ext = path.splitext(fpath)
+ paths = [ "%s.%s" % (basename,ext) for ext in _shp_exts ]
+ paths.append(fpath)
+ return filter(lambda f: path.exists(f), paths)
+
+def create_zip(fpaths):
+ _,payload = tempfile.mkstemp(suffix='.zip')
+ zipf = ZipFile(payload,"w")
+ for fp in fpaths:
+ basename = path.basename(fp)
+ zipf.write(fp,basename)
+ zipf.close()
+ return payload
View
4 geonode/layers/forms.py
@@ -78,8 +78,8 @@ def clean(self):
if base_ext.lower() == '.zip':
# for now, no verification, but this could be unified
pass
- elif base_ext.lower() not in (".shp", ".tif", ".tiff", ".geotif", ".geotiff", ".csv"):
- raise forms.ValidationError("Only Shapefiles, GeoTiffs, and CSV files are supported. You uploaded a %s file" % base_ext)
+ elif base_ext.lower() not in (".shp", ".tif", ".tiff", ".geotif", ".geotiff"):
+ raise forms.ValidationError("Only Shapefiles and GeoTiffs are supported. You uploaded a %s file" % base_ext)
if base_ext.lower() == ".shp":
dbf_file = cleaned["dbf_file"]
shx_file = cleaned["shx_file"]
View
2  geonode/layers/models.py
@@ -41,7 +41,7 @@
from geonode import GeoNodeException
from geonode.utils import _wms, _user, _password, get_wms, bbox_to_wkt
-from geonode.gs_helpers import cascading_delete
+from geonode.geoserver.helpers import cascading_delete
from geonode.people.models import Profile, Role
from geonode.security.models import PermissionLevelMixin
from geonode.security.models import AUTHENTICATED_USERS, ANONYMOUS_USERS
View
36 geonode/layers/utils.py
@@ -30,6 +30,7 @@
import sys
# Django functionality
+from django.contrib.auth.models import User
from django.template.defaultfilters import slugify
from django.conf import settings
@@ -39,17 +40,17 @@
from geonode.utils import check_geonode_is_up
from geonode.people.utils import get_valid_user
from geonode.layers.models import Layer
+from geonode.people.models import Profile
+from geonode.geoserver.helpers import cascading_delete, get_sld_for, delete_from_postgis
from geonode.layers.metadata import set_metadata
from geonode.people.models import Profile
-from geonode.gs_helpers import cascading_delete
-from geonode.gs_helpers import get_sld_for
-from geonode.gs_helpers import delete_from_postgis
from django.contrib.auth.models import User
from geonode.security.models import AUTHENTICATED_USERS, ANONYMOUS_USERS
# Geoserver functionality
import geoserver
from geoserver.catalog import FailedRequestError
from geoserver.resource import FeatureType, Coverage
+from zipfile import ZipFile
logger = logging.getLogger('geonode.layers.utils')
@@ -75,11 +76,28 @@ def layer_type(filename):
returns a gsconfig resource_type string
that can be either 'featureType' or 'coverage'
"""
- extension = os.path.splitext(filename)[1]
- if extension.lower() in ['.shp']:
- return FeatureType.resource_type
- elif extension.lower() in ['.tif', '.tiff', '.geotiff', '.geotif']:
- return Coverage.resource_type
+ base_name, extension = os.path.splitext(filename)
+
+ shp_exts = ['.shp',]
+ cov_exts = ['.tif', '.tiff', '.geotiff', '.geotif']
+ csv_exts = ['.csv']
+ kml_exts = ['.kml']
+
+ if extension.lower() == '.zip':
+ zf = ZipFile(filename)
+ # ZipFile doesn't support with statement in 2.6, so don't do it
+ try:
+ for n in zf.namelist():
+ b, e = os.path.splitext(n.lower())
+ if e in shp_exts or e in cov_exts or e in csv_exts:
+ base_name, extension = b,e
+ finally:
+ zf.close()
+
+ if extension.lower() in shp_exts + csv_exts + kml_exts:
+ return FeatureType.resource_type
+ elif extension.lower() in cov_exts:
+ return Coverage.resource_type
else:
msg = ('Saving of extension [%s] is not implemented' % extension)
raise GeoNodeException(msg)
@@ -554,7 +572,6 @@ def get_default_user():
'before importing data. '
'Try: django-admin.py createsuperuser')
-
def file_upload(filename, user=None, title=None,
skip=True, overwrite=False, keywords=()):
"""Saves a layer in GeoNode asking as little information as possible.
@@ -732,3 +749,4 @@ def _create_db_featurestore(name, data, overwrite=False, charset=None):
except Exception:
delete_from_postgis(name)
raise
+
View
22 geonode/local_settings.py.sample
@@ -0,0 +1,22 @@
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.postgresql_psycopg2',
+ 'NAME': 'geonode',
+ 'USER': 'geonode',
+ 'PASSWORD': 'geonode',
+ }
+}
+
+# GeoNode vector data backend configuration.
+
+#Import uploaded shapefiles into a database such as PostGIS?
+DB_DATASTORE = True
+
+#Database datastore connection settings
+DB_DATASTORE_DATABASE = 'geonode_imports'
+DB_DATASTORE_USER = 'geonode'
+DB_DATASTORE_PASSWORD = 'geonode'
+DB_DATASTORE_HOST = 'localhost'
+DB_DATASTORE_PORT = '5432'
+DB_DATASTORE_TYPE = 'postgis'
+DB_DATASTORE_NAME = 'geonode_imports'
View
1  geonode/settings.py
@@ -163,6 +163,7 @@
# GeoNode internal apps
'geonode.maps',
+ 'geonode.upload',
'geonode.layers',
'geonode.people',
'geonode.proxy',
View
421 geonode/static/geonode/js/upload/ext/layer_upload.js
@@ -0,0 +1,421 @@
+function setup(options) {
+ Ext.onReady(function() {
+ init(options);
+ });
+}
+function init(options) {
+ Ext.QuickTips.init();
+ options = Ext.apply({
+ is_featuretype : true,
+ layer_name : null
+ },options);
+
+ var xml_unsafe = /(^[^a-zA-Z\._]+)|([^a-zA-Z0-9\._])/g;
+ var layer_title;
+ if (options.layer_name) {
+ layer_title = new Ext.form.TextField({
+ id: 'layer_name',
+ name: 'layer_name',
+ emptyText: options.layer_name,
+ fieldLabel: gettext('Name'),
+ allowBlank: true,
+ disabled: true
+ });
+ } else {
+ layer_title = new Ext.form.TextField({
+ id: 'layer_title',
+ fieldLabel: gettext('Title'),
+ name: 'layer_title'
+ });
+ }
+
+ var listeners = {
+ "fileselected": function(cmp, value) {
+ // remove the path from the filename - avoids C:/fakepath etc.
+ cmp.setValue(value.split(/[/\\]/).pop());
+ }
+ };
+ var form_fields = [layer_title,{
+ xtype: "hidden",
+ name: "csrfmiddlewaretoken",
+ value: options.csrf_token
+ }];
+
+ var base_file = new Ext.ux.form.FileUploadField({
+ id: 'base_file',
+ emptyText: gettext('Select a layer data file'),
+ fieldLabel: gettext('Data'),
+ name: 'base_file',
+ allowBlank: false,
+ listeners: listeners
+ });
+
+ var dbf_file = new Ext.ux.form.FileUploadField({
+ id: 'dbf_file',
+ emptyText: gettext('Select a .dbf data file'),
+ fieldLabel: gettext('DBF'),
+ name: 'dbf_file',
+ allowBlank: true,
+ listeners: listeners,
+ validator: function(name) {
+ if ((name.length > 0) && (name.search(/\.dbf$/i) == -1)) {
+ return gettext("Invalid DBF File.");
+ } else {
+ return true;
+ }
+ }
+ });
+
+ var shx_file = new Ext.ux.form.FileUploadField({
+ id: 'shx_file',
+ emptyText: gettext('Select a .shx data file'),
+ fieldLabel: gettext('SHX'),
+ name: 'shx_file',
+ allowBlank: true,
+ listeners: listeners,
+ validator: function(name) {
+ if ((name.length > 0) && (name.search(/\.shx$/i) == -1)) {
+ return gettext("Invalid SHX File.");
+ } else {
+ return true;
+ }
+ }
+ });
+
+ var prj_file = new Ext.ux.form.FileUploadField({
+ id: 'prj_file',
+ emptyText: gettext('Select a .prj data file (optional)'),
+ fieldLabel: gettext('PRJ'),
+ name: 'prj_file',
+ allowBlank: true,
+ listeners: listeners,
+ validator: function(name) {
+ if ((name.length > 0) && (name.search(/\.prj$/i) == -1)) {
+ return gettext("Invalid PRJ File.");
+ } else {
+ return true;
+ }
+ }
+ });
+
+ var sld_file = new Ext.ux.form.FileUploadField({
+ id: 'sld_file',
+ emptyText: gettext('Select a .sld style file (optional)'),
+ fieldLabel: gettext('SLD'),
+ name: 'sld_file',
+ allowBlank: true,
+ listeners: listeners
+ });
+
+ var xml_file = new Ext.ux.form.FileUploadField({
+ id: 'xml_file',
+ emptyText: gettext('Select a .xml metadata file (ISO, Dublin Core, FGDC [optional])'),
+ fieldLabel: gettext('XML'),
+ name: 'xml_file',
+ allowBlank: true,
+ listeners: listeners
+ });
+
+ var abstractField = new Ext.form.TextArea({
+ id: 'abstract',
+ fieldLabel: gettext('Abstract'),
+ name: 'abstract',
+ allowBlank: true
+ });
+
+ var permissionsField = new Ext.form.Hidden({
+ name: "permissions"
+ });
+
+ form_fields.push(base_file);
+
+ if (options.is_featuretype) {
+ form_fields = form_fields.concat(dbf_file, shx_file, prj_file);
+ }
+
+ if (!options.layer_name) {
+ form_fields = form_fields.concat(sld_file, xml_file, abstractField,permissionsField);
+ }
+
+ var fp = new Ext.FormPanel({
+ renderTo: 'upload_form',
+ fileUpload: true,
+ width: 500,
+ frame: true,
+ autoHeight: true,
+ unstyled: true,
+ labelWidth: 50,
+ bodyStyle: 'padding: 10px 10px 0 10px;',
+ defaults: {
+ anchor: '95%',
+ msgTarget: 'side'
+ },
+ items: form_fields,
+ buttons: [{
+ text: gettext('Upload'),
+ handler: function(){
+ if (fp.getForm().isValid()) {
+ fp.getForm().submit({
+ url: options.form_target,
+ waitMsg: gettext('Uploading your data...'),
+ success: function(form, action) {
+ document.location = action.result.redirect_to;
+ },
+ failure: function(form, action) {
+ Ext.Msg.show({
+ title: gettext('Error'),
+ msg: action.response.responseText,
+ midWidth: 200,
+ modal: true,
+ icon: Ext.Msg.ERROR,
+ buttons: Ext.Msg.Ok
+ });
+ }
+ });
+ }
+ }
+ }]
+ });
+
+ var disable_shapefile_inputs = function() {
+ dbf_file.hide();
+ shx_file.hide();
+ prj_file.hide();
+ };
+
+ var enable_shapefile_inputs = function() {
+ dbf_file.show();
+ shx_file.show();
+ prj_file.show();
+ };
+
+ var check_shapefile = function() {
+ if ((/\.shp$/i).test(base_file.getValue())) {
+ enable_shapefile_inputs();
+ } else {
+ disable_shapefile_inputs();
+ }
+ };
+
+ base_file.addListener('fileselected', function(cmp, value) {
+ check_shapefile();
+ });
+
+ if (options.layer_name) {
+ enable_shapefile_inputs();
+ } else {
+ disable_shapefile_inputs();
+ }
+
+ if (! options.layer_name) {
+ var permissionsEditor = new GeoNode.PermissionsEditor({
+ renderTo: "permissions_form",
+ userLookup: options.userLookup,
+ listeners: {
+ updated: function(pe) {
+ permissionsField.setValue(Ext.util.JSON.encode(pe.writePermissions()));
+ }
+ },
+ permissions: {
+ anonymous: 'layer_readonly',
+ authenticated: 'layer_readonly',
+ users:[]
+ }
+ });
+ permissionsEditor.fireEvent("updated", permissionsEditor);
+ }
+
+ function test_file_api() {
+ var fi = document.createElement('INPUT');
+ fi.type = 'file';
+ return 'files' in fi;
+ }
+
+ if (test_file_api()) {
+ // track dropped files separately from values of input fields
+ var dropped_files = {};
+ // drop handler
+ var drop = function(ev) {
+ ev.preventDefault();
+ var dt = ev.dataTransfer, files = dt.files, i = 0, ext, key, w;
+ // this is the single file drop - it may be a tiff or a shp file or a zip
+ if (files.length == 1 && !dbf_file.isVisible()) {
+ base_file.setValue(files[i].name);
+ check_shapefile();
+ dropped_files.base_file = files[i];
+ } else {
+ // multiple file drop
+ for (; i < files.length; i++) {
+ ext = files[i].name.split('.');
+ // grab the last part to avoid .shp.xml getting sucked in
+ ext = ext[ext.length - 1];
+ if (ext == 'shp') {
+ base_file.setValue(files[i].name);
+ enable_shapefile_inputs();
+ dropped_files.base_file = files[i];
+ } else {
+ try {
+ key = ext + '_file', w = eval(key);
+ w.setValue(files[i].name);
+ dropped_files[key] = files[i];
+ } catch (ReferenceError) {}
+ }
+ }
+ }
+ };
+
+ // drop target w/ drag over/exit effects
+ var dropPanel = new Ext.Container({
+ html: "Drop Files Here",
+ cls: 'x-panel-body',
+ style: { borderWidth: '1px', borderStyle: 'solid', textAlign: 'center'},
+ listeners: {
+ render: function(p) {
+ var el = p.getEl().dom;
+ function t() {p.getEl().toggleClass('x-grid3-cell-selected');}
+ el.addEventListener("dragover", function(ev) {
+ ev.stopPropagation();
+ ev.preventDefault();
+ }, true);
+ el.addEventListener("drop", function(ev) {
+ p.getEl().removeClass('x-grid3-cell-selected');
+ drop(ev);
+ },false);
+ el.addEventListener("dragexit",t);
+ el.addEventListener("dragenter",t);
+ }
+ }
+ });
+ fp.add(dropPanel);
+ fp.doLayout();
+
+ function createDragFormData() {
+ var data = new FormData(), id, value, fields = fp.getForm().getFieldValues(), size = 0;
+ for (id in fields) {
+ value = fields[id];
+ if (id in dropped_files) {
+ size = size + dropped_files[id].size;
+ data.append(id,dropped_files[id],value);
+ } else {
+ data.append(id,value);
+ }
+ }
+ return data;
+ }
+
+ function upload(formData) {
+ var xhr = new XMLHttpRequest();
+ var progress;
+ xhr.upload.addEventListener('loadstart', function(ev) {
+ progress = Ext.MessageBox.progress("Please wait","Uploading your data...");
+ }, false);
+ xhr.upload.addEventListener('progress', function(ev) {
+ if (ev.lengthComputable) {
+ // assume that 25% of the time will be actual server work, not just upload time
+ var msg = parseInt(ev.loaded / 1024) + " of " + parseInt(ev.total / 1024);
+ progress.updateProgress( (ev.loaded/ev.total)* .75, msg);
+ if (ev.loaded == ev.total) {
+ progress.updateProgress(.75,"Awaiting response");
+ }
+ }
+ }, false);
+
+ function error(ev,result) {
+ var error_message;
+ if (typeof result != 'undefined') {
+ error_message = '<ul>';
+ for (var i = 0; i < result.errors.length; i++) {
+ error_message += '<li>' + result.errors[i] + '</li>'
+ }
+ error_message += '</ul>'
+ } else {
+ error_message = "Unexpected Error:<p>" + xhr.responseText;
+ }
+
+ Ext.Msg.show({
+ title: gettext("Error"),
+ msg: error_message,
+ minWidth: 200,
+ modal: true,
+ icon: Ext.Msg.ERROR,
+ buttons: Ext.Msg.OK
+ });
+ }
+ xhr.addEventListener('load', function(ev) {
+ try {
+ var result = Ext.decode(xhr.responseText);
+ if (result.success) {
+ if (result.progress) {
+ pollProgress(result.redirect_to, result.progress, fp.getForm().el);
+ } else {
+ document.location = result.redirect_to;
+ }
+ } else {
+ error(ev, result);
+ }
+ } catch (ex) {
+ console.log(ex);
+ error(ev);
+ }
+ }, false);
+ xhr.addEventListener('error', error, false);
+
+ xhr.open("POST",options.form_target, true);
+ xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
+ xhr.send(formData);
+ }
+
+ var originalHandler = fp.buttons[0].handler;
+ fp.buttons[0].handler = function() {
+ if (!fp.getForm().isValid()) return;
+ if ('base_file' in dropped_files) {
+ upload(createDragFormData());
+ } else {
+ originalHandler();
+ }
+ }
+ }
+
+ var confirmDelete = true;
+ var activeDelete = null;
+ function deleteUpload(el) {
+ var a = new Ext.Element(el);
+ Ext.Ajax.request({
+ url : a.getAttribute('href'),
+ success : function() {
+ var uip = a.parent('.uip');
+ Ext.get('confirm-delete').hide().appendTo(uip.parent());
+ uip.remove();
+ if (Ext.select('div.uip').getCount() == 0) {
+ Ext.select('section.uip').addClass('hide');
+ }
+ },
+ failure : function() {
+ alert('Uh oh. An error occurred.')
+ }
+ });
+ Ext.get('confirm-delete').hide();
+ }
+ Ext.select('#confirm-delete a').on('click',function(ev) {
+ var resp = Ext.get(this).getAttribute('href');
+ ev.preventDefault();
+ if (/n/.test(resp)) {
+ Ext.get('confirm-delete').hide();
+ } else {
+ if (/yy/.test(resp)) {
+ confirmDelete = false;
+ }
+ deleteUpload(activeDelete);
+ }
+
+ });
+ Ext.select('.uip .icon-trash').on('click',function(ev) {
+ ev.preventDefault();
+ if (confirmDelete) {
+ activeDelete = this;
+ Ext.get('confirm-delete').removeClass('hide').appendTo(Ext.get(this).parent('.uip')).enableDisplayMode().show();
+ } else {
+ deleteUpload(this);
+ }
+ });
+}
View
26 geonode/static/geonode/js/upload/ext/upload_common.js
@@ -2,7 +2,7 @@ function pollProgress(redirectTo, progressEndpoint, form) {
var progress = Ext.MessageBox.progress("Please wait","Ingesting data"),
formDom = form.dom;
function success(response,redirectTo) {
- var state, msg;
+ var percent, msg;
response = Ext.decode(response.responseText);
// response will contain state, one of :
// PENDING, READY, RUNNING, NO_CRS, NO_BOUNDS, ERROR, COMPLETE
@@ -16,7 +16,9 @@ function pollProgress(redirectTo, progressEndpoint, form) {
return;
} else if ('progress' in response) {
msg = 'Ingested ' + response.progress + " of " + response.total;
- progress.updateProgress( response.progress/response.total, msg );
+ percent = response.progress/response.total;
+ percent = isNaN(percent) ? 0 : percent;
+ progress.updateProgress( percent, msg );
} else {
switch (response.state) {
// give it a chance to start running or return complete
@@ -57,14 +59,26 @@ function enableUploadProgress(uploadFormID) {
// AJAX submit form
var form = Ext.get(uploadFormID), extForm = new Ext.form.BasicForm(form);
form.on('submit',function(ev) {
- ev.preventDefault();
+ // IE8 event handling doesn't order the handlers properly
+ // if more than one is added to the form submit listeners
+ if ('beforeaction' in form) {
+ if (form.beforeaction() == false) {
+ ev.preventDefault();
+ return;
+ }
+ }
extForm.on('actioncomplete',function(form,xhrlike) {
var resp = Ext.decode(xhrlike.response.responseText);
- pollProgress(resp.redirect_to, resp.progress, Ext.get(uploadFormID));
+ if (resp.progress) {
+ pollProgress(resp.redirect_to, resp.progress, Ext.get(uploadFormID));
+ } else {
+ // if there is no progress, we should just continue on to the next step
+ document.location = resp.redirect_to;
+ }
});
extForm.on('actionfailed',function(form,xhrlike) {
var msg = "result" in xhrlike ?
- xhrlike.result.errors.join("\n") :
+ xhrlike.result.errors.join("<br/>") :
xhrlike.response.responseText;
Ext.MessageBox.show({
icon : Ext.MessageBox.ERROR,
@@ -75,4 +89,4 @@ function enableUploadProgress(uploadFormID) {
});
}
});
-}
+}
View
7 geonode/tests/integration.py
@@ -50,7 +50,7 @@
from geonode.maps.utils import *
-from geonode.gs_helpers import cascading_delete, fixup_style
+from geonode.geoserver.helpers import cascading_delete, fixup_style
import gisdata
import zipfile
@@ -383,6 +383,7 @@ def test_delete_layer(self):
self.assertRaises(ObjectDoesNotExist,
lambda: Layer.objects.get(pk=shp_layer_id))
+ # geonode.geoserver.helpers
# If catalogue is installed, then check that it is deleted from there too.
if 'geonode.catalogue' in settings.INSTALLED_APPS:
from geonode.catalogue import get_catalogue
@@ -393,10 +394,8 @@ def test_delete_layer(self):
assert shp_layer_gn_info == None
- # geonode.maps.gs_helpers
-
def test_cascading_delete(self):
- """Verify that the gs_helpers.cascading_delete() method is working properly
+ """Verify that the helpers.cascading_delete() method is working properly
"""
gs_cat = Layer.objects.gs_catalog
View
0  geonode/upload/__init__.py
No changes.
View
36 geonode/upload/admin.py
@@ -0,0 +1,36 @@
+#########################################################################
+#
+# Copyright (C) 2012 OpenPlans
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+#########################################################################
+from geonode.upload.models import Upload, UploadFile
+
+from django.contrib import admin
+
+
+def import_link(obj):
+ return "<a href='%s'>Geoserver Importer Link</a>" % obj.get_import_url()
+
+import_link.short_description = 'Link'
+import_link.allow_tags = True
+
+class UploadAdmin(admin.ModelAdmin):
+ list_display = ('user','date', 'state', import_link)
+ date_hierarchy = 'date'
+ list_filter = ('user','state')
+
+admin.site.register(Upload, UploadAdmin)
+admin.site.register(UploadFile)
View
172 geonode/upload/files.py
@@ -0,0 +1,172 @@
+#########################################################################
+#
+# Copyright (C) 2012 OpenPlans
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+#########################################################################
+'''An incomplete replacement for the various file support functions currently
+scattered over the codebase
+
+@todo complete and use
+'''
+
+import os.path
+from geoserver.resource import FeatureType
+from geoserver.resource import Coverage
+
+import zipfile
+import os
+import re
+
+
+vector = FeatureType.resource_type
+raster = Coverage.resource_type
+
+
+xml_unsafe = re.compile(r"(^[^a-zA-Z\._]+)|([^a-zA-Z\._0-9]+)")
+
+
+class SpatialFile(object):
+
+ is_compressed = False
+ file_type = "unknown"
+ layer_type = None
+ base_file = None
+ auxillary_files = None
+ sld_files = None
+
+ def __init__(self, **kwargs):
+ for k in kwargs:
+ if hasattr(self, k):
+ setattr(self, k, kwargs[k])
+ else:
+ raise ValueError("%s invalid arg" % k)
+
+
+class FileType(object):
+
+ name = None
+ code = None
+ auxillary_file_exts = None
+ aliases = None
+ layer_type = None
+
+ def __init__(self, name, code, layer_type, aliases=None, auxillary_file_exts=None):
+ self.name = name
+ self.code = code
+ self.layer_type = layer_type
+ self.aliases = aliases or []
+ self.auxillary_file_exts = auxillary_file_exts or []
+
+ def matches(self, ext):
+ return ext == self.code or ext in self.aliases
+
+ def build_spatial_file(self, base, others, is_compressed):
+ aux_files, slds = self.find_auxillary_files(base, others)
+ return SpatialFile( is_compressed=is_compressed, file_type=self.code,
+ layer_type = self.layer_type, base_file=base,
+ auxillary_files = aux_files, sld_files = slds
+ )
+
+ def find_auxillary_files(self, base, others):
+ base_name = os.path.splitext(base)[0]
+ base_matches = filter( lambda f: os.path.splitext(f)[0] == base_name, others)
+ slds = _find_sld_files(base_matches)
+ aux_files = filter( lambda f: os.path.splitext(f)[1][1:].lower() in self.aux_files, others)
+ return aux_files, slds
+
+ def __repr__(self):
+ return "%s - %s" % (self.__class__, self.code)
+
+
+TYPE_UNKNOWN = FileType("unknown", None, None)
+
+types = [
+ FileType("Shapefile", "shp", vector, auxillary_file_exts=('dbf','shx','prj')),
+ FileType("GeoTIFF", "tif", raster, aliases=('tiff','geotif','geotiff')),
+ FileType("CSV", "csv", vector),
+]
+
+
+def _contains_bad_names(file_names):
+ '''return True if the list of names contains a bad one'''
+ xml_unsafe = re.compile(r"(^[^a-zA-Z\._]+)|([^a-zA-Z\._0-9]+)")
+ return any([ xml_unsafe.search(f) for f in file_names ])
+
+
+def _rename_files(file_names):
+ renamed = []
+ for f in file_names:
+ dirname, base_name = os.path.split(f)
+ safe = xml_unsafe.sub("_", base_name)
+ if safe != base_name:
+ safe = os.path.join(dirname, safe)
+ os.rename(f, safe)
+ renamed.append(safe)
+ return renamed
+
+
+def _find_sld_files(file_names):
+ return filter( lambda f: f.lower().endswith('.sld'), file_names)
+
+
+def scan_file(file_name):
+ '''get a list of SpatialFiles for the provided file'''
+
+ dirname = os.path.dirname(file_name)
+ files = None
+ is_compressed = False
+
+ if zipfile.is_zipfile(file_name):
+ zf = None
+ try:
+ zf = zipfile.ZipFile(file_name, 'r')
+ files = zf.namelist()
+ if _contains_bad_names(files):
+ zf.extractall(dirname)
+ files = None
+ else:
+ is_compressed = True
+ for f in _find_sld_files(files):
+ zf.extract(f, dirname)
+ except:
+ raise Exception('Unable to read zip file')
+ zf.close()
+
+ if files is None:
+ files = os.listdir(dirname)
+
+ _rename_files(files)
+ found = []
+
+ for file_type in types:
+ for f in files:
+ name, ext = os.path.splitext(f)
+ ext = ext[1:].lower()
+ if file_type.matches(ext):
+ found.append( file_type.build(f, files, is_compressed) )
+
+ found.extend( [SpatialFile(f) for f in found] )
+
+ # detect slds and assign iff a single upload is found
+ sld_files = _find_sld_files(files)
+ if sld_files:
+ if len(found) == 1:
+ found[0].sld_files = sld_files
+ else:
+ raise Exception("One or more SLD files was provided, but no " +
+ "matching files were found for them.")
+
+ return found
View
130 geonode/upload/forms.py
@@ -0,0 +1,130 @@
+#########################################################################
+#
+# Copyright (C) 2012 OpenPlans
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+#########################################################################
+from django import forms
+from django.conf import settings
+from geonode.layers.forms import JSONField
+from geonode.upload.models import UploadFile
+import os
+import tempfile
+import files
+
+class UploadFileForm(forms.ModelForm):
+ class Meta:
+ model = UploadFile
+
+
+class LayerUploadForm(forms.Form):
+ base_file = forms.FileField()
+ dbf_file = forms.FileField(required=False)
+ shx_file = forms.FileField(required=False)
+ prj_file = forms.FileField(required=False)
+ sld_file = forms.FileField(required=False)
+ xml_file = forms.FileField(required=False)
+
+ abstract = forms.CharField(required=False)
+ layer_title = forms.CharField(required=False)
+ permissions = JSONField()
+
+ spatial_files = ("base_file", "dbf_file", "shx_file", "prj_file", "sld_file", "xml_file")
+
+ def clean(self):
+ requires_datastore = () if settings.DB_DATASTORE else ('csv',)
+ types = [ t for t in files.types if t.code not in requires_datastore]
+ supported_type = lambda ext: any([t.matches(ext) for t in types])
+
+ cleaned = super(LayerUploadForm, self).clean()
+ base_name, base_ext = os.path.splitext(cleaned["base_file"].name)
+ if base_ext.lower() == '.zip':
+ # for now, no verification, but this could be unified
+ pass
+ elif not supported_type(base_ext.lower()[1:]):
+ supported = " , ".join([t.name for t in types])
+ raise forms.ValidationError("%s files are supported. You uploaded a %s file" % (supported, base_ext))
+ if base_ext.lower() == ".shp":
+ dbf_file = cleaned["dbf_file"]
+ shx_file = cleaned["shx_file"]
+ if dbf_file is None or shx_file is None:
+ raise forms.ValidationError("When uploading Shapefiles, .SHX and .DBF files are also required.")
+ dbf_name, __ = os.path.splitext(dbf_file.name)
+ shx_name, __ = os.path.splitext(shx_file.name)
+ if dbf_name != base_name or shx_name != base_name:
+ raise forms.ValidationError("It looks like you're uploading "
+ "components from different Shapefiles. Please "
+ "double-check your file selections.")
+ if cleaned["prj_file"] is not None:
+ prj_file = cleaned["prj_file"].name
+ if os.path.splitext(prj_file)[0] != base_name:
+ raise forms.ValidationError("It looks like you're "
+ "uploading components from different Shapefiles. "
+ "Please double-check your file selections.")
+ return cleaned
+
+ def write_files(self):
+ tempdir = tempfile.mkdtemp(dir=settings.FILE_UPLOAD_TEMP_DIR)
+ for field in self.spatial_files:
+ f = self.cleaned_data[field]
+ if f is not None:
+ path = os.path.join(tempdir, f.name)
+ with open(path, 'w') as writable:
+ for c in f.chunks():
+ writable.write(c)
+ absolute_base_file = os.path.join(tempdir,
+ self.cleaned_data["base_file"].name)
+ return tempdir, absolute_base_file
+
+
+class TimeForm(forms.Form):
+ presentation_strategy = forms.CharField(required=False)
+ precision_value = forms.IntegerField(required=False)
+ precision_step = forms.ChoiceField(required=False, choices=[
+ ('years',)*2,
+ ('months',)*2,
+ ('days',)*2,
+ ('hours',)*2,
+ ('minutes',)*2,
+ ('seconds',)*2
+ ])
+
+ def __init__(self, *args, **kwargs):
+ # have to remove these from kwargs or Form gets mad
+ time_names = kwargs.pop('time_names', None)
+ text_names = kwargs.pop('text_names', None)
+ year_names = kwargs.pop('year_names', None)
+ super(TimeForm, self).__init__(*args, **kwargs)
+ self._build_choice('time_attribute', time_names)
+ self._build_choice('end_time_attribute', time_names)
+ self._build_choice('text_attribute', text_names)
+ self._build_choice('end_text_attribute', text_names)
+ if text_names:
+ self.fields['text_attribute_format'] = forms.CharField(required=False)
+ self.fields['end_text_attribute_format'] = forms.CharField(required=False)
+ self._build_choice('year_attribute', year_names)
+ self._build_choice('end_year_attribute', year_names)
+
+ def _build_choice(self, att, names):
+ if names:
+ names.sort()
+ choices = [('', '<None>')] + [(a, a) for a in names]
+ self.fields[att] = forms.ChoiceField(
+ choices=choices, required=False)
+ # @todo implement clean
+
+
+class SRSForm(forms.Form):
+ srs = forms.CharField(required=True)
View
138 geonode/upload/models.py
@@ -0,0 +1,138 @@
+#########################################################################
+#
+# Copyright (C) 2012 OpenPlans
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+#########################################################################
+from geonode.maps.models import Layer
+
+from geonode.geoserver.uploader.uploader import NotFound
+from geonode.upload.utils import gs_uploader
+
+from django.conf import settings
+from django.contrib.auth.models import User
+from django.core.urlresolvers import reverse
+from django.db import models
+
+import cPickle as pickle
+from datetime import datetime
+import logging
+from os import path
+import shutil
+
+
+class UploadManager(models.Manager):
+ def __init__(self):
+ models.Manager.__init__(self)
+
+ def update_from_session(self, upload_session):
+ self.get(import_id = upload_session.import_session.id).update_from_session(upload_session)
+
+ def create_from_session(self, user, import_session):
+ return self.create(
+ user = user,
+ import_id = import_session.id,
+ state= import_session.state)
+
+ def get_incomplete_uploads(self, user):
+ return self.filter(user=user, complete=False).exclude(state=Upload.STATE_INVALID)
+
+
+class Upload(models.Model):
+ objects = UploadManager()
+
+ import_id = models.BigIntegerField(null=True)
+ user = models.ForeignKey(User, null=True)
+ # hold importer state or internal state (STATE_)
+ state = models.CharField(max_length=16)
+ date = models.DateTimeField('date', default = datetime.now)
+ layer = models.ForeignKey(Layer, null=True)
+ upload_dir = models.CharField(max_length=100, null=True)
+ name = models.CharField(max_length=64, null=True)
+ complete = models.BooleanField(default = False)
+ # hold our serialized session object
+ session = models.TextField(null=True)
+ # hold a dict of any intermediate Layer metadata - not used for now
+ metadata = models.TextField(null=True)
+
+ class Meta:
+ ordering = ['-date']
+
+ STATE_INVALID = 'INVALID'
+
+ def get_session(self):
+ if self.session:
+ return pickle.loads(str(self.session))
+
+ def update_from_session(self, upload_session):
+ self.state = upload_session.import_session.state
+ self.date = datetime.now()
+ if "COMPLETE" == self.state:
+ self.complete = True
+ self.session = None
+ else:
+ self.session = pickle.dumps(upload_session)
+ if self.upload_dir is None:
+ self.upload_dir = path.dirname(upload_session.base_file)
+ self.name = upload_session.layer_title or upload_session.name
+ self.save()
+
+ def get_resume_url(self):
+ return reverse('data_upload') + "?id=%s" % self.import_id
+
+ def get_delete_url(self):
+ return reverse('data_upload_delete', args=[self.import_id])
+
+ def get_import_url(self):
+ return "%srest/imports/%s" % (settings.GEOSERVER_BASE_URL, self.import_id)
+
+ def delete(self, cascade=True):
+ models.Model.delete(self)
+ if cascade:
+ try:
+ session = gs_uploader().get_session(self.import_id)
+ except NotFound:
+ session = None
+ if session:
+ try:
+ session.delete()
+ except:
+ logging.exception('error deleting upload session')
+ if self.upload_dir and path.exists(self.upload_dir):
+ shutil.rmtree(self.upload_dir)
+
+ def __unicode__(self):
+ return 'Upload [%s] gs%s - %s, %s' % (self.pk, self.import_id, self.name, self.user)
+
+
+class UploadFile(models.Model):
+ upload = models.ForeignKey(Upload, null=True, blank=True)
+ file = models.FileField(upload_to="uploads")
+ slug = models.SlugField(max_length=50, blank=True)
+
+ def __unicode__(self):
+ return self.slug
+
+ @models.permalink
+ def get_absolute_url(self):
+ return ('data_upload_new', )
+
+ def save(self, *args, **kwargs):
+ self.slug = self.file.name
+ super(UploadFile, self).save(*args, **kwargs)
+
+ def delete(self, *args, **kwargs):
+ self.file.delete(False)
+ super(UploadFile, self).delete(*args, **kwargs)
View
21 geonode/upload/signals.py
@@ -0,0 +1,21 @@
+#########################################################################
+#
+# Copyright (C) 2012 OpenPlans
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+#########################################################################
+from django.dispatch import Signal
+
+upload_complete = Signal(providing_args=['layer'])
View
1  geonode/upload/templates/upload/base.html
@@ -0,0 +1 @@
+{% extends "site_base.html" %}
View
93 geonode/upload/templates/upload/layer_upload.html
@@ -0,0 +1,93 @@
+{% extends "base.html" %}
+{% load i18n %}
+{% load static from staticfiles %}
+
+{% block title %} {% trans "Upload Layer" %} - {{ block.super }} {% endblock %}
+
+{% block body_class %}data upload{% endblock body_class %}
+
+{% block head %}
+{% include "geonode/ext_header.html" %}
+{% include "geonode/app_header.html" %}
+{% include "geonode/geo_header.html" %}
+<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}geonode/css/fileuploadfield.css"/>
+<style type="text/css">
+ .uip {
+ border: 1px solid gray;
+ border-radius: 3px;
+ padding: .5em;
+ margin: .5em
+ }
+ /* workaround bizarre firefox bug where filechooser button doesn't work */
+ #base_file-file {
+ width: auto;
+ }
+</style>
+{{ block.super }}
+{% endblock %}
+
+{% block body %}
+ <div class="block">
+ <div class="span8">
+ <h2 class="page-title">{% trans "Upload Layers" %}</h2>
+ {% if incomplete %}
+ <section style="border: none" class="uip">
+ <h3 class="uip">Incomplete Uploads</h3>
+ <p>You have the following incomplete uploads:</p>
+ {% for u in incomplete %}
+ <div class="clearfix uip" style="">
+ <div class="pull-left">{{ u.name }}, last updated on {{ u.date }}</div>
+ <div class="upload_actions pull-right">
+ <a class="btn btn-mini" href="{{ u.get_resume_url }}">Resume</a>
+ <a class="btn btn-mini icon-trash" href="{{ u.get_delete_url }}">Delete</a>
+ </div>
+ </div>
+ </section>
+ {% endfor %}
+ <div id="confirm-delete" class="hide alert alert-warn" style="padding:10px; margin: 10px 0;">
+ Are you sure you want to delete this upload?
+ <div style="margin: 5px 0">
+ <a href="#y" class="btn btn-danger">Delete</a>
+ <a href="#n" class="btn">Cancel</a>
+ </div>
+ <a href="#yy" style="font-weight:normal">Delete, and don't ask me again.</a>
+ </div>
+ {% endif %}
+ {% block additional_info %}{% endblock %}
+ {% if errors %}
+ <div id="errors">
+ {% for error in errors %}
+ <div>{{ error }}</div>
+ {% endfor %}
+ </div>
+ {% endif %}
+
+ {% if enough_storage %}
+ <div id="upload_form"></div>
+ <script type="text/javascript" src="{% static "geonode/js/upload/ext/layer_upload.js" %}"></script>
+ <script type="text/javascript" src="{% static "geonode/js/upload/ext/upload_common.js" %}"></script>
+ <script type="text/javascript">
+ {% autoescape off %}
+ setup({
+ csrf_token : "{{ csrf_token }}",
+ form_target : "{% url data_upload %}",
+ userLookup : "{% url geonode.views.ajax_lookup %}"
+ });
+ {% if async_upload %}
+ enableUploadProgress('upload_form');
+ {% endif %}
+ {% endautoescape %}
+ </script>
+ {% endif %}
+ </div>
+ </div>
+{% endblock %}
+
+{% block sidebar %}
+
+{% if enough_storage %}
+<h3>{%trans "Permissions" %}</h3>
+
+<div id="permissions_form"></div>
+{% endif %}
+{% endblock %}
View
46 geonode/upload/templates/upload/layer_upload_crs.html
@@ -0,0 +1,46 @@
+{% extends "base.html" %}
+{% load i18n %}
+
+{% block title %} {% trans "Upload Layer" %} - {{ block.super }} {% endblock %}
+
+{% block head %}
+{% include "geonode/ext_header.html" %}
+{% include "geonode/app_header.html" %}
+{% include "geonode/geo_header.html" %}
+<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}geonode/css/fileuploadfield.css"/>
+<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}geonode/css/style.css"/>
+{{ block.super }}
+{% endblock %}
+
+{% block body %}
+ <h2> {% trans "Provide CRS for " %} "{{ layer_name }}" </h2>
+
+ <form method="POST" id="crsForm">
+ {% csrf_token %}
+ <h3>Coordinate Reference System</h3>
+ {% if native_crs %}
+ <p>A coordinate reference system for this layer could not be determined.
+ Locate or enter the appropriate ESPG code for this layer below.
+ One way to do this is do visit:
+ <a href="http://prj2epsg.org/search" target="_">prj2epsg</a>
+ and enter the following:
+ </p>
+ <pre>
+ {{ native_crs }}
+ </pre>
+ <div>
+ {{ form.srs.errors }}
+ <label for="id_srs">EPSG Code (SRS) :</label>{{ form.srs }}
+ </div>
+ <input type="submit" class="btn btn-primary" value="Submit">
+ {% else %}
+ <p>There is a problem recognizing the projection of this data. Please
+ reproject this data to a more common projection.</p>
+ </p>
+ {% endif %}
+ </form>
+ <script type="text/javascript" src="{{ STATIC_URL }}geonode/js/upload/ext/upload_common.js"></script>
+ <script type="text/javascript">
+ enableUploadProgress('crsForm');
+ </script>
+{% endblock %}
View
69 geonode/upload/templates/upload/layer_upload_csv.html
@@ -0,0 +1,69 @@
+{% extends "base.html" %}
+{% load i18n %}
+{% load static from staticfiles %}
+
+{% block title %} {% trans "Upload Layer" %} - {{ block.super }} {% endblock %}
+
+{% block head %}
+{% include "geonode/ext_header.html" %}
+{% include "geonode/app_header.html" %}
+{% include "geonode/geo_header.html" %}
+{{ block.super }}
+{% endblock %}
+{% block body %}
+{% if present_choices %}
+<h2>Geospatial Data</h2>
+<p>Please indicate which attributes contain the latitude and longitude
+coordinates in the CSV data.</p>
+{% if guessed_lat_or_lng %}
+<p>With this data, GeoNode was able to guess which attributes contain the
+latitude and longitude coordinates, but please confirm that the correct
+attributes are selected below.</p>
+{% endif %}
+<form method="POST" id="csvForm">
+ {% csrf_token %}
+ {% if error %}
+ <div class="msg alert alert-error">{{ error }}</div>
+ {% endif %}
+ <div>
+ <label for="lat" style="display: inline-block; width: 75px">Latitude</label>
+ <select id="lat" name="lat">
+ <option value="None">Select an attribute</option>
+ {% for option in point_candidates %}
+ <option value="{{ option }}"
+ {% if selected_lat and selected_lat == option %}
+ selected="selected"
+ {% endif %}
+ >{{ option }}</option>
+ {% endfor %}
+ </select>
+ </div>
+ <div style="margin: 10px 0">
+ <label for="lat" style="display: inline-block; width: 75px">Longitude</label>
+ <select id="lng" name="lng">
+ <option value="None">Select an attribute</option>
+ {% for option in point_candidates %}
+ <option value="{{ option }}"
+ {% if selected_lng and selected_lng == option %}
+ selected="selected"
+ {% endif %}
+ >{{ option }}</option>
+ {% endfor %}
+ </select>
+ </div>
+ <input type="submit" value="Next" />
+</form>
+<script type="text/javascript" src="{{ STATIC_URL }}geonode/js/upload/ext/upload_common.js"></script>
+<script type="text/javascript">
+{% autoescape off %}
+{% if async_upload %}
+enableUploadProgress('csvForm');
+{% endif %}
+{% endautoescape %}
+</script>
+{% else %}
+<p>We did not detect columns that could be used for the latitude and longitude.
+Please verify that you have two columns in your csv file that can be used for
+the latitude and longitude.</p>
+{% endif %}
+{% endblock %}
View
218 geonode/upload/templates/upload/layer_upload_time.html
@@ -0,0 +1,218 @@
+{% extends "base.html" %}
+{% load i18n %}
+{% load static from staticfiles %}
+
+{% block title %} {% trans "Upload Layer Step 2" %} - {{ block.super }} {% endblock %}
+
+{% block head %}
+{% include "geonode/ext_header.html" %}
+{% include "geonode/app_header.html" %}
+{% include "geonode/geo_header.html" %}
+<style type="text/css">
+ /* bootstrap workaround */
+ #timeForm label {
+ display: inline;
+ }
+ .formSection {
+ margin-bottom: 1em;
+ }
+ .right input, .right select {
+ }
+ .left input {
+ line-height: 2em;
+ }
+ form input, form select {
+ font-size: small;
+ }
+ form label {
+ line-height: 2em;
+ }
+ .clearfix:after {
+ content: ".";
+ display: block;
+ height: 0;
+ clear: both;
+ visibility: hidden;
+ }
+ .clearfix {
+ display: inline-block;
+ }
+ * html .clearfix {
+ height: 1%;
+ } /* Hides from IE-mac \*/
+ .clearfix {
+ display: block;
+ }
+ #format_input input {
+ width:8em;
+ }
+ #timehelp p {
+ color: black !important;
+ }
+ #timehelp code {
+ background: lightgray;
+ }
+</style>
+{{ block.super }}
+{% endblock %}
+{% block body %}
+<div class="twocol">
+ <h2> {% trans "Editing details for " %} {{ layer_name }} </h2>
+
+ <form method="POST" id="timeForm">
+
+ {% if missing_crs %}
+ <h3>Coordinate Reference System</h3>
+ <p>A coordinate reference system for this layer could not be determined.
+ Locate or enter the appropriate ESPG code for this layer below.
+ One way to do this is do visit: <a href="http://prj2epsg.org/search" target="_">prj2epsg</a> and enter the following:
+ </p>
+ <pre>
+ {{ native_crs }}
+ </pre>
+ <label for="srs">EPSG Code (SRS) :</label><input type="text" name="srs">
+ {% endif %}
+
+ <h3>Time Options</h3>
+ {% csrf_token %}
+
+ <h4>Choose time attribute:</h4>
+ <div class="formSection clearfix" title="Map animations will not be enabled">
+ <input type="radio" name="timetype" checked="checked" id="notime">
+ <label for="notime">{% trans "This data does not have a time attribute" %}</label>
+ </div>
+ {% if time_form.time_attribute %}
+ <div class="formSection clearfix right" title="Use an existing timestamp attribute in the data">
+ <input type="radio" name="timetype" id="existing">
+ <label for="existing">{% trans "Existing Time Attribute" %} :</label>{{ time_form.time_attribute }}
+ </div>
+ {% endif %}
+ {% if time_form.text_attribute %}
+ <div class="formSection clearfix right" title="Convert text in the data to a timestamp using standard date/time representation or a custom format">
+ <input type="radio" name="timetype" id="textattribute">
+ <label for="textattribute">{% trans "Convert Text Attribute" %} :</label>{{ time_form.text_attribute }}
+ <label for="format_select">{% trans "Date Format" %} :
+ <select id="format_select">
+ <option selected="true" value="0">Best Guess</option>
+ <option value="1">Custom</option>
+ </select>
+ <div id="format_input" style="display: inline; visibility: hidden;" class="clearfix">
+ {{ time_form.text_attribute_format }}
+ </div>
+ </div>
+ {% endif %}
+ {% if time_form.year_attribute %}
+ <div class="formSection clearfix right" title="Convert a number field into a year">
+ <input type="radio" name="timetype" id="convertnumber">
+ <label for="convertnumber">{% trans "Convert Number (As Year)" %} :</label>{{ time_form.year_attribute }}
+ </div>
+ {% endif %}
+
+ <h4>Choose optional end time attribute:</h4>
+ {% if time_form.time_attribute %}
+ <div class="formSection clearfix right" title="Use an existing timestamp attribute in the data">
+ <input type="radio" name="end_timetype" id="end_existing">
+ <label for="end_existing">{% trans "Existing Time Attribute" %} :</label>{{ time_form.end_time_attribute }}
+ </div>
+ {% endif %}
+ {% if time_form.text_attribute %}
+ <div class="formSection clearfix right" title="Convert text in the data to a timestamp using standard date/time representation or a custom format">
+ <input type="radio" name="end_timetype" id="textattribute">
+ <label for="end_textattribute">{% trans "Convert Text Attribute" %} :</label>{{ time_form.end_text_attribute }}
+ <label for="end_format_select">{% trans "Date Format" %} :
+ <select id="end_format_select">
+ <option selected="true" value="0">Best Guess</option>
+ <option value="1">Custom</option>
+ </select>
+ <div id="format_input" style="display: inline; visibility: hidden;" class="clearfix">
+ {{ time_form.end_text_attribute_format }}
+ </div>
+ </div>
+ {% endif %}
+ {% if time_form.year_attribute %}
+ <div class="formSection clearfix right" title="Convert a number field into a year">
+ <input type="radio" name="end_timetype" id="end_convertnumber">
+ <label for="end_convertnumber">{% trans "Convert Number (As Year)" %} :</label>{{ time_form.end_year_attribute }}
+ </div>
+ {% endif %}
+
+ <div id="presentation" class="formSection left">
+ <h4>Present time attribute as:</h4>
+ <div>
+ <input id="LIST" type='radio' value='LIST' checked='checked' name='presentation_strategy'/>
+ <label for="LIST"><strong>List</strong> of all the distinct time values</label>
+ </div>
+ <div>
+ <input id="DISCRETE_INTERVAL" type='radio' value='DISCRETE_INTERVAL' name='presentation_strategy'/>
+ <label for="DISCRETE_INTERVAL"><strong>Intervals</strong> defined by the resolution</label>
+ </div>
+ <div>
+ <input id="CONTINUOUS_INTERVAL" type='radio' value='CONTINUOUS_INTERVAL' name='presentation_strategy'/>
+ <label for="CONTINUOUS_INTERVAL"><strong>Continuous Intervals</strong> for data that is frequently updated, resolution describes the frequency of updates</label>
+ </div>
+
+ <div id="precision" style="display:none">
+ <p><strong>Resolution of time attribute: <input type="text" name="precision_value" size="3"/>
+ {{ time_form.precision_step }}
+ </p>
+ </div>
+ </div>
+
+ <input type="submit" value="{% trans "Next" %}"/>
+ </form>
+</div>
+<div id="timehelp" class="threecol">
+ <h3>Need Help?</h3>
+ <h4>Enabling Time</h4>
+ <p>A feature can currently support one or two time attributes. If a single
+ attribute is used, the feature is considered relevant at that single point in time. If two
+ attributes are used, the second attribute represents the end of a valid period for the
+ feature.</p>
+ <h4>Selecting an Attribute</h4>
+ <p>A time attribute can be one of:</p>
+ <ul>
+ <li>An existing date</li>
+ <li>Text that can be converted to a timestamp</li>
+ <li>A number representing a year</li>
+ </ul>
+ <p>
+ For text attributes, one can specify a custom format or use the 'best guess' approach.
+ The most common formatting flags are:
+ </p>
+ <ul>
+ <li><code>y</code> year</li>
+ <li><code>M</code> month</li>
+ <li><code>d</code> day of month</li>
+ <li><code>H</code> hour of day (0-23)</li>
+ <li><code>k</code> hour of day (1-24)</li>
+ <li><code>m</code> minute in hour</li>
+ <li><code>s</code> second in minute</li>
+ </ul>
+
+ <p class="alert alert-info">Note that single quotes represent a literal character.</p>
+ <p class="alert alert-info">To remove ambiguity, repeat a code to represent the maximum number of digits - for example yyyy</p>
+
+ The 'best guess' will handle date and optional time variants of <a href="http://en.wikipedia.org/wiki/ISO_8601">ISO-8601</a>.
+ In terms of the formatting flags noted above, these are:
+ <pre>
+ yyyy-MM-dd'T'HH:mm:ss.SSS'Z'
+ yyyy-MM-dd'T'HH:mm:sss'Z'
+ yyyy-MM-dd'T'HH:mm:ss'Z'
+ yyyy-MM-dd'T'HH:mm'Z'
+ yyyy-MM-dd'T'HH'Z'
+ yyyy-MM-dd
+ yyyy-MM
+ yyyy
+ </pre>
+ </p>
+</div>
+<script type="text/javascript" src="{{ STATIC_URL }}geonode/js/upload/ext/layer_upload_time.js"></script>
+<script type="text/javascript" src="{{ STATIC_URL }}geonode/js/upload/ext/upload_common.js"></script>
+<script type="text/javascript">
+{% autoescape off %}
+{% if async_upload %}
+enableUploadProgress('timeForm');
+{% endif %}
+{% endautoescape %}
+</script>
+{% endblock %}
View
18 geonode/upload/templates/upload/no_upload.html
@@ -0,0 +1,18 @@
+{% extends "base.html" %}
+
+{% block body %}
+<div class="twocol">
+ <p>Your upload is either complete or you haven't resumed an earlier one.</p>
+ <p>Returning to the upload starting page in <span id="cnt">5</span> seconds.</p>
+ <p>Or just go <a href="{% url data_upload %}">now</a>.</p>
+</div>
+<script type="text/javascript">
+ var cnt = 5;
+ window.setInterval(function() {
+ Ext.get('cnt').dom.innerHTML = cnt--;
+ if (cnt == 0) {
+ window.location = "{% url data_upload %}";
+ }
+ }, 1000);
+</script>
+{% endblock %}
View
7 geonode/upload/templates/upload/upload_error.html
@@ -0,0 +1,7 @@
+{% extends "base.html" %}
+
+{% block body %}
+<div class="twocol">
+ <p>{{ error_msg|safe }}
+</div>
+{% endblock %}
View
18 geonode/upload/templates/upload/uploadfile_confirm_delete.html
@@ -0,0 +1,18 @@
+{% extends "base.html" %}
+
+{% block body %}
+<div class="container">
+ <div class="page-header">
+ <h1>Delete {{ object.file.name }}?</h1>
+ </div>
+ <img src="{{ object.file.url }}" alt="">
+
+ <form method="post" action="" class="form">{% csrf_token %}
+ <div class="form-actions">
+ <button type="submit" class="btn btn-danger"><i class="icon-trash icon-white"></i> Delete</button>
+ <button type="button" onclick="window.history.back();" class="btn"><i class="icon-remove"></i> Cancel</button>
+ </div>
+ </form>
+</div>
+
+{% endblock %}
View
73 geonode/upload/templates/upload/uploadfile_form.html
@@ -0,0 +1,73 @@
+{% extends "upload/base.html" %}
+{% load upload_tags %}
+{% block extra_head %}
+<meta name="viewport" content="width=device-width">
+<link rel="stylesheet" href="{{ STATIC_URL }}fileupload/css/bootstrap-responsive.min.css">
+<link rel="stylesheet" href="{{ STATIC_URL }}fileupload/css/bootstrap-image-gallery.min.css">
+<link rel="stylesheet" href="{{ STATIC_URL }}fileupload/css/jquery.fileupload-ui.css">
+
+{% endblock extra_head %}
+{% block body %}
+ <div class="uploader well" align='center'>
+ <!-- The file upload form used as target for the file upload widget -->
+ <form id="fileupload" action="." method="POST" enctype="multipart/form-data">
+ {% csrf_token %}
+ <!-- The fileupload-buttonbar contains buttons to add/delete files and start/cancel the upload -->
+ <div class="row fileupload-buttonbar" align='center'>
+ <div class="span7">
+ <h3>Drag &amp; drop the file here</h3>
+ <div class="note">We support Shapefile and GeoTIFF uploads.</div>
+ <span class="btn btn-success fileinput-button">
+ <i class="icon-plus icon-white"></i>
+ <span>Choose files to Upload</span>
+ <input type="file" name="file" multiple>
+ </span>
+ <button type="submit" class="btn btn-primary start">
+ <i class="icon-upload icon-white"></i>
+ <span>Start upload</span>
+ </button>
+ <button type="reset" class="btn btn-warning cancel">
+ <i class="icon-ban-circle icon-white"></i>
+ <span>Cancel upload</span>
+ </button>
+ <button type="button" class="btn btn-danger delete">
+ <i class="icon-trash icon-white"></i>
+ <span>Delete</span>
+ </button>
+ <input type="checkbox" class="toggle">
+ </div>
+ <!-- The global progress information -->
+ <div class="fileupload-progress fade">
+ <!-- The global progress bar -->
+ <div class="progress progress-success progress-striped active" role="progressbar" aria-valuemin="0" aria-valuemax="100">
+ <div class="bar" style="width:0%;"></div>
+ </div>
+ <!-- The extended global progress information -->
+ <div class="progress-extended">&nbsp;</div>
+ </div>
+ </div>
+ <!-- The loading indicator is shown during file processing -->
+ <div class="fileupload-loading"></div>
+ <br>
+ <!-- The table listing the files available for upload/download -->
+ <table role="presentation" class="table table-striped"><tbody class="files" data-toggle="modal-gallery" data-target="#modal-gallery"></tbody></table>
+ </form>
+ </div>
+ <center><button class="btn btn-large">Next</button></center>
+{% endblock %}
+{% block extra_script %}
+{% upload_js %}
+<script src="{{ STATIC_URL }}fileupload/js/jquery.ui.widget.js"></script>
+<script src="{{ STATIC_URL }}fileupload/js/tmpl.min.js"></script>
+<script src="{{ STATIC_URL }}fileupload/js/load-image.min.js"></script>
+<script src="{{ STATIC_URL }}fileupload/js/canvas-to-blob.min.js"></script>
+<script src="{{ STATIC_URL }}fileupload/js/bootstrap.min.js"></script>
+<script src="{{ STATIC_URL }}fileupload/js/bootstrap-image-gallery.min.js"></script>
+<script src="{{ STATIC_URL }}fileupload/js/jquery.iframe-transport.js"></script>
+<script src="{{ STATIC_URL }}fileupload/js/jquery.fileupload.js"></script>
+<script src="{{ STATIC_URL }}fileupload/js/jquery.fileupload-fp.js"></script>
+<script src="{{ STATIC_URL }}fileupload/js/jquery.fileupload-ui.js"></script>
+<script src="{{ STATIC_URL }}fileupload/js/locale.js"></script>
+<script src="{{ STATIC_URL }}fileupload/js/main.js"></script>
+<script src="{{ STATIC_URL }}fileupload/js/csrf.js"></script>
+{% endblock %}
View
0  geonode/upload/templatetags/__init__.py
No changes.
View
68 geonode/upload/templatetags/upload_tags.py
@@ -0,0 +1,68 @@
+from django import template
+
+register = template.Library()
+
+@register.simple_tag
+def upload_js():
+ return """
+<!-- The template to display files available for upload -->
+<script id="template-upload" type="text/x-tmpl">
+{% for (var i=0, file; file=o.files[i]; i++) { %}
+ <tr class="templ