From 55cddb5c9fee44f2f97197fa135302b8a073afcf Mon Sep 17 00:00:00 2001 From: Nathan Cahill Date: Thu, 19 Nov 2015 22:47:40 -0700 Subject: [PATCH] move scripts and submission to submodules --- .gitmodules | 6 + scripts | 1 + scripts/README.md | 50 ------- scripts/adapters/__init__.py | 4 - scripts/adapters/geojson.py | 7 - scripts/adapters/kml.py | 7 - scripts/adapters/shp.py | 74 ---------- scripts/process.py | 67 --------- scripts/upload.py | 78 ---------- scripts/utils.py | 109 -------------- scripts/validate.py | 41 ------ submission | 1 + submission/.babelrc | 3 - submission/.eslintrc | 10 -- submission/README.md | 3 - submission/dist/submission.min.css | 1 - submission/dist/submission.min.js | 1 - submission/package.json | 36 ----- submission/src/css/dropdown.css | 74 ---------- submission/src/css/submission.css | 67 --------- submission/src/dropdown.js | 39 ----- submission/src/github.js | 172 ---------------------- submission/src/index.js | 229 ----------------------------- submission/src/utils.js | 19 --- submission/webpack.config.js | 17 --- 25 files changed, 8 insertions(+), 1108 deletions(-) create mode 100644 .gitmodules create mode 160000 scripts delete mode 100644 scripts/README.md delete mode 100644 scripts/adapters/__init__.py delete mode 100644 scripts/adapters/geojson.py delete mode 100644 scripts/adapters/kml.py delete mode 100644 scripts/adapters/shp.py delete mode 100644 scripts/process.py delete mode 100644 scripts/upload.py delete mode 100644 scripts/utils.py delete mode 100644 scripts/validate.py create mode 160000 submission delete mode 100644 submission/.babelrc delete mode 100644 submission/.eslintrc delete mode 100644 submission/README.md delete mode 100644 submission/dist/submission.min.css delete mode 100644 submission/dist/submission.min.js delete mode 100644 submission/package.json delete mode 100644 submission/src/css/dropdown.css delete mode 100644 submission/src/css/submission.css delete mode 100644 submission/src/dropdown.js delete mode 100644 submission/src/github.js delete mode 100644 submission/src/index.js delete mode 100644 submission/src/utils.js delete mode 100644 submission/webpack.config.js diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..5cf4be8 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "submission"] + path = submission + url = https://github.com/OpenBounds/SubmissionForm.git +[submodule "scripts"] + path = scripts + url = https://github.com/OpenBounds/Processing.git diff --git a/scripts b/scripts new file mode 160000 index 0000000..8b8f681 --- /dev/null +++ b/scripts @@ -0,0 +1 @@ +Subproject commit 8b8f681ad9b3ef9a1147985f124b94c2064d73a1 diff --git a/scripts/README.md b/scripts/README.md deleted file mode 100644 index 17c5f72..0000000 --- a/scripts/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# Scripts - -### Python Environment - -Create a virtual environment and install the requirements: - -``` -$ virtualenv env -$ source env/bin/activate -$ pip install -r requirements.txt -``` - -### validate.py - -Validate a JSON file against a JSON schema: - -``` -$ python scripts/validate.py schemas/source.json sources/US/MT/antelope.json -``` - -Usage: - -``` -validate.py [OPTIONS] SCHEMA JSONFILE - - SCHEMA: JSON schema to validate against. Required. - JSONFILE: JSON file to validate. Required. - -Options: - --help Show this message and exit. -``` - -### upload.py - -Upload a directory to S3. Requires environment variables `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` and `AWS_BUCKET` to be set. - -``` -$ python scripts/upload.py generated/ -``` - -Usage: - -``` -Usage: upload.py [OPTIONS] DIRECTORY - - DIRECTORY: Directory to upload. Required. - -Options: - --help Show this message and exit. -``` \ No newline at end of file diff --git a/scripts/adapters/__init__.py b/scripts/adapters/__init__.py deleted file mode 100644 index 2941443..0000000 --- a/scripts/adapters/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ - -import shp -import kml -import geojson diff --git a/scripts/adapters/geojson.py b/scripts/adapters/geojson.py deleted file mode 100644 index c042dd8..0000000 --- a/scripts/adapters/geojson.py +++ /dev/null @@ -1,7 +0,0 @@ - -def read(url): - return {} - - -def write(): - pass diff --git a/scripts/adapters/kml.py b/scripts/adapters/kml.py deleted file mode 100644 index c042dd8..0000000 --- a/scripts/adapters/kml.py +++ /dev/null @@ -1,7 +0,0 @@ - -def read(url): - return {} - - -def write(): - pass diff --git a/scripts/adapters/shp.py b/scripts/adapters/shp.py deleted file mode 100644 index d0d459a..0000000 --- a/scripts/adapters/shp.py +++ /dev/null @@ -1,74 +0,0 @@ - -from functools import partial - -import fiona -from fiona.transform import transform_geom - - -def _explode(coords): - """Explode a GeoJSON geometry's coordinates object and - yield coordinate tuples. As long as the input is conforming, - the type of the geometry doesn't matter. - - From @sgillies answer: http://gis.stackexchange.com/a/90554/27367 - """ - for e in coords: - if isinstance(e, (float, int, long)): - yield coords - break - else: - for f in _explode(e): - yield f - - -def _bbox(feat): - x, y = zip(*list(_explode(feat['geometry']['coordinates']))) - return min(x), min(y), max(x), max(y) - - -def _transformer(crs, feat): - tg = partial(transform_geom, crs, 'EPSG:4326', - antimeridian_cutting=True, precision=6) - feat['geometry'] = tg(feat['geometry']) - return feat - - -def read(fp, prop_map): - """Read shapefile. - - :param fp: file-like object - """ - layers = fiona.listlayers('/', vfs='zip://' + fp.name) - - if not layers: - raise IOError - - filename = '/' + layers[0] + '.shp' - - with fiona.open(filename, vfs='zip://' + fp.name) as source: - collection = { - 'type': 'FeatureCollection', - 'features': [], - 'bbox': [float('inf'), float('inf'), float('-inf'), float('-inf')] - } - - for rec in source: - transformed = _transformer(source.crs, rec) - transformed['properties'] = { - key: str(transformed['properties'][value]) - for key, value in prop_map.iteritems() - } - collection['bbox'] = [ - comparator(values) - for comparator, values in zip( - [min, min, max, max], - zip(collection['bbox'], _bbox(transformed)) - ) - ] - collection['features'].append(transformed) - - return collection - - -def write(): - pass diff --git a/scripts/process.py b/scripts/process.py deleted file mode 100644 index 19bd0da..0000000 --- a/scripts/process.py +++ /dev/null @@ -1,67 +0,0 @@ - -import os -from urlparse import urlparse - -import click - -import adapters -import utils - - -@click.command() -@click.argument('sources', type=click.Path(exists=True), required=True) -@click.argument('output', type=click.Path(exists=True), required=True) -@click.option('--force', is_flag=True) -def process(sources, output, force): - """Download sources and process the file to the output directory. - - \b - SOURCES: Source JSON file or directory of files. Required. - OUTPUT: Destination directory for generated data. Required. - """ - for path in utils.get_files(sources): - pathparts = utils.get_path_parts(path) - pathparts[0] = output.strip(os.sep) - pathparts[-1] = pathparts[-1].replace('.json', '.geojson') - - outdir = os.sep.join(pathparts[:-1]) - outfile = os.sep.join(pathparts) - - source = utils.read_json(path) - urlfile = urlparse(source['url']).path.split('/')[-1] - - if not hasattr(adapters, source['filetype']): - utils.error('Unknown filetype', source['filetype'], '\n') - continue - - if os.path.isfile(outfile) and not force: - utils.error('Skipping', path, 'since generated file exists.', - 'Use --force to regenerate.', '\n') - continue - - utils.info('Downloading', source['url']) - - try: - fp = utils.download(source['url']) - except IOError: - utils.error('Failed to download', source['url'], '\n') - continue - - utils.info('Reading', urlfile) - - try: - geojson = getattr(adapters, source['filetype']).read(fp, source['properties']) - except IOError: - utils.error('Failed to read', urlfile) - continue - finally: - os.remove(fp.name) - - utils.make_sure_path_exists(outdir) - utils.write_json(outfile, geojson) - - utils.success('Done. Processed to', outfile, '\n') - - -if __name__ == '__main__': - process() diff --git a/scripts/upload.py b/scripts/upload.py deleted file mode 100644 index 5c39e68..0000000 --- a/scripts/upload.py +++ /dev/null @@ -1,78 +0,0 @@ - -import os - -from boto.s3.connection import S3Connection -from boto.s3.key import Key -from boto.exception import S3ResponseError -import click - -import utils - -AWS_BUCKET = os.environ.get('AWS_BUCKET') - - -def get_or_create_bucket(conn, bucket_name): - """Get or create an S3 bucket. - - :param conn: boto.s3.connection.S3Connection - :param bucket_name: string - :returns: boto.s3.bucket.Bucket - """ - try: - bucket = conn.get_bucket(bucket_name) - except S3ResponseError: - bucket = conn.create_bucket(bucket_name) - - return bucket - - -def sizeof_fmt(num): - """Human readable file size. - - Modified from http://stackoverflow.com/a/1094933/1377021 - - :param num: float - :returns: string - """ - for unit in ['', 'k', 'm', 'g', 't', 'p', 'e', 'z']: - if abs(num) < 1024.0: - return "%.0f%s%s" % (num, unit, 'b') - num /= 1024.0 - - return "%.f%s%s" % (num, 'y', 'b') - - -@click.command() -@click.argument('directory', type=click.Path(exists=True), required=True) -def upload(directory): - """Upload a directory to S3. - - DIRECTORY: Directory to upload. Required. - """ - if not AWS_BUCKET: - utils.error('AWS_BUCKET environment variable not set. Exiting.') - return - - conn = S3Connection() - bucket = get_or_create_bucket(conn, AWS_BUCKET) - - files = list(utils.get_files(directory)) - total_size = 0 - - utils.info('Found', len(files), 'files to upload to s3://' + AWS_BUCKET) - - for path in files: - filesize = os.path.getsize(path) - total_size += filesize - - utils.info('Uploading', path, '-', sizeof_fmt(filesize)) - - k = Key(bucket) - k.key = path - k.set_contents_from_filename(path) - - utils.success('Done. Uploaded', sizeof_fmt(total_size)) - - -if __name__ == '__main__': - upload() diff --git a/scripts/utils.py b/scripts/utils.py deleted file mode 100644 index 8972856..0000000 --- a/scripts/utils.py +++ /dev/null @@ -1,109 +0,0 @@ - -import os -import ujson -import logging -import tempfile -import sys -from urlparse import urlparse - -import click -import requests - -CHUNK_SIZE = 1024 - -logging.basicConfig(level=logging.INFO) - - -def get_files(path): - """Returns an iterable containing the full path of all files in the - specified path. - - :param path: string - :yields: string - """ - if os.path.isdir(path): - for (dirpath, dirnames, filenames) in os.walk(path): - for filename in filenames: - if not filename[0] == '.': - yield os.path.join(dirpath, filename) - else: - yield path - - -def read_json(path): - """Returns JSON dict from file. - - :param path: string - :returns: dict - """ - with open(path, 'r') as jsonfile: - return ujson.loads(jsonfile.read()) - - -def write_json(path, data): - with open(path, 'w') as jsonfile: - jsonfile.write(ujson.dumps(data, double_precision=5)) - - -def make_sure_path_exists(path): - """Make directories in path if they do not exist. - - Modified from http://stackoverflow.com/a/5032238/1377021 - - :param path: string - """ - try: - os.makedirs(path) - except: - pass - - -def get_path_parts(path): - """Splits a path into parent directories and file. - - :param path: string - """ - return path.split(os.sep) - - -def download(url): - """Downloads a file and returns a file pointer to a temporary file. - - :param url: string - """ - res = requests.get(url, stream=True, verify=False) - urlfile = urlparse(url).path.split('/')[-1] - _, extension = os.path.split(urlfile) - - if not res.ok: - raise IOError - - fp = tempfile.NamedTemporaryFile('wb', suffix=extension, delete=False) - - for chunk in res.iter_content(CHUNK_SIZE): - fp.write(chunk) - - fp.close() - - return fp - - -def info(*strings): - if sys.stdout.isatty(): - click.echo(' '.join(strings)) - else: - logging.info(' '.join(strings)) - - -def error(*strings): - if sys.stdout.isatty(): - click.secho(' '.join(strings), fg='red') - else: - logging.error(' '.join(strings)) - - -def success(*strings): - if sys.stdout.isatty(): - click.secho(' '.join(strings), fg='green') - else: - logging.info(' '.join(strings)) diff --git a/scripts/validate.py b/scripts/validate.py deleted file mode 100644 index d0667d0..0000000 --- a/scripts/validate.py +++ /dev/null @@ -1,41 +0,0 @@ - -import json -import re - -import click -import jsonschema - -import utils - - -@click.command() -@click.argument('schema', type=click.File('r'), required=True) -@click.argument('jsonfiles', type=click.Path(exists=True), required=True) -def validate(schema, jsonfiles): - """Validate a JSON files against a JSON schema. - - \b - SCHEMA: JSON schema to validate against. Required. - JSONFILE: JSON files to validate. Required. - """ - schema = json.loads(schema.read()) - - for path in utils.get_files(jsonfiles): - if path.startswith('sources'): - regex = r'sources/[A-Z]{2}/[A-Z]{2}/[a-z-]+.json' - elif path.startswith('generated'): - regex = r'generated/[A-Z]{2}/[A-Z]{2}/[a-z-]+.geojson' - else: - regex = r'' - - if not re.compile(regex).match(path): - raise AssertionError('Path does not match spec for ' + path) - - with open(path) as f: - jsonfile = json.loads(f.read()) - - jsonschema.validate(jsonfile, schema) - - -if __name__ == '__main__': - validate() diff --git a/submission b/submission new file mode 160000 index 0000000..e30fa51 --- /dev/null +++ b/submission @@ -0,0 +1 @@ +Subproject commit e30fa5127b7f34403cc49a0dc13fea602a6527c2 diff --git a/submission/.babelrc b/submission/.babelrc deleted file mode 100644 index eaf3238..0000000 --- a/submission/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["es2015", "stage-0"] -} diff --git a/submission/.eslintrc b/submission/.eslintrc deleted file mode 100644 index b435e4f..0000000 --- a/submission/.eslintrc +++ /dev/null @@ -1,10 +0,0 @@ -{ - "parser": "babel-eslint", - "rules": { - "strict": 0 - }, - "extends": "eslint:recommended", - "env": { - "browser": true - } -} diff --git a/submission/README.md b/submission/README.md deleted file mode 100644 index 0304964..0000000 --- a/submission/README.md +++ /dev/null @@ -1,3 +0,0 @@ -## OpenBounds Data Submission - -A Javascript app running on Github Pages for data submission via pull request. \ No newline at end of file diff --git a/submission/dist/submission.min.css b/submission/dist/submission.min.css deleted file mode 100644 index 8ea1186..0000000 --- a/submission/dist/submission.min.css +++ /dev/null @@ -1 +0,0 @@ -.dropdown-menu::after,.dropdown-menu::before{position:absolute;content:"";left:auto;display:inline-block}.dropdown-btn.selected:focus,.dropdown-btn:focus,.dropdown-btn:focus:hover{border-color:#b5b5b5}.dropdown-btn:focus,.dropdown-btn:focus:hover{box-shadow:none}.dropdown-btn.selected,.dropdown-btn.selected:focus{background-color:#dcdcdc;box-shadow:inset 0 2px 4px rgba(0,0,0,.15)}.dropdown-btn::after{display:inline-block;width:0;height:0;content:"";vertical-align:-2px;margin-left:5px;border:4px solid;border-right-color:transparent;border-left-color:transparent;border-bottom-color:transparent}.dropdown-menu{width:180px;position:absolute;right:10px;top:45px;padding-top:5px;padding-bottom:5px;background-clip:padding-box;box-shadow:0 3px 12px rgba(0,0,0,.15)}.dropdown-menu::before{border:8px solid transparent;border-bottom-color:rgba(0,0,0,.15);top:-16px;right:9px}.dropdown-menu::after{border:7px solid transparent;border-bottom-color:#fff;top:-14px;right:10px}.dropdown-item{padding:4px 10px 4px 15px;color:#333}.dropdown-item:hover{color:#fff;text-decoration:none;text-shadow:none;background-color:#4078c0}#main{position:relative;margin-top:50px}#manual textarea{width:100%;font-family:monospace}#doneerror{float:right}.form.form-inline{display:inline-block;margin-right:10px}.form.form-inline:last-child{margin-right:0}dl.form>dd input[type=text]{width:100%}h2{margin-top:5px;margin-bottom:30px}.avatar{width:20px;margin-right:5px}table,td input{width:100%}table{margin:15px 0}td,th{text-align:left;padding:6px 15px;border-bottom:1px solid #E1E1E1}tr:last-child td{border-bottom:none}td:first-child,th:first-child{padding-left:0}td:last-child,th:last-child{padding-right:0} \ No newline at end of file diff --git a/submission/dist/submission.min.js b/submission/dist/submission.min.js deleted file mode 100644 index 1fb5c26..0000000 --- a/submission/dist/submission.min.js +++ /dev/null @@ -1 +0,0 @@ -!function(modules){function __webpack_require__(moduleId){if(installedModules[moduleId])return installedModules[moduleId].exports;var module=installedModules[moduleId]={exports:{},id:moduleId,loaded:!1};return modules[moduleId].call(module.exports,module,module.exports,__webpack_require__),module.loaded=!0,module.exports}var installedModules={};return __webpack_require__.m=modules,__webpack_require__.c=installedModules,__webpack_require__.p="",__webpack_require__(0)}([function(module,exports,__webpack_require__){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{"default":obj}}function _interopRequireWildcard(obj){if(obj&&obj.__esModule)return obj;var newObj={};if(null!=obj)for(var key in obj)Object.prototype.hasOwnProperty.call(obj,key)&&(newObj[key]=obj[key]);return newObj["default"]=obj,newObj}var _github=__webpack_require__(1),github=_interopRequireWildcard(_github),_utils=__webpack_require__(3),utils=_interopRequireWildcard(_utils),_dropdown=__webpack_require__(4),_dropdown2=_interopRequireDefault(_dropdown),BASE_REPO="OpenBounds/OpenHuntingData",REPO_NAME="OpenHuntingData",TIMEOUT_SECS=15,params=utils.getParams(),form=window.document.forms.submission,pr=window.document.getElementById("pr"),alert=window.document.getElementById("alert"),manual=window.document.getElementById("manual"),signinUser=function(err,user){var button=window.document.getElementById("signin"),signout=window.document.getElementById("signout"),blank=window.document.getElementById("unauthenticated");button.setAttribute("href","#"),button.innerHTML=' '+user.login,blank.style.display="none",form.style.display="block",signout.addEventListener("click",signoutUser),new _dropdown2["default"](button)},signoutUser=function(){github.clearToken(),window.location.href=window.location.pathname},startSubmitting=function(){pr.setAttribute("disabled","disabled"),pr.textContent="Submitting..."},doneSubmitting=function(){pr.removeAttribute("disabled"),pr.textContent="Submit Pull Request"},errorSubmitting=function(msg,content){alert.innerHTML=msg,manual.getElementsByTagName("textarea")[0].textContent=content,alert.style.display="block",manual.style.display="block"},doneError=function(){alert.innerHTML="",manual.getElementsByTagName("textarea")[0].textContent="",alert.style.display="none",manual.style.display="none",doneSubmitting()},addSource=function(username,repo,source){var filename=source.species.join("-").replace(/[\s]/g,"").toLowerCase(),path="sources/"+source.country+"/"+source.state+"/"+filename+".json",branch="add-"+source.country+"-"+source.state+"-"+filename,msg="add "+source.country+"/"+source.state+"/"+filename+".json",errMsg="Error submitting pull request. Create the file "+path+" with the JSON below.",raw=JSON.stringify(source,null,3),content=window.btoa(raw);github.getHead(repo,function(err,sha){return err?errorSubmitting(errMsg,raw):void github.branchRepo(repo,branch,sha,function(err){return err?errorSubmitting(errMsg,raw):void github.createFile(repo,branch,path,content,msg,function(err){return err?errorSubmitting(errMsg,raw):void github.pullRequest(BASE_REPO,username+":"+branch,msg,function(err){return err?errorSubmitting(errMsg,raw):void doneSubmitting()})})})})},submit=function(e){var source=void 0,filename=void 0,path=void 0,errMsg=void 0,raw=void 0;if(e.preventDefault(),github.getToken()){startSubmitting(),source={url:form.url.value,species:form.species.value.split(", "),attribution:form.attribution.value,properties:{},country:form.country.value,state:form.state.value,filetype:form.filetype.value};for(var _arr=["id","name"],_i=0;_i<_arr.length;_i++){var property=_arr[_i];source.properties[property]=form[property].value}filename=source.species.join("-").replace(/[\s]/g,"").toLowerCase(),path="sources/"+source.country+"/"+source.state+"/"+filename+".json",errMsg="Error submitting pull request. Create the file "+path+" with the JSON below.",raw=JSON.stringify(source,null,3),github.getUser(function(err,user){if(err)return errorSubmitting(errMsg,raw);var username=user.login,repo=username+"/"+REPO_NAME;github.getRepo(repo,function(err,response){return err?errorSubmitting(errMsg,raw):void(response?addSource(username,repo,source):github.forkRepo(BASE_REPO,function(err){return err?errorSubmitting(errMsg,raw):void github.getRepo(repo,function(err){if(err)return errorSubmitting(errMsg,raw);var count=0,ping=window.setInterval(function(){github.getHead(repo,function(err,sha){sha?(window.clearInterval(ping),addSource(username,repo,source)):(count+=1,count>2*TIMEOUT_SECS&&(window.clearInterval(ping),errorSubmitting(errMsg,raw)))})},500)})}))})})}};github.getToken()?github.getUser(signinUser):params.code&&github.accessToken(params.code,function(){window.history.replaceState({},window.document.title,window.location.pathname),github.getUser(signinUser)}),form.addEventListener("submit",submit,!1),window.document.getElementById("doneerror").addEventListener("click",doneError,!1)},function(module,exports,__webpack_require__){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{"default":obj}}Object.defineProperty(exports,"__esModule",{value:!0}),exports.pullRequest=exports.createFile=exports.branchRepo=exports.forkRepo=exports.getHead=exports.getRepo=exports.getUser=exports.ajax=exports.accessToken=exports.clearToken=exports.getToken=void 0;var _nanoajax=__webpack_require__(2),_nanoajax2=_interopRequireDefault(_nanoajax),API_BASE="https://api.github.com",token=window.localStorage.getItem("token"),ajax=(exports.getToken=function(){return token},exports.clearToken=function(){window.localStorage.removeItem("token")},exports.accessToken=function(code,cb){_nanoajax2["default"].ajax({url:"http://github-gatekeeper.aws.gaiagps.com/authenticate/"+code},function(code,response){token=JSON.parse(response).token,window.localStorage.setItem("token",token),cb(token)})},exports.ajax=function(options,cb){options.headers={Authorization:"token "+token},_nanoajax2["default"].ajax(options,function(code,response){var parsed=void 0;try{parsed=JSON.parse(response)}catch(e){return cb(e)}return cb(null,parsed)})});exports.getUser=function(cb){ajax({url:API_BASE+"/user"},cb)},exports.getRepo=function(repo,cb){ajax({url:API_BASE+"/repos/"+repo},function(err,response){return err?cb(err):response.message&&"Not Found"===response.message?cb(null,null):void cb(null,response)})},exports.getHead=function(repo,cb){ajax({url:API_BASE+"/repos/"+repo+"/git/refs/heads/master"},function(err,response){return err?cb(err):response.message&&"Git Repository is empty."===response.message?cb(null,null):void cb(null,response.object.sha)})},exports.forkRepo=function(repo,cb){ajax({url:API_BASE+"/repos/"+repo+"/forks",method:"POST"},cb)},exports.branchRepo=function(repo,branch,sha,cb){ajax({url:API_BASE+"/repos/"+repo+"/git/refs",body:JSON.stringify({ref:"refs/heads/"+branch,sha:sha})},cb)},exports.createFile=function(repo,branch,path,content,message,cb){ajax({url:API_BASE+"/repos/"+repo+"/contents/"+path,method:"PUT",body:JSON.stringify({message:message,content:content,branch:branch})},cb)},exports.pullRequest=function(repo,head,message,cb){ajax({url:API_BASE+"/repos/"+repo+"/pulls",body:JSON.stringify({title:message,head:head,base:"master"})},cb)}},function(module,exports){(function(global){function getRequest(cors){return cors&&global.XDomainRequest&&!/MSIE 1/.test(navigator.userAgent)?new XDomainRequest:global.XMLHttpRequest?new XMLHttpRequest:void 0}function setDefault(obj,key,value){obj[key]=obj[key]||value}var reqfields=["responseType","withCredentials","timeout","onprogress"];exports.ajax=function(params,callback){function cb(statusCode,responseText){return function(){called||callback(req.status||statusCode,req.response||req.responseText||responseText,req),called=!0}}var headers=params.headers||{},body=params.body,method=params.method||(body?"POST":"GET"),called=!1,req=getRequest(params.cors);req.open(method,params.url,!0);var success=req.onload=cb(200);req.onreadystatechange=function(){4===req.readyState&&success()},req.onerror=cb(null,"Error"),req.ontimeout=cb(null,"Timeout"),req.onabort=cb(null,"Abort"),body&&(setDefault(headers,"X-Requested-With","XMLHttpRequest"),setDefault(headers,"Content-Type","application/x-www-form-urlencoded"));for(var field,i=0,len=reqfields.length;len>i;i++)field=reqfields[i],void 0!==params[field]&&(req[field]=params[field]);for(var field in headers)req.setRequestHeader(field,headers[field]);return req.send(body),req}}).call(exports,function(){return this}())},function(module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:!0});exports.getParams=function(){var params={},_iteratorNormalCompletion=!0,_didIteratorError=!1,_iteratorError=void 0;try{for(var _step,_iterator=window.location.search.substring(1).split("&")[Symbol.iterator]();!(_iteratorNormalCompletion=(_step=_iterator.next()).done);_iteratorNormalCompletion=!0){var param=_step.value,nv=param.split("=");nv[0]&&(params[nv[0]]=nv[1]||!0)}}catch(err){_didIteratorError=!0,_iteratorError=err}finally{try{!_iteratorNormalCompletion&&_iterator["return"]&&_iterator["return"]()}finally{if(_didIteratorError)throw _iteratorError}}return params}},function(module,exports){"use strict";function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(exports,"__esModule",{value:!0});var Dropdown=function Dropdown(button){var _this=this;_classCallCheck(this,Dropdown),this.open=function(e){e.preventDefault(),_this.closed?(_this.closed=!1,_this.button.nextElementSibling.style.display="block",_this.button.classList.add("selected"),window.setTimeout(function(){window.document.addEventListener("click",_this.close)},50)):_this.close()},this.close=function(e){e.preventDefault(),window.document.removeEventListener("click",_this.close),_this.button.classList.remove("selected"),_this.button.nextElementSibling.style.display="none",_this.closed=!0},this.closed=!0,this.button=button,this.button.classList.add("dropdown-btn"),this.button.addEventListener("click",this.open,!0)};exports["default"]=Dropdown}]); \ No newline at end of file diff --git a/submission/package.json b/submission/package.json deleted file mode 100644 index ba06e15..0000000 --- a/submission/package.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "name": "openboundsdata", - "version": "1.0.0", - "description": "OpenBounds Data Submission", - "main": "src/index.js", - "repository": { - "type": "git", - "url": "https://github.com/OpenBounds/OpenHuntingData.git" - }, - "scripts": { - "lint": "eslint src/*.js", - "build-css": "cleancss --output dist/submission.min.css src/css/*.css", - "build-js": "webpack && uglifyjs dist/submission.js -c -o dist/submission.min.js", - "build": "npm run build-js && npm run build-css" - }, - "author": "OpenBounds", - "license": "MIT", - "bugs": { - "url": "https://github.com/OpenBounds/OpenHuntingData/issues" - }, - "homepage": "https://github.com/OpenBounds/OpenHuntingData", - "dependencies": { - "nanoajax": "^0.4.0" - }, - "devDependencies": { - "babel-core": "^6.1.2", - "babel-eslint": "^4.1.4", - "babel-loader": "^6.0.1", - "babel-preset-es2015": "^6.1.2", - "babel-preset-stage-0": "^6.1.18", - "clean-css": "^3.4.7", - "eslint": "^1.9.0", - "uglify": "^0.1.5", - "webpack": "^1.12.3" - } -} diff --git a/submission/src/css/dropdown.css b/submission/src/css/dropdown.css deleted file mode 100644 index ee5dae7..0000000 --- a/submission/src/css/dropdown.css +++ /dev/null @@ -1,74 +0,0 @@ - -.dropdown-btn:focus, -.dropdown-btn:focus:hover, -.dropdown-btn.selected:focus { - border-color: #b5b5b5; -} - -.dropdown-btn:focus, -.dropdown-btn:focus:hover { - box-shadow: none; -} - -.dropdown-btn.selected, .dropdown-btn.selected:focus { - background-color: #dcdcdc; - box-shadow: inset 0 2px 4px rgba(0,0,0,0.15); -} - -.dropdown-btn::after { - display: inline-block; - width: 0; - height: 0; - content: ""; - vertical-align: -2px; - margin-left: 5px; - border: 4px solid; - border-right-color: transparent; - border-left-color: transparent; - border-bottom-color: transparent; -} - -.dropdown-menu { - width: 180px; - position: absolute; - right: 10px; - top: 45px; - padding-top: 5px; - padding-bottom: 5px; - background-clip: padding-box; - box-shadow: 0 3px 12px rgba(0,0,0,0.15); -} - -.dropdown-menu::before { - position: absolute; - display: inline-block; - content: ""; - border: 8px solid transparent; - border-bottom-color: rgba(0,0,0,0.15); - top: -16px; - left: auto; - right: 9px; -} - -.dropdown-menu::after { - position: absolute; - display: inline-block; - content: ""; - border: 7px solid transparent; - border-bottom-color: #fff; - top: -14px; - left: auto; - right: 10px; -} - -.dropdown-item { - padding: 4px 10px 4px 15px; - color: #333; -} - -.dropdown-item:hover { - color: #fff; - text-decoration: none; - text-shadow: none; - background-color: #4078c0; -} diff --git a/submission/src/css/submission.css b/submission/src/css/submission.css deleted file mode 100644 index 2ac10d5..0000000 --- a/submission/src/css/submission.css +++ /dev/null @@ -1,67 +0,0 @@ - -#main { - position: relative; - margin-top: 50px; -} - -#manual textarea { - width: 100%; - font-family: monospace; -} - -#doneerror { - float: right; -} - -.form.form-inline { - display: inline-block; - margin-right: 10px; -} - -.form.form-inline:last-child { - margin-right: 0; -} - -dl.form > dd input[type="text"] { - width: 100%; -} - -h2 { - margin-top: 5px; - margin-bottom: 30px; -} - -.avatar { - width: 20px; - margin-right: 5px; -} - -table { - width: 100%; - margin: 15px 0; -} - -th, -td { - text-align: left; - padding: 6px 15px; - border-bottom: 1px solid #E1E1E1; -} - -tr:last-child td { - border-bottom: none; -} - -td input { - width: 100%; -} - -th:first-child, -td:first-child { - padding-left: 0; -} - -th:last-child, -td:last-child { - padding-right: 0; -} diff --git a/submission/src/dropdown.js b/submission/src/dropdown.js deleted file mode 100644 index 3d227db..0000000 --- a/submission/src/dropdown.js +++ /dev/null @@ -1,39 +0,0 @@ - -/** - * Create dropdown with button - * - */ -export default class Dropdown { - constructor (button) { - this.closed = true - this.button = button - this.button.classList.add('dropdown-btn') - this.button.addEventListener('click', this.open, true) - } - - open = e => { - e.preventDefault() - - if (this.closed) { - this.closed = false - this.button.nextElementSibling.style.display = 'block' - this.button.classList.add('selected') - - window.setTimeout(() => { - window.document.addEventListener('click', this.close) - }, 50) - } else { - this.close() - } - } - - close = e => { - e.preventDefault() - - window.document.removeEventListener('click', this.close) - - this.button.classList.remove('selected') - this.button.nextElementSibling.style.display = 'none' - this.closed = true - } -} diff --git a/submission/src/github.js b/submission/src/github.js deleted file mode 100644 index 0890ff9..0000000 --- a/submission/src/github.js +++ /dev/null @@ -1,172 +0,0 @@ - -import nanoajax from 'nanoajax' - -const API_BASE = 'https://api.github.com' - -/* - * Github is restrictive on cookie usage on Github Pages. Use localStorage - * to store the OAuth token. - */ -let token = window.localStorage.getItem('token') - - -export const getToken = () => token - -export const clearToken = () => { - window.localStorage.removeItem('token') -} - -/** - * Get access token from Github OAuth code. - * - * @param {string} code OAuth code from Github API - */ -export const accessToken = (code, cb) => { - nanoajax.ajax({ - url: 'http://github-gatekeeper.aws.gaiagps.com/authenticate/' + code - }, (code, response) => { - token = JSON.parse(response).token - window.localStorage.setItem('token', token) - - cb(token) - }) -} - -/** - * AJAX call with Github Authorization header. - * - * @param {object} options nanoajax options - * @param {function} cb callback(err, data) - */ -export const ajax = (options, cb) => { - options.headers = {'Authorization': 'token ' + token} - - nanoajax.ajax(options, (code, response) => { - let parsed - - try { - parsed = JSON.parse(response) - } catch (e) { - return cb(e) - } - - return cb(null, parsed) - }) -} - -/** - * Get user. - * - * @param {function} cb callback(err, data) - */ -export const getUser = cb => { - ajax({ url: API_BASE + '/user' }, cb) -} - -/** - * Get repo if exists. - * - * @param {string} repo repo to check. - * @param {function} cb callback(err, data) - */ -export const getRepo = (repo, cb) => { - ajax({ url: API_BASE + '/repos/' + repo }, (err, response) => { - if (err) return cb(err) - - if (response.message && response.message === 'Not Found') { - return cb(null, null) - } - - cb(null, response) - }) -} - -/** - * Get latest commit SHA on master branch. - * - * @param {string} repo repo to get commit from. - * @param {function} cb callback(err, data) - */ -export const getHead = (repo, cb) => { - ajax({ url: API_BASE + '/repos/' + repo + '/git/refs/heads/master' }, (err, response) => { - if (err) return cb(err) - - if (response.message && response.message === 'Git Repository is empty.') { - return cb(null, null) - } - - cb(null, response.object.sha) - }) -} - -/** - * Fork repo. - * - * @param {string} repo repo to fork, ie. 'OpenBounds/OpenHuntingData' - * @param {function} cb callback(err, data) - */ -export const forkRepo = (repo, cb) => { - ajax({ - url: API_BASE + '/repos/' + repo + '/forks', - method: 'POST' - }, cb) -} - -/** - * Create branch in repo. - * - * @param {string} repo repo to create the branch in. - * @param {string} branch branch name. - * @param {string} sha SHA1 to set the branch to. - * @param {function} cb callback(err, data) - */ -export const branchRepo = (repo, branch, sha, cb) => { - ajax({ - url: API_BASE + '/repos/' + repo + '/git/refs', - body: JSON.stringify({ - ref: 'refs/heads/' + branch, - sha: sha - }) - }, cb) -} - -/** - * Create file in repo. - * - * @param {string} repo repo to create the file in. - * @param {string} branch branch to create the file in. - * @param {string} path file path. - * @param {base64} content base64 encoded file content. - * @param {string} message commit message. - * @param {function} cb callback(err, data) - */ -export const createFile = (repo, branch, path, content, message, cb) => { - ajax({ - url: API_BASE + '/repos/' + repo + '/contents/' + path, - method: 'PUT', - body: JSON.stringify({ - message: message, - content: content, - branch: branch - }) - }, cb) -} - -/** - * Create a pull request - * - * @param {string} repo repo to create the pull request in. - * @param {string} head branch to pull request, ie. user:add-source - * @param {string} message pull request title. - * @param {function} cb callback(err, data) - */ -export const pullRequest = (repo, head, message, cb) => { - ajax({ - url: API_BASE + '/repos/' + repo + '/pulls', - body: JSON.stringify({ - title: message, - head: head, - base: 'master' - }) - }, cb) -} diff --git a/submission/src/index.js b/submission/src/index.js deleted file mode 100644 index e9d9f31..0000000 --- a/submission/src/index.js +++ /dev/null @@ -1,229 +0,0 @@ - -import * as github from './github' -import * as utils from './utils' -import Dropdown from './dropdown' - -const BASE_REPO = 'OpenBounds/OpenHuntingData' -const REPO_NAME = 'OpenHuntingData' -const TIMEOUT_SECS = 15 - - -let params = utils.getParams() - , form = window.document.forms['submission'] - , pr = window.document.getElementById('pr') - , alert = window.document.getElementById('alert') - , manual = window.document.getElementById('manual') - - -/** - * Sign in user, when loading the page or after authentication. - * - * @param {object} user Github user object. - */ -const signinUser = (err, user) => { - let button = window.document.getElementById('signin') - , signout = window.document.getElementById('signout') - , blank = window.document.getElementById('unauthenticated') - - button.setAttribute('href', '#') - button.innerHTML = ` ${user.login}` - - blank.style.display = 'none' - form.style.display = 'block' - - signout.addEventListener('click', signoutUser) - - new Dropdown(button) -} - -const signoutUser = () => { - github.clearToken() - - window.location.href = window.location.pathname -} - -/* - * Handle UI changes for start, done and error submitting events. - */ -const startSubmitting = () => { - pr.setAttribute('disabled', 'disabled') - pr.textContent = 'Submitting...' - -} - -const doneSubmitting = () => { - pr.removeAttribute('disabled') - pr.textContent = 'Submit Pull Request' -} - -const errorSubmitting = (msg, content) => { - alert.innerHTML = msg - manual.getElementsByTagName('textarea')[0].textContent = content - - alert.style.display = 'block' - manual.style.display = 'block' -} - -const doneError = () => { - alert.innerHTML = '' - manual.getElementsByTagName('textarea')[0].textContent = '' - - alert.style.display = 'none' - manual.style.display = 'none' - - doneSubmitting() -} - -/** - * Create a pull request to add a source file. - * - * Get the head sha of the master branch. Create a feature branch at that sha - * named after the file being submitted. In the branch, create the source file - * with Base64 encoded JSON pretty-printed content. Then submit a pull request - * of the feature branch to the base repo. - * - * @param {string} username Github user's username. - * @param {string} repo Repo to create the file in, ie. user/OpenHuntingData - * @param {object} source Source object. - */ -const addSource = (username, repo, source) => { - let filename = source.species.join('-').replace(/[\s]/g, '').toLowerCase() - , path = `sources/${source.country}/${source.state}/${filename}.json` - , branch = `add-${source.country}-${source.state}-${filename}` - , msg = `add ${source.country}/${source.state}/${filename}.json` - , errMsg = `Error submitting pull request. Create the file ${path} with the JSON below.` - , raw = JSON.stringify(source, null, 3) - , content = window.btoa(raw) - - github.getHead(repo, (err, sha) => { - if (err) return errorSubmitting(errMsg, raw) - - github.branchRepo(repo, branch, sha, (err) => { - if (err) return errorSubmitting(errMsg, raw) - - github.createFile(repo, branch, path, content, msg, (err) => { - if (err) return errorSubmitting(errMsg, raw) - - github.pullRequest(BASE_REPO, username + ':' + branch, msg, (err) => { - if (err) return errorSubmitting(errMsg, raw) - - doneSubmitting() - }) - }) - }) - }) -} - -/* - * Submit source form to Github pull request. - * - * Create a source object from the source form. Get the authenticated user and - * username from Github, then check if the user has already forked the repo. - * - * If the repo is found, add the source to the repo. Otherwise, create a fork - * of the repo, and wait until it becomes available (async call). If fork - * does not become available within TIMEOUT_SEC, fail. - */ -const submit = e => { - let source - , filename - , path - , errMsg - , raw - - e.preventDefault() - - if (!github.getToken()) return - - startSubmitting() - - source = { - url: form.url.value, - species: form.species.value.split(', '), - attribution: form.attribution.value, - properties: {}, - country: form.country.value, - state: form.state.value, - filetype: form.filetype.value - } - - for (let property of ['id', 'name']) { - source.properties[property] = form[property].value - } - - filename = source.species.join('-').replace(/[\s]/g, '').toLowerCase() - path = `sources/${source.country}/${source.state}/${filename}.json` - errMsg = `Error submitting pull request. Create the file ${path} with the JSON below.` - raw = JSON.stringify(source, null, 3) - - github.getUser((err, user) => { - if (err) return errorSubmitting(errMsg, raw) - - let username = user.login - , repo = username + '/' + REPO_NAME - - github.getRepo(repo, (err, response) => { - if (err) return errorSubmitting(errMsg, raw) - - if (response) { - addSource(username, repo, source) - } else { - github.forkRepo(BASE_REPO, (err) => { - if (err) return errorSubmitting(errMsg, raw) - - github.getRepo(repo, (err) => { - if (err) return errorSubmitting(errMsg, raw) - - let count = 0 - , ping = window.setInterval(() => { - github.getHead(repo, (err, sha) => { - if (sha) { - window.clearInterval(ping) - addSource(username, repo, source) - } else { - count += 1 - - if (count > TIMEOUT_SECS * 2) { - window.clearInterval(ping) - - errorSubmitting(errMsg, raw) - } - } - }) - }, 500) - }) - }) - } - }) - }) -} - -/* - * Handle user authentication and OAuth response. If the user token is present - * when the page loads, retrieve the user object from Github and update the - * UI. Otherwise, if the URL parameter `code` is set (a resposne from Github's - * OAuth API), exchange it for a token with Gatekeeper. - * - * Replace the window history state to prevent multiple tokens being created, - * then update the UI. - */ -if (github.getToken()) { - github.getUser(signinUser) -} else { - if (params.code) { - github.accessToken(params.code, () => { - window.history.replaceState({}, window.document.title, window.location.pathname) - github.getUser(signinUser) - }) - } -} - -/* - * Listen for the form submit event, and submit a pull request of the new source. - */ -form.addEventListener('submit', submit, false) - -/* - * Clear error message when done button is clicked. - */ -window.document.getElementById('doneerror').addEventListener('click', doneError, false) diff --git a/submission/src/utils.js b/submission/src/utils.js deleted file mode 100644 index 633aa03..0000000 --- a/submission/src/utils.js +++ /dev/null @@ -1,19 +0,0 @@ - -/** - * Get params from URL. - * - * Modified from http://stackoverflow.com/a/979996/1377021 - */ -export const getParams = () => { - let params = {} - - for (let param of window.location.search.substring(1).split('&')) { - let nv = param.split('=') - - if (!nv[0]) continue; - - params[nv[0]] = nv[1] || true - } - - return params -} diff --git a/submission/webpack.config.js b/submission/webpack.config.js deleted file mode 100644 index 8494ecb..0000000 --- a/submission/webpack.config.js +++ /dev/null @@ -1,17 +0,0 @@ - -webpack = require('webpack') - -module.exports = { - entry: "./src/index.js", - module: { - loaders: [{ - test: /\.js$/, - exclude: /node_modules/, - loader: "babel-loader" - }] - }, - output: { - path: './dist/', - filename: 'submission.js' - } -};