diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 7ff4f74..ce7ec33 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -23,7 +23,7 @@ rules: no-else-return: 2 no-inner-declarations: 2 no-lonely-if: 2 - no-magic-numbers: [2, {ignore: [-1, 0, 1]}] + # no-magic-numbers: [2, {ignore: [-1, 0, 1]}] no-shadow: 2 no-unneeded-ternary: 2 no-unused-expressions: 2 diff --git a/.gitignore b/.gitignore index b7e8526..93cda27 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,10 @@ -*.pyc -*.pyo .DS_Store .git* .hg* .idea +.vscode +*.pyc +*.pyo bower_components main/index.yaml main/lib @@ -12,4 +13,7 @@ main/static/dev main/static/ext main/static/min node_modules +npm-debug.log +package-lock.json temp/ +yarn-error.log diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..e69de29 diff --git a/.prettierrc.yaml b/.prettierrc.yaml index 5c65149..a4389b9 100644 --- a/.prettierrc.yaml +++ b/.prettierrc.yaml @@ -10,6 +10,6 @@ trailingComma: all useTabs: false overrides: -- files: "*.json" - options: - printWidth: 200 + - files: '*.json' + options: + printWidth: 200 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..44c69f1 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,49 @@ +language: node_js + +node_js: + - '8' + +python: + - '2.7' + +sudo: required + +cache: + pip: true + yarn: true + directories: + - '$HOME/google-cloud-sdk/' + - node_modules + +notifications: + - email: false + +env: + # Make sure gcloud command is on our PATH and the App Engine SDK is in the Python path + - PATH=$PATH:${HOME}/google-cloud-sdk/bin CLOUDSDK_CORE_DISABLE_PROMPTS=1 + +before_install: + - python -V + # Travis defaults to python 2.7.6 we need at least 2.7.9 + # askubuntu suggests using a PPA repository + - sudo add-apt-repository -y ppa:jonathonf/python-2.7 + - sudo apt-get update + - sudo apt-get install -y python2.7 + # Check if gcloud doesn't exist or if there is gcloud update, in which case delete the current installation + - if ! command -v gcloud || gcloud version 2>&1 | grep -F update; then rm -rf $HOME/google-cloud-sdk; sudo rm -rf /usr/lib/google-cloud-sdk; else echo "Keeping cached Google Cloud SDK"; fi + # Check if Google Cloud SDK is installed and if not remove remnants and install it + - if [ ! -d "$HOME/google-cloud-sdk/" ] ; then sudo rm -rf /usr/lib/google-cloud-sdk; export CLOUDSDK_CORE_DISABLE_PROMPTS=1; curl https://sdk.cloud.google.com | bash; fi + # Check if Google Cloud SDK is installed *properly* and if not remove remnants and install it + - if [ ! -f "$HOME/google-cloud-sdk/path.bash.inc" ]; then ls -al $HOME/google-cloud-sdk; rm -rf $HOME/google-cloud-sdk; sudo rm -rf /usr/lib/google-cloud-sdk; export CLOUDSDK_CORE_DISABLE_PROMPTS=1; curl https://sdk.cloud.google.com | bash; fi + # Add gcloud to $PATH or give some diagnostic info + - source $HOME/google-cloud-sdk/path.bash.inc || ls -al $HOME/google-cloud-sdk + - gcloud version + - gcloud components install app-engine-python + - python -V + +before_script: + - yarn global add gulp-cli + +script: + - yarn test + - gulp deploy --dryrun diff --git a/README.md b/README.md index d74e8d5..875f4e8 100644 --- a/README.md +++ b/README.md @@ -10,17 +10,17 @@ The latest version is always accessible from [http://upload.gae-init.appspot.com ## Requirements -* [Google App Engine SDK for Python][] -* [Node.js][], [pip][], [virtualenv][] -* [macOS][] or [Linux][] or [Windows][] +- [Google App Engine SDK for Python][] +- [Node.js][], [Yarn][], [pip][], [virtualenv][] +- [macOS][] or [Linux][] or [Windows][] Make sure you have all of the above or refer to the docs on how to [install the requirements](http://docs.gae-init.appspot.com/requirement/). ## Running the Development Environment ```bash -$ cd /path/to/project-name -$ gulp +cd /path/to/project-name +gulp ``` To test it visit `http://localhost:3000` in your browser. @@ -30,23 +30,23 @@ To test it visit `http://localhost:3000` in your browser. For a complete list of commands: ```bash -$ gulp help +gulp help ``` ## Initializing or Resetting the project ```bash -$ cd /path/to/project-name -$ npm install -$ gulp +cd /path/to/project-name +yarn +gulp ``` If something goes wrong you can always do: ```bash -$ gulp reset -$ npm install -$ gulp +gulp reset +yarn +gulp ``` --- @@ -54,32 +54,31 @@ $ gulp To install [Gulp][] as a global package: ```bash -$ npm install -g gulp +yarn global add gulp-cli ``` ## Deploying on Google App Engine ```bash -$ gulp deploy -$ gulp deploy --project=foo -$ gulp deploy --project=foo --version=bar -$ gulp deploy --project=foo --version=bar --no-promote +gulp deploy +gulp deploy --project=foo +gulp deploy --project=foo --version=bar +gulp deploy --project=foo --version=bar --no-promote ``` ## Tech Stack -* [Google App Engine][], [NDB][] -* [Jinja2][], [Flask][], [Flask-RESTful][], [Flask-WTF][] -* [CoffeeScript][], [Less][] -* [Bootstrap][], [Font Awesome][], [Social Buttons][] -* [jQuery][], [Moment.js][] -* [OpenID][] sign in (Google, Facebook, Twitter and more) -* [Python 2.7][], [pip][], [virtualenv][] -* [Gulp][], [Bower][] +- [Google App Engine][], [NDB][] +- [Jinja2][], [Flask][], [Flask-RESTful][], [Flask-WTF][] +- [Less][] +- [Bootstrap][], [Font Awesome][], [Social Buttons][] +- [jQuery][], [Moment.js][] +- [OpenID][] sign in (Google, Facebook, Twitter and more) +- [Python 2.7][], [pip][], [virtualenv][] +- [Gulp][], [Bower][] [bootstrap]: http://getbootstrap.com/ [bower]: http://bower.io/ -[coffeescript]: http://coffeescript.org/ [documentation]: http://docs.gae-init.appspot.com [feature list]: http://docs.gae-init.appspot.com/features/ [flask-restful]: https://flask-restful.readthedocs.org @@ -105,3 +104,4 @@ $ gulp deploy --project=foo --version=bar --no-promote [tutorial]: http://docs.gae-init.appspot.com/tutorial/ [virtualenv]: http://www.virtualenv.org/ [windows]: http://windows.microsoft.com/ +[yarn]: https://yarnpkg.com/ diff --git a/bin/requirements_linux.sh b/bin/requirements_linux.sh index f371bff..85b563a 100755 --- a/bin/requirements_linux.sh +++ b/bin/requirements_linux.sh @@ -8,7 +8,7 @@ gcloud components install app-engine-python sudo apt-get install nodejs # Gulp.js -sudo npm install -g gulp +npm install --global gulp-cli # Python related curl https://bootstrap.pypa.io/get-pip.py | sudo python diff --git a/bin/requirements_osx_brew.sh b/bin/requirements_osx_brew.sh index 2458f35..98562d7 100755 --- a/bin/requirements_osx_brew.sh +++ b/bin/requirements_osx_brew.sh @@ -8,7 +8,7 @@ gcloud components install app-engine-python brew install node # Gulp.js -npm install -g gulp +npm install --global gulp-cli # Python related curl https://bootstrap.pypa.io/get-pip.py | python diff --git a/bin/requirements_osx_port.sh b/bin/requirements_osx_port.sh index ff5abe9..504ce1d 100755 --- a/bin/requirements_osx_port.sh +++ b/bin/requirements_osx_port.sh @@ -8,7 +8,7 @@ gcloud components install app-engine-python sudo port install nodejs # Gulp.js -npm install -g gulp +npm install --global gulp-cli # Python related curl https://bootstrap.pypa.io/get-pip.py | python diff --git a/bin/test_hello_world.sh b/bin/test_hello_world.sh index 6fcda5c..d6fa040 100755 --- a/bin/test_hello_world.sh +++ b/bin/test_hello_world.sh @@ -4,5 +4,5 @@ cd ~ git clone https://github.com/gae-init/gae-init.git hello-world cd hello-world -npm install +yarn gulp diff --git a/bower.json b/bower.json index 18c7336..104ced4 100644 --- a/bower.json +++ b/bower.json @@ -3,7 +3,7 @@ "bootstrap": "3.3.7", "font-awesome": "4.7.0", "jquery": "3.3.1", - "moment": "2.20.1" + "moment": "2.22.2" }, "name": "gae-init", "overrides": { diff --git a/gulp/tasks/build.coffee b/gulp/tasks/build.coffee index df340d9..1f8bb05 100644 --- a/gulp/tasks/build.coffee +++ b/gulp/tasks/build.coffee @@ -16,21 +16,28 @@ gulp.task 'rebuild', $.sequence 'reset', 'build' -gulp.task 'deploy', 'Deploy project to Google App Engine.', ['build'], -> - options = yargs process.argv, configuration: - 'boolean-negation': false - 'camel-case-expansion': false - delete options['_'] - options_str = '' - for k of options - if options[k] == true - options[k] = '' - options_str += " #{if k.length > 1 then '-' else ''}-#{k} #{options[k]}" +gulp.task 'deploy', + 'Deploy project to Google App Engine. Available options:', ['build'], -> + options = yargs process.argv, configuration: + 'boolean-negation': false + 'camel-case-expansion': false + delete options['_'] + options_str = '' + dryrun = '' + for k of options + if k == 'dryrun' + dryrun = 'echo DRYRUN - would run: ' + else + if options[k] == true + options[k] = '' + options_str += " #{if k.length > 1 then '-' else ''}-#{k} #{options[k]}" - gulp.src('run.py').pipe $.start [{ - match: /run.py$/ - cmd: "gcloud app deploy main/*.yaml#{options_str}" - }] + gulp.src('run.py').pipe $.start [{ + match: /run.py$/ + cmd: "#{dryrun}gcloud app deploy main/*.yaml#{options_str}" + }] + , options: + 'dryrun': 'run all preparations but do not actually deploy' gulp.task 'run', diff --git a/gulp/tasks/clean.coffee b/gulp/tasks/clean.coffee index a2ab06a..bd52288 100644 --- a/gulp/tasks/clean.coffee +++ b/gulp/tasks/clean.coffee @@ -29,7 +29,7 @@ gulp.task 'clean:venv', false, -> gulp.task 'reset', - 'Complete reset of project. Run "npm install" after this procedure.', + 'Complete reset of project. Run "yarn install" after this procedure.', ['clean', 'clean:dev', 'clean:min', 'clean:venv'], -> del paths.dep.bower_components del paths.dep.node_modules diff --git a/gulp/tasks/dep.coffee b/gulp/tasks/dep.coffee index 4f3f0ab..849af4f 100644 --- a/gulp/tasks/dep.coffee +++ b/gulp/tasks/dep.coffee @@ -1,14 +1,15 @@ fs = require 'fs' gulp = require('gulp-help') require 'gulp' +yarn = require('gulp-yarn') main_bower_files = require 'main-bower-files' $ = require('gulp-load-plugins')() paths = require '../paths' -gulp.task 'npm', false, -> - gulp.src 'package.json' +gulp.task 'yarn', false, -> + gulp.src ['./package.json', './yarn.lock'] .pipe $.plumber() - .pipe $.start() + .pipe yarn() gulp.task 'bower', false, -> diff --git a/gulp/tasks/script.coffee b/gulp/tasks/script.coffee index e3f4c40..bcc1662 100644 --- a/gulp/tasks/script.coffee +++ b/gulp/tasks/script.coffee @@ -15,7 +15,7 @@ gulp.task 'script', false, -> .pipe $.plumber errorHandler: util.onError .pipe $.if is_coffee, $.coffee() .pipe $.concat 'script.js' - .pipe $.babel presets: ['es2015'] + .pipe $.babel presets: ['@babel/env'] .pipe uglify() .pipe $.size {title: 'Minified scripts'} .pipe gulp.dest "#{paths.static.min}/script" diff --git a/gulp/tasks/watch.coffee b/gulp/tasks/watch.coffee index c481ea8..cbfd608 100644 --- a/gulp/tasks/watch.coffee +++ b/gulp/tasks/watch.coffee @@ -24,7 +24,7 @@ gulp.task 'watch', false, -> $.watch 'requirements.txt', -> $.sequence('pip')() $.watch 'package.json', -> - $.sequence('npm')() + $.sequence('yarn')() $.watch 'bower.json', -> $.sequence('ext_watch_rebuild')() $.watch 'gulp/config.coffee', -> diff --git a/magic.py b/magic.py index fca890a..e6ef8a3 100755 --- a/magic.py +++ b/magic.py @@ -13,7 +13,7 @@ ############################################################################### # Options ############################################################################### -MAGIC_URL = 'http://magic.gae-init.appspot.com' +MAGIC_URL = 'https://magic-dot-gae-init.appspot.com' PARSER = argparse.ArgumentParser(description='Visit %s for more.' % MAGIC_URL) PARSER.add_argument( @@ -22,7 +22,7 @@ ) PARSER.add_argument( '-r', '--remote', dest='remote_url', action='store', default=MAGIC_URL, - help="set the remote URL if it's not http://magic.gae-init.appspot.com", + help="set the remote URL if it's not https://magic-dot-gae-init.appspot.com", ) ARGS = PARSER.parse_args() diff --git a/main/app.yaml b/main/app.yaml index 6186169..b73b446 100644 --- a/main/app.yaml +++ b/main/app.yaml @@ -5,72 +5,72 @@ api_version: 1 threadsafe: true builtins: -- appstats: on -- deferred: on -- remote_api: on + - appstats: on + - deferred: on + - remote_api: on inbound_services: -- warmup + - warmup libraries: -- name: jinja2 - version: latest -- name: PIL - version: latest + - name: PIL + version: latest + - name: ssl + version: latest error_handlers: -- file: templates/error_static.html + - file: templates/error_static.html handlers: -- url: /favicon.ico - static_files: static/img/favicon.ico - upload: static/img/favicon.ico + - url: /favicon.ico + static_files: static/img/favicon.ico + upload: static/img/favicon.ico -- url: /robots.txt - static_files: static/robots.txt - upload: static/robots.txt + - url: /robots.txt + static_files: static/robots.txt + upload: static/robots.txt -- url: /p/(.*\.ttf) - static_files: static/\1 - upload: static/(.*\.ttf) - mime_type: font/ttf - expiration: "365d" + - url: /p/(.*\.ttf) + static_files: static/\1 + upload: static/(.*\.ttf) + mime_type: font/ttf + expiration: '365d' -- url: /p/(.*\.woff2) - static_files: static/\1 - upload: static/(.*\.woff2) - mime_type: font/woff2 - expiration: "365d" + - url: /p/(.*\.woff2) + static_files: static/\1 + upload: static/(.*\.woff2) + mime_type: font/woff2 + expiration: '365d' -- url: /p/ - static_dir: static/ - expiration: "365d" + - url: /p/ + static_dir: static/ + expiration: '365d' -- url: /serve/.* - script: control.serve.app + - url: /serve/.* + script: control.serve.app -- url: /.* - script: main.app - secure: always - redirect_http_response_code: 301 + - url: /.* + script: main.app + secure: always + redirect_http_response_code: 301 skip_files: -- ^(.*/)?#.*# -- ^(.*/)?.*/RCS/.* -- ^(.*/)?.*\.bak$ -- ^(.*/)?.*\.py[co] -- ^(.*/)?.*~ -- ^(.*/)?Icon\r -- ^(.*/)?\..* -- ^(.*/)?app\.yaml -- ^(.*/)?app\.yml -- ^(.*/)?index\.yaml -- ^(.*/)?index\.yml -- ^lib/.* -- ^static/dev/.* -- ^static/ext/.*\.coffee -- ^static/ext/.*\.css -- ^static/ext/.*\.js -- ^static/ext/.*\.less -- ^static/ext/.*\.json -- ^static/src/.* + - ^(.*/)?#.*# + - ^(.*/)?.*/RCS/.* + - ^(.*/)?.*\.bak$ + - ^(.*/)?.*\.py[co] + - ^(.*/)?.*~ + - ^(.*/)?Icon\r + - ^(.*/)?\..* + - ^(.*/)?app\.yaml + - ^(.*/)?app\.yml + - ^(.*/)?index\.yaml + - ^(.*/)?index\.yml + - ^lib/.* + - ^static/dev/.* + - ^static/ext/.*\.coffee + - ^static/ext/.*\.css + - ^static/ext/.*\.js + - ^static/ext/.*\.less + - ^static/ext/.*\.json + - ^static/src/.* diff --git a/main/control/error.py b/main/control/error.py index fc5b3ca..87762bc 100644 --- a/main/control/error.py +++ b/main/control/error.py @@ -3,6 +3,7 @@ import logging import flask +import werkzeug from api import helpers import config @@ -44,4 +45,6 @@ def error_handler(e): if config.PRODUCTION: @app.errorhandler(Exception) def production_error_handler(e): + if isinstance(e, werkzeug.exceptions.HTTPException) and e.code in (301, 302): + return e return error_handler(e) diff --git a/main/model/resource.py b/main/model/resource.py index 8551d86..ca1bbd0 100644 --- a/main/model/resource.py +++ b/main/model/resource.py @@ -20,8 +20,8 @@ class Resource(model.Base): size = ndb.IntegerProperty(default=0) @ndb.ComputedProperty - def size_human(self): - return util.size_human(self.size or 0) + def sizeHuman(self): + return util.sizeHuman(self.size or 0) @property def download_url(self): @@ -51,7 +51,7 @@ def serve_url(self): 'name': fields.String, 'serve_url': fields.String, 'size': fields.Integer, - 'size_human': fields.String, + 'sizeHuman': fields.String, 'view_url': fields.String, } diff --git a/main/static/src/script/common/api.coffee b/main/static/src/script/common/api.coffee deleted file mode 100644 index 98c60b4..0000000 --- a/main/static/src/script/common/api.coffee +++ /dev/null @@ -1,39 +0,0 @@ -window.api_call = (method, url, params, data, callback) -> - callback = callback || data || params - data = data || params - if arguments.length == 4 - data = undefined - if arguments.length == 3 - params = undefined - data = undefined - params = params || {} - for k, v of params - delete params[k] if not v? - separator = if url.search('\\?') >= 0 then '&' else '?' - $.ajax - type: method - url: "#{url}#{separator}#{$.param params}" - contentType: 'application/json' - accepts: 'application/json' - dataType: 'json' - data: if data then JSON.stringify(data) else undefined - success: (data) -> - if data.status == 'success' - more = undefined - if data.next_url - more = (callback) -> api_call(method, data.next_url, {}, callback) - callback? undefined, data.result, more - else - callback? data - error: (jqXHR, textStatus, errorThrown) -> - error = - error_code: 'ajax_error' - text_status: textStatus - error_thrown: errorThrown - jqXHR: jqXHR - try - error = $.parseJSON(jqXHR.responseText) if jqXHR.responseText - catch e - error = error - LOG 'api_call error', error - callback? error diff --git a/main/static/src/script/common/api.js b/main/static/src/script/common/api.js new file mode 100644 index 0000000..cca0a42 --- /dev/null +++ b/main/static/src/script/common/api.js @@ -0,0 +1,55 @@ +window.apiCall = function(method, url, params, data, callback) { + callback = callback || data || params; + data = data || params; + if (arguments.length === 4) { + data = void 0; + } + if (arguments.length === 3) { + params = void 0; + data = void 0; + } + params = params || {}; + for (const key in params) { + if (params[key] == null) { + delete params[key]; + } + } + const separator = url.search('\\?') >= 0 ? '&' : '?'; + $.ajax({ + accepts: 'application/json', + contentType: 'application/json', + data: data ? JSON.stringify(data) : void 0, + dataType: 'json', + error(jqXHR, textStatus, errorThrown) { + let error = { + error_code: 'ajax_error', + error_thrown: errorThrown, + jqXHR, + text_status: textStatus, + }; + try { + if (jqXHR.responseText) { + error = $.parseJSON(jqXHR.responseText); + } + } catch (_error) { + error = _error; + } + LOG('apiCall error', error); + return typeof callback === 'function' ? callback(error) : void 0; + }, + success(data_) { + if (data_.status === 'success') { + let more = void 0; + if (data_.next_url) { + more = callback_ => apiCall(method, data_.next_url, {}, callback); + } + return typeof callback === 'function' + ? callback(void 0, data_.result, more) + : void 0; + } + return typeof callback === 'function' ? callback(data) : void 0; + }, + type: method, + url: `${url}${separator}${$.param(params)}`, + }); +}; diff --git a/main/static/src/script/common/upload.coffee b/main/static/src/script/common/upload.coffee index 9b7bc4d..0a91f37 100644 --- a/main/static/src/script/common/upload.coffee +++ b/main/static/src/script/common/upload.coffee @@ -51,7 +51,7 @@ get_upload_urls: (n, callback) => return if n <= 0 - api_call 'GET', @upload_url, {count: n}, (error, result) -> + apiCall 'GET', @upload_url, {count: n}, (error, result) -> if error callback error throw error diff --git a/main/static/src/script/common/util.coffee b/main/static/src/script/common/util.coffee deleted file mode 100644 index 76e7a02..0000000 --- a/main/static/src/script/common/util.coffee +++ /dev/null @@ -1,89 +0,0 @@ -window.LOG = -> - console?.log? arguments... - - -window.init_common = -> - init_loading_button() - init_confirm_button() - init_password_show_button() - init_time() - init_announcement() - init_row_link() - - -window.init_loading_button = -> - $('body').on 'click', '.btn-loading', -> - $(this).button 'loading' - - -window.init_confirm_button = -> - $('body').on 'click', '.btn-confirm', -> - if not confirm $(this).data('message') or 'Are you sure?' - event.preventDefault() - - -window.init_password_show_button = -> - $('body').on 'click', '.btn-password-show', -> - $target = $($(this).data 'target') - $target.focus() - if $(this).hasClass 'active' - $target.attr 'type', 'password' - else - $target.attr 'type', 'text' - - -window.init_time = -> - if $('time').length > 0 - recalculate = -> - $('time[datetime]').each -> - date = moment.utc $(this).attr 'datetime' - diff = moment().diff date , 'days' - if diff > 25 - $(this).text date.local().format 'YYYY-MM-DD' - else - $(this).text date.fromNow() - $(this).attr 'title', date.local().format 'dddd, MMMM Do YYYY, HH:mm:ss Z' - setTimeout recalculate, 1000 * 45 - recalculate() - - -window.init_announcement = -> - $('.alert-announcement button.close').click -> - sessionStorage?.setItem 'closedAnnouncement', $('.alert-announcement').html() - - if sessionStorage?.getItem('closedAnnouncement') != $('.alert-announcement').html() - $('.alert-announcement').show() - - -window.init_row_link = -> - $('body').on 'click', '.row-link', -> - window.location.href = $(this).data 'href' - - $('body').on 'click', '.not-link', (e) -> - e.stopPropagation() - - -window.clear_notifications = -> - $('#notifications').empty() - - -window.show_notification = (message, category='warning') -> - clear_notifications() - return if not message - - $('#notifications').append """ -