From c2dfc1bb34e8153bc7372ce6056c6883616ece9b Mon Sep 17 00:00:00 2001 From: Joseph Date: Wed, 28 Feb 2018 23:33:35 +0000 Subject: Add error messages (#30) * Add error messages Signed-off-by: JoeBanks13 * Remove un-used keyword arg from 404 * Assert for status code instead of full content * PEP8 * test lint * please coverage * oh * Exclude websockets.py from coverage * Move code output into terminal * Switch typewriter href protocol * Add tests for websockets.py * Abort previous commit, coveralls did not let coverage go down * Add more pauses and request => response * move css and js out, add typewriter JS to our own repo & add method for appending text in bulk. * Enable REPL on 4XX and change error descriptions * commas * /error path --- .coveragerc | 2 +- app.py | 1 - app_test.py | 33 +- deploy.py | 1 - pysite/constants.py | 23 ++ pysite/database.py | 4 +- pysite/decorators.py | 1 - pysite/mixins.py | 4 +- pysite/route_manager.py | 1 - pysite/views/api/bot/user.py | 1 - pysite/views/error_handlers/http_404.py | 12 - pysite/views/error_handlers/http_4xx.py | 22 ++ pysite/views/error_handlers/http_5xx.py | 17 +- pysite/views/main/abort.py | 12 + pysite/views/main/error.py | 12 + pysite/views/tests/index.py | 1 - pysite/websockets.py | 1 - static/css/window.css | 262 ++++++++++++++ static/js/500.js | 36 ++ static/js/typewriter.js | 612 ++++++++++++++++++++++++++++++++ templates/errors/error.html | 59 +++ templates/main/base.html | 4 +- tox.ini | 2 + 23 files changed, 1089 insertions(+), 34 deletions(-) delete mode 100644 pysite/views/error_handlers/http_404.py create mode 100644 pysite/views/error_handlers/http_4xx.py create mode 100644 pysite/views/main/abort.py create mode 100644 pysite/views/main/error.py create mode 100644 static/css/window.css create mode 100644 static/js/500.js create mode 100644 static/js/typewriter.js create mode 100644 templates/errors/error.html diff --git a/.coveragerc b/.coveragerc index 58b972ee..89207693 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,2 +1,2 @@ [run] -omit = /usr/*, gunicorn_config.py, deploy.py, app_test.py, app.py, pysite/views/*__init__.py, pysite/route_manager.py +omit = /usr/*, gunicorn_config.py, deploy.py, app_test.py, app.py, pysite/websockets.py, pysite/views/*__init__.py, pysite/route_manager.py diff --git a/app.py b/app.py index f3bb9a60..ba1a5343 100644 --- a/app.py +++ b/app.py @@ -2,7 +2,6 @@ from pysite.route_manager import RouteManager - manager = RouteManager() app = manager.app diff --git a/app_test.py b/app_test.py index 35b36af4..2e8a53fb 100644 --- a/app_test.py +++ b/app_test.py @@ -1,12 +1,11 @@ import json import os -from app import manager - from flask import Blueprint - from flask_testing import TestCase +from app import manager + manager.app.tests_blueprint = Blueprint("tests", __name__) manager.load_views(manager.app.tests_blueprint, "pysite/views/tests") manager.app.register_blueprint(manager.app.tests_blueprint) @@ -44,6 +43,11 @@ class BaseEndpoints(SiteTest): response = self.client.get('/nonexistentpath') self.assertEqual(response.status_code, 404) + def test_error(self): + """ Check the /error/XYZ page """ + response = self.client.get('/error/418') + self.assertEqual(response.status_code, 418) + def test_invite(self): """ Check invite redirects """ response = self.client.get('/invite') @@ -59,6 +63,11 @@ class BaseEndpoints(SiteTest): response = self.client.get('/datadog') self.assertEqual(response.status_code, 302) + def test_500_easter_egg(self): + """Check the status of the /500 page""" + response = self.client.get("/500") + self.assertEqual(response.status_code, 500) + class ApiEndpoints(SiteTest): """ test cases for the api subdomain """ @@ -172,6 +181,20 @@ class Utilities(SiteTest): return True raise Exception('Expected runtime error on setup() when giving wrongful arguments') + def test_websocket_callback(self): + """ Check that websocket default callbacks work """ + import pysite.websockets + + class TestWS(pysite.websockets.WS): + pass + + try: + TestWS(None).on_message("test") + return False + except NotImplementedError: + return True + + class MixinTests(SiteTest): """ Test cases for mixins """ @@ -202,9 +225,9 @@ class MixinTests(SiteTest): from werkzeug.exceptions import InternalServerError from pysite.views.error_handlers import http_5xx - error_view = http_5xx.Error404View() + error_view = http_5xx.Error500View() error_message = error_view.get(InternalServerError) - self.assertEqual(error_message, ('Internal server error. Please try again later!', 500)) + self.assertEqual(error_message[1], 500) def test_route_view_runtime_error(self): """ Check that wrong values for route view setup raises runtime error """ diff --git a/deploy.py b/deploy.py index 189b5f0c..20d8edd5 100644 --- a/deploy.py +++ b/deploy.py @@ -2,7 +2,6 @@ import os import requests - branch = os.environ.get("TRAVIS_BRANCH") url = os.environ.get("AUTODEPLOY_WEBHOOK") token = os.environ.get("AUTODEPLOY_TOKEN") diff --git a/pysite/constants.py b/pysite/constants.py index 59febcc9..f70f48ad 100644 --- a/pysite/constants.py +++ b/pysite/constants.py @@ -20,3 +20,26 @@ OWNER_ROLE = 267627879762755584 ADMIN_ROLE = 267628507062992896 MODERATOR_ROLE = 267629731250176001 HELPER_ROLE = 267630620367257601 + +ERROR_DESCRIPTIONS = { + # 5XX + 500: "The server encountered an unexpected error ._.", + 501: "Woah! You seem to have found something we haven't even implemented yet!", + 502: "This is weird, one of our upstream servers seems to have experienced an error.", + 503: "Looks like one of our services is down for maintenance and couldn't respond to your request.", + 504: "Looks like an upstream server experienced a timeout while we tried to talk to it!", + 505: "You're using an old HTTP version. It might be time to upgrade your browser.", + # 4XX + 400: "You sent us a request that we don't know what to do with.", + 401: "Nope! You'll need to authenticate before we let you do that.", + 403: "No way! You're not allowed to do that.", + 404: "We looked, but we couldn't seem to find that page.", + 405: "That's a real page, but you can't use that method.", + 408: "We waited a really long time, but never got your request.", + 410: "This used to be here, but it's gone now.", + 411: "You forgot to tell us the length of the content.", + 413: "No way! That payload is, like, way too big!", + 415: "The thing you sent has the wrong format.", + 418: "Sorry, I'm not a server, I'm a teapot.", + 429: "Please don't send us that many requests." +} diff --git a/pysite/database.py b/pysite/database.py index 239a2fdc..78c4368a 100644 --- a/pysite/database.py +++ b/pysite/database.py @@ -1,12 +1,10 @@ # coding=utf-8 import logging import os - from typing import Any, Callable, Dict, Iterator, List, Optional, Union -from flask import abort - import rethinkdb +from flask import abort from rethinkdb.ast import RqlMethodQuery, Table, UserError from rethinkdb.net import DefaultConnection diff --git a/pysite/decorators.py b/pysite/decorators.py index 03d5e6b8..94239fbc 100644 --- a/pysite/decorators.py +++ b/pysite/decorators.py @@ -4,7 +4,6 @@ from functools import wraps from json import JSONDecodeError from flask import request - from schema import Schema, SchemaError from pysite.constants import ErrorCodes, ValidationTypes diff --git a/pysite/mixins.py b/pysite/mixins.py index 930a7eb7..059f871d 100644 --- a/pysite/mixins.py +++ b/pysite/mixins.py @@ -1,9 +1,7 @@ # coding=utf-8 -from _weakref import ref - from flask import Blueprint - from rethinkdb.ast import Table +from _weakref import ref from pysite.database import RethinkDB diff --git a/pysite/route_manager.py b/pysite/route_manager.py index 494dfbde..53b24def 100644 --- a/pysite/route_manager.py +++ b/pysite/route_manager.py @@ -5,7 +5,6 @@ import logging import os from flask import Blueprint, Flask - from flask_sockets import Sockets from pysite.base_route import APIView, BaseView, ErrorView, RouteView diff --git a/pysite/views/api/bot/user.py b/pysite/views/api/bot/user.py index f80bb826..c9686f56 100644 --- a/pysite/views/api/bot/user.py +++ b/pysite/views/api/bot/user.py @@ -1,7 +1,6 @@ # coding=utf-8 from flask import jsonify - from schema import Schema from pysite.base_route import APIView diff --git a/pysite/views/error_handlers/http_404.py b/pysite/views/error_handlers/http_404.py deleted file mode 100644 index 1d557d9b..00000000 --- a/pysite/views/error_handlers/http_404.py +++ /dev/null @@ -1,12 +0,0 @@ -# coding=utf-8 -from werkzeug.exceptions import NotFound - -from pysite.base_route import ErrorView - - -class Error404View(ErrorView): - name = "error_404" - error_code = 404 - - def get(self, error: NotFound): - return "replace me with a template, 404 not found", 404 diff --git a/pysite/views/error_handlers/http_4xx.py b/pysite/views/error_handlers/http_4xx.py new file mode 100644 index 00000000..1417c1f6 --- /dev/null +++ b/pysite/views/error_handlers/http_4xx.py @@ -0,0 +1,22 @@ +# coding=utf-8 +from flask import render_template, request +from werkzeug.exceptions import NotFound + +from pysite.base_route import ErrorView +from pysite.constants import ERROR_DESCRIPTIONS + + +class Error400View(ErrorView): + name = "error_4xx" + error_code = range(400, 430) + + def get(self, error: NotFound): + error_desc = ERROR_DESCRIPTIONS.get(error.code, "We're not really sure what happened there, please try again.") + + return render_template("errors/error.html", code=error.code, req=request, error_title=error_desc, + error_message=error_desc + + " If you believe we have made a mistake, " + "please open an issue " + "on our GitHub (" + "https://github.com" + "/discord-python/site/issues)."), error.code diff --git a/pysite/views/error_handlers/http_5xx.py b/pysite/views/error_handlers/http_5xx.py index ed4d8d82..ecf4a35e 100644 --- a/pysite/views/error_handlers/http_5xx.py +++ b/pysite/views/error_handlers/http_5xx.py @@ -1,12 +1,25 @@ # coding=utf-8 +from flask import render_template, request from werkzeug.exceptions import HTTPException from pysite.base_route import ErrorView +from pysite.constants import ERROR_DESCRIPTIONS -class Error404View(ErrorView): +class Error500View(ErrorView): name = "error_5xx" error_code = range(500, 600) def get(self, error: HTTPException): - return "Internal server error. Please try again later!", error.code + error_desc = ERROR_DESCRIPTIONS.get(error.code, "We're not really sure what happened there, please try again.") + + return render_template("errors/error.html", code=error.code, req=request, error_title=error_desc, + error_message="An error occured while " + "processing this " + "request, please try " + "again later. " + "If you believe we have made a mistake, " + "please open an issue " + "on our GitHub (" + "https://github.com" + "/discord-python/site/issues)."), error.code diff --git a/pysite/views/main/abort.py b/pysite/views/main/abort.py new file mode 100644 index 00000000..d9e3282f --- /dev/null +++ b/pysite/views/main/abort.py @@ -0,0 +1,12 @@ +# coding=utf-8 +from werkzeug.exceptions import InternalServerError + +from pysite.base_route import RouteView + + +class EasterEgg500(RouteView): + path = "/500" + name = "500" + + def get(self): + raise InternalServerError diff --git a/pysite/views/main/error.py b/pysite/views/main/error.py new file mode 100644 index 00000000..18c20c6e --- /dev/null +++ b/pysite/views/main/error.py @@ -0,0 +1,12 @@ +# coding=utf-8 +from flask import abort + +from pysite.base_route import RouteView + + +class ErrorView(RouteView): + path = "/error/" + name = "error" + + def get(self, code): + return abort(code) diff --git a/pysite/views/tests/index.py b/pysite/views/tests/index.py index 78b7ef2e..3071bf0e 100644 --- a/pysite/views/tests/index.py +++ b/pysite/views/tests/index.py @@ -1,7 +1,6 @@ # coding=utf-8 from flask import jsonify - from schema import Schema from pysite.base_route import RouteView diff --git a/pysite/websockets.py b/pysite/websockets.py index fa1fd0eb..1e7960f7 100644 --- a/pysite/websockets.py +++ b/pysite/websockets.py @@ -1,6 +1,5 @@ # coding=utf-8 from flask import Blueprint - from geventwebsocket.websocket import WebSocket diff --git a/static/css/window.css b/static/css/window.css new file mode 100644 index 00000000..3f3b7f56 --- /dev/null +++ b/static/css/window.css @@ -0,0 +1,262 @@ + .window { + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -moz-box-shadow: 0 4px 12px rgba(0, 0, 0, .5); + -webkit-box-shadow: 0 4px 12px rgba(0, 0, 0, .5); + box-shadow: 0 4px 12px rgba(0, 0, 0, .5); + width: 800px; + margin: auto; + margin-top: 20px; + border: 1px solid #C1C2C2; + height: 500px; + padding-bottom: 20px; + } + + .inside { + background: black; + padding-right: 20px; + height: 100%; + } + + .inside .blok { + width: 100%; + background: black; + } + + .top { + padding: 7px 0; + position: relative; + background: #f1f1f1; + background: -moz-linear-gradient(top, #E9E9E9 3%, #d8d8d8 100%); + background: -webkit-gradient(left top, left bottom, color-stop(3%, #E9E9E9), color-stop(100%, #d8d8d8)); + background: -webkit-linear-gradient(top, #E9E9E9 3%, #d8d8d8 100%); + background: -o-linear-gradient(top, #E9E9E9 3%, #d8d8d8 100%); + background: -ms-linear-gradient(top, #E9E9E9 3%, #d8d8d8 100%); + background: linear-gradient(to bottom, #E9E9E9 3%, #d8d8d8 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#f1f1f1', endColorstr='#d8d8d8', GradientType=0); + + -webkit-box-shadow: inset 0px 1px 1px 0px rgba(255, 255, 255, 0.76); + -moz-box-shadow: inset 0px 1px 1px 0px rgba(255, 255, 255, 0.76); + box-shadow: inset 0px 1px 1px 0px rgba(255, 255, 255, 0.76); + + overflow: hidden; + border-bottom: 2px solid #BDBCC1; + } + + .top > div { + float: left; + } + + .panel { + padding-left: 9px; + padding-top: 2px; + } + + .panel > span { + display: inline-block; + float: left; + width: 12px; + height: 12px; + margin-right: 7px; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + cursor: pointer; + + } + + .panel span.first { + background: #FF5F4F; + } + + .panel span.second { + background: #F9C206; + } + + .panel span.third { + background: #19CC32; + } + + .nav { + overflow: hidden; + } + + .nav > span { + display: inline-block; + float: left; + background: #FBFBFB; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + height: 23px; + padding: 0 8px; + cursor: pointer; + color: #B4B4B4; + border-bottom: 1px solid #CECECE; + } + + .nav > span:hover { + background: #f2f2f2; + color: #666; + } + + .nav > span i.fa { + font-size: 23px; + } + + .nav span.active { + color: #707070; + } + + .nav span.prev { + margin-right: 1px; + margin-left: 7px; + } + + .nav span.next { + margin-right: 7px; + } + + .nav span.set i { + font-size: 14px; + position: relative; + top: 3px; + } + + .nav span.address { + width: 400px; + margin-left: 75px; + display: inline-block; + background: #fff; + line-height: 23px; + padding: 0; + text-align: center; + position: relative; + } + + .nav span.address > input { + font-size: 12px; + color: #505050; + border: none; + background: none; + text-align: center; + position: relative; + width: 300px; + } + + .nav span.address > input:focus { + outline: none; + } + + .nav span.address > input.class { + text-align: left; + } + + .nav span.address > i.fa { + position: absolute; + right: 5px; + top: 7px; + font-size: 11px; + color: #010101; + } + + .nav.right { + float: right !important; + margin-right: 35px; + } + + .nav span.share { + margin-right: 7px; + } + + .nav span.share > i.fa { + font-size: 11px; + position: relative; + top: 2px; + } + + .nav span.tabs { + position: relative; + width: 26px; + padding: 0; + } + + .nav span.tabs span { + height: 7px; + width: 7px; + border: 1px solid #B4B4B4; + display: inline-block; + position: absolute; + background: #FBFBFB; + } + + .nav span.tabs span.front { + top: 8px; + left: 6px; + z-index: 6; + } + + .nav span.tabs span.behind { + top: 6px; + left: 8px; + z-index: 5; + } + + .nav span.tabs:hover span { + border: 1px solid #666; + } + + span.new { + cursor: pointer; + position: absolute; + right: 0; + bottom: 0; + background: #CACACA; + width: 23px; + height: 23px; + text-align: center; + line-height: 23px; + border-top: 1px solid #C1C2C2; + border-left: 1px solid #C1C2C2; + } + + span.new:hover { + -webkit-box-shadow: inset 0px 1px 1px 0px rgba(0, 0, 0, 0.1); + -moz-box-shadow: inset 0px 1px 1px 0px rgba(0, 0, 0, 0.1); + box-shadow: inset 0px 1px 1px 0px rgba(0, 0, 0, 0.1); + } + + span.new .plus { + position: absolute; + background: #b0b0b0; + display: inline-block; + } + + span.new .plus.x { + height: 1px; + width: 14px; + top: 12px; + right: 0; + left: 0; + margin: auto; + } + + span.new .plus.y { + height: 14px; + width: 1px; + bottom: 0; + top: 0; + margin: auto; + + } + + #terminal { + height: 100%; + width: 100%; + } + + pre { + border: 0; + border-radius: 3px; + } \ No newline at end of file diff --git a/static/js/500.js b/static/js/500.js new file mode 100644 index 00000000..2b1c461e --- /dev/null +++ b/static/js/500.js @@ -0,0 +1,36 @@ + var app = document.getElementById('error'); + + var typewriter = new Typewriter(app, { + loop: false, + deleteSpeed: 40, + typingSpeed: "natural", + devMode: false + }); + + + + typewriter.appendText('Python 3.6.4 (default, Jan 5 2018, 02:35:40)\n') + .appendText('[GCC 7.2.1 20171224] on linux\n') + .appendText('Type "help", "copyright", "credits" or "license" for more information.\n') + .appendText('>>> ') + .pauseFor(1000) + .typeString("impor requests") + .deleteChars(9) + .typeString("t requests\n") + .appendText(">>> ") + .pauseFor(750) + .changeSettings({typingSpeed: "natural"}) + .typeString("response = requests."+window._RequestMethod+"('https://pythim") + .deleteChars(2) + .typeString("ondiscord.con/") + .deleteChars(2) + .typeString("m"+window._Path+"')\n") + .pauseFor(1000) + .appendText("<Response ["+window._Code+"]>\n>>> ") + .typeString("# hmmmm") + .pauseFor(1000) + .deleteChars(7) + .pauseFor(1000) + .typeString("response.text\n") + .appendText("'"+window._ErrorMsg+"'\n>>> ") + .start(); diff --git a/static/js/typewriter.js b/static/js/typewriter.js new file mode 100644 index 00000000..e9f82adc --- /dev/null +++ b/static/js/typewriter.js @@ -0,0 +1,612 @@ +/* + * Title: Typewriter JS + * Description: A native javascript plugin that can be used to create an elegent automatic typewriter animation effect on websites. + * Author: Tameem Safi + * Website: https://safi.me.uk + * Version: 1.0.0 + */ + +(function() { + + "use strict"; + + // http://paulirish.com/2011/requestanimationframe-for-smart-animating/ + // http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating + // requestAnimationFrame polyfill by Erik Möller. fixes from Paul Irish and Tino Zijdel + // MIT license + (function() { + var lastTime = 0; + var vendors = ['ms', 'moz', 'webkit', 'o']; + for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { + window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame']; + window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] + || window[vendors[x]+'CancelRequestAnimationFrame']; + } + + if (!window.requestAnimationFrame) + window.requestAnimationFrame = function(callback, element) { + var currTime = new Date().getTime(); + var timeToCall = Math.max(0, 16 - (currTime - lastTime)); + var id = window.setTimeout(function() { callback(currTime + timeToCall); }, + timeToCall); + lastTime = currTime + timeToCall; + return id; + }; + + if (!window.cancelAnimationFrame) + window.cancelAnimationFrame = function(id) { + clearTimeout(id); + }; + }()); + + window.Typewriter = function Typewriter(element, options) { + this._settings = { + cursorAnimationPaused: false, + opacityIncreasing: false, + currentOpacity: 1, + delayedQue: [], + delayItemsCount: 0, + eventQue: [], + calledEvents: [], + eventRunning: false, + timeout: false, + delayExecution: false, + fps: (60/1000), + typingFrameCount: 0, + stringToTypeHTMLArray: [], + currentTypedCharacters: [], + typing: false, + usedIDs: [], + charAmountToDelete: false, + userOptions: {}, + eventLoopRerun: 0 + }; + + if(!element) { + return console.error('Please choose an DOM element so that type writer can display itself.'); + } + + // if(!options.strings && !(options.strings instanceof Array || typeof options.strings === 'string')) { + // return console.error('Please enter an array of strings for the typewriter animation to work.'); + // } + + if(typeof options !== 'object') { + return console.error('Typewriter only accepts the options as an object.'); + } + + this._settings.userOptions = options; + + this.default_options = { + strings: false, + cursorClassName: 'typewriter-cursor', + cursor: '|', + animateCursor: true, + blinkSpeed: 50, + typingSpeed: 'natural', + deleteSpeed: 'natural', + charSpanClassName: 'typewriter-char', + wrapperClassName: 'typewriter-wrapper', + loop: false, + autoStart: false, + devMode: false + }; + + this.options = this._setupOptions(options); + + this.el = element; + + this._setupTypwriterWrapper(); + + this._startCursorAnimation(); + + if(this.options.autoStart === true && this.options.strings) { + this.typeOutAllStrings(); + } + + }; + + var TypewriterPrototype = window.Typewriter.prototype; + + TypewriterPrototype.stop = function() { + this._addToEventQue(this._stopEventLoop) + return this; + }; + + TypewriterPrototype.start = function() { + this._startEventLoop(); + return this; + }; + + TypewriterPrototype.rerun = function() { + this._addToEventQue(this._rerunCalledEvents); + return this; + }; + + TypewriterPrototype.typeString = function(string) { + if(!string || typeof string != 'string') { + return console.error('Please enter a string as the paramater.'); + } + + var string_chars = this._getCharacters(string); + + this._addToEventQue([this._typeCharacters, [string_chars]]); + return this; + }; + + TypewriterPrototype.deleteAll = function() { + this._addToEventQue([this._deleteChars, ['all']]); + return this; + }; + + TypewriterPrototype.deleteChars = function(amount) { + this._addToEventQue([this._deleteChars, [amount]]); + return this; + }; + + TypewriterPrototype.pauseFor = function(ms) { + this._addToEventQue([this._pauseFor, [ms]]); + return this; + }; + + TypewriterPrototype.typeOutAllStrings = function() { + var characters_array = this._getStringsAsCharsArray(); + + if(characters_array.length === 1) { + this._typeCharacters(characters_array[0]); + } else { + for(var i = 0, length = characters_array.length; i < length; i++) { + this._addToEventQue([this._typeCharacters, [characters_array[i]]]); + this.pauseFor(this._randomInteger(1500, 2500)); + this.deleteAll(); + this.pauseFor(this._randomInteger(1500, 2500)); + } + } + + return this; + + }; + + TypewriterPrototype.changeSettings = function(new_settings) { + if(!new_settings && typeof new_settings !== 'object') { + return console.error('Typewriter will only accept an object as the settings.'); + } + + this._addToEventQue([this._changeSettings, [JSON.stringify(new_settings)]]); + + return this; + + }; + + TypewriterPrototype.changeBlinkSpeed = function(new_speed) { + if(!new_speed && typeof new_speed !== 'number') { + return console.error('Please enter a number for the new blink speed.'); + } + + this.changeSettings({ + blinkSpeed: new_speed + }); + + return this; + }; + + TypewriterPrototype.changeTypingSpeed = function(new_speed) { + if(!new_speed && typeof new_speed !== 'number') { + return console.error('Please enter a number for the new typing speed.'); + } + + var new_settings = { + typingSpeed: new_speed + }; + + this.changeSettings({ + typingSpeed: new_speed + }); + + return this; + }; + + TypewriterPrototype.changeDeleteSpeed = function(new_speed) { + if(!new_speed && typeof new_speed !== 'number') { + return console.error('Please enter a number for the new delete speed.'); + } + + this.changeSettings({ + changeDeleteSpeed: new_speed + }); + + return this; + }; + + TypewriterPrototype._rerunCalledEvents = function() { + if(this._settings.currentTypedCharacters.length > 0) { + this.deleteAll(); + this._resetEventLoop('rerunCalledEvents'); + } else { + this._settings.eventQue = this._settings.calledEvents; + this._settings.calledEvents = []; + this.options = this._setupOptions(this._settings.userOptions); + this._settings.usedIDs = []; + this.charAmountToDelete = false; + this._startEventLoop(); + } + }; + + TypewriterPrototype._deleteChars = function(amount) { + + + if(amount) { + this._settings.charAmountToDelete = amount; + } + this._deletingCharIdsAnimation = window.requestAnimationFrame(this._deletingCharAnimationFrame.bind(this)); + return this; + }; + + TypewriterPrototype._pauseFor = function(ms) { + var self = this; + self._settings.eventRunning = true; + setTimeout(function() { + self._resetEventLoop('pauseFor'); + }, ms); + }; + + TypewriterPrototype._changeSettings = function(new_settings) { + this.options = this._setupOptions(JSON.parse(new_settings[0])); + this._resetEventLoop('changeSettings'); + + if(this.options.devMode) { + console.log('New settings', this.options); + } + + }; + + TypewriterPrototype._deletingCharAnimationFrame = function() { + var self = this; + var delete_speed = this.options.deleteSpeed; + var typewriter_wrapper_class_name = self.options.wrapperClassName; + var current_typed_char_ids = self._settings.currentTypedCharacters; + var char_amount_to_delete = self._settings.charAmountToDelete; + + if(!self._settings.charAmountToDelete || self._settings.charAmountToDelete === 0 || current_typed_char_ids === 0) { + self._resetEventLoop('deletingCharAnimationFrame'); + return true; + } + + if(delete_speed == 'natural') { + delete_speed = self._randomInteger(50, 150); + } + + if(char_amount_to_delete == 'all') { + char_amount_to_delete = current_typed_char_ids.length; + self._settings.charAmountToDelete = char_amount_to_delete; + } + + setTimeout(function() { + if(self._settings.charAmountToDelete) { + var last_typed_char_index = current_typed_char_ids.length - 1; + var get_last_typed_char = current_typed_char_ids[last_typed_char_index]; + + self._settings.currentTypedCharacters.splice(last_typed_char_index, 1); + + var char_to_delete_el = document.getElementById(get_last_typed_char); + + if(char_to_delete_el) { + var typewriter_wrapper_el = self.el.querySelector('.' + typewriter_wrapper_class_name); + typewriter_wrapper_el.removeChild(char_to_delete_el); + self._settings.charAmountToDelete = char_amount_to_delete - 1; + + if(self.options.devMode) { + console.log('Deleted char with ID', get_last_typed_char); + } + } + + } + + self._deletingCharIdsAnimation = window.requestAnimationFrame(self._deletingCharAnimationFrame.bind(self)); + + }, delete_speed); + }; + + TypewriterPrototype._setupOptions = function(new_options) { + var merged_options = {}; + + for (var attrname in this.default_options) { + merged_options[attrname] = this.default_options[attrname]; + } + + if(this._settings.userOptions) { + for (var attrname in this._settings.userOptions) { + merged_options[attrname] = this._settings.userOptions[attrname]; + } + } + + for (var attrname in new_options) { + merged_options[attrname] = new_options[attrname]; + } + + return merged_options; + } + + TypewriterPrototype._addToEventQue = function(event) { + this._settings.eventQue.push(event); + if(this._settings.eventQue.length > 0 && !this._settings.eventRunning && this.options.autoStart) { + this._startEventLoop(); + } + }; + + TypewriterPrototype._startEventLoop = function() { + if(this.options.devMode) { + console.log('Event loop started.'); + } + + if(!this._settings.eventRunning) { + + if(this._settings.eventQue.length > 0) { + this.eventLoopRerun = 0; + var first_event = this._settings.eventQue[0]; + if(typeof first_event == 'function') { + this._settings.eventRunning = true; + this._settings.calledEvents.push(first_event); + this._settings.eventQue.splice(0, 1); + first_event.call(this); + if(this.options.devMode) { + console.log('Event started.'); + } + } else if(first_event instanceof Array) { + if(typeof first_event[0] == 'function' && first_event[1] instanceof Array) { + this._settings.eventRunning = true; + this._settings.calledEvents.push(first_event); + this._settings.eventQue.splice(0, 1); + first_event[0].call(this, first_event[1]); + if(this.options.devMode) { + console.log('Event started.'); + } + } + } + } + this._eventQueAnimation = window.requestAnimationFrame(this._startEventLoop.bind(this)); + } + + if(!this._settings.eventRunning && this._settings.eventQue.length <= 0) { + var self = this; + self._stopEventLoop(); + setTimeout(function() { + if(self.options.loop) { + self.eventLoopRerun++; + if(self.options.devMode) { + console.log('Before Loop State', self._settings); + } + if(self.eventLoopRerun > 4) { + console.error('Maximum amount of loop retries reached.'); + self._stopEventLoop(); + } else { + if(self.options.devMode) { + console.log('Looping events.'); + } + self._rerunCalledEvents(); + } + } + }, 1000); + return; + } + + }; + + TypewriterPrototype._resetEventLoop = function(name) { + var event_name = name || 'Event'; + this._settings.eventRunning = false; + this._startEventLoop(); + if(this.options.devMode) { + console.log(event_name, 'Finished'); + } + }; + + TypewriterPrototype._stopEventLoop = function() { + window.cancelAnimationFrame(this._eventQueAnimation); + if(this.options.devMode) { + console.log('Event loop stopped.'); + } + }; + + TypewriterPrototype._setupTypwriterWrapper = function() { + var typewriter_wrapper_class_name = this.options.wrapperClassName; + var typewriter_wrapper = document.createElement('span'); + typewriter_wrapper.className = typewriter_wrapper_class_name; + this.el.innerHTML = ''; + this.el.appendChild(typewriter_wrapper); + }; + + TypewriterPrototype._typeCharacters = function(characters_array) { + this._settings.stringToTypeHTMLArray = this._convertCharsToHTML(characters_array); + this._typingAnimation = window.requestAnimationFrame(this._typingAnimationFrame.bind(this, characters_array.length)); + return this; + }; + + TypewriterPrototype._typingAnimationFrame = function(total_items) { + var self = this; + var typing_speed = this.options.typingSpeed; + var typewriter_wrapper_class_name = self.options.wrapperClassName; + + if(self._settings.stringToTypeHTMLArray.length == 0) { + window.cancelAnimationFrame(self._typingAnimation); + this._resetEventLoop('typingAnimationFrame'); + return true; + } + + if(typing_speed == 'natural') { + typing_speed = this._randomInteger(50, 150); + } + + setTimeout(function() { + var el_inner_html = self.el.innerHTML; + var item_to_type = self._settings.stringToTypeHTMLArray[0]; + self.el.querySelector('.' + typewriter_wrapper_class_name).appendChild(item_to_type.el); + self._settings.currentTypedCharacters.push(item_to_type.id); + self._settings.stringToTypeHTMLArray.splice(0, 1); + self._typingAnimation = window.requestAnimationFrame(self._typingAnimationFrame.bind(self, total_items)); + if(self.options.devMode) { + console.log('Typed', item_to_type); + } + }, typing_speed); + }; + + TypewriterPrototype._convertCharsToHTML = function(chars) { + var chars_html_wrap_array = []; + var char_class_name = this.options.charSpanClassName; + var chars_array = chars[0]; + + for(var i = 0, length = chars_array.length; i < length; i++) { + var char_element = document.createElement('span'); + var char_id = this._generateUniqueID(); + char_element.id = char_id; + char_element.className = char_class_name + ' typewriter-item-' + i; + char_element.innerHTML = chars_array[i]; + chars_html_wrap_array.push({ + id: char_id, + el: char_element + }); + } + + return chars_html_wrap_array; + }; + + TypewriterPrototype._getCharacters = function(string) { + if(typeof string !== 'string') { + return false; + } + return string.split(""); + }; + + TypewriterPrototype._getStringsAsCharsArray = function() { + var strings_array_check = this.options.strings instanceof Array; + var strings_string_check = typeof this.options.strings === 'string'; + if(!strings_array_check) { + if(!strings_string_check) { + return console.error('Typewriter only accepts strings or an array of strings as the input.'); + } + return [this.options.strings.split("")]; + } + + var strings_chars_array = []; + + for (var i = 0, length = this.options.strings.length; i < length; i++) { + var string_chars = this._getCharacters(this.options.strings[i]); + if(!string_chars) { + console.error('Please enter only strings.'); + break; + } + strings_chars_array.push(string_chars); + } + + return strings_chars_array; + }; + + TypewriterPrototype._cursorAnimationFrame = function() { + if(!this._settings.cursorAnimationPaused) { + var blink_speed = this.options.blinkSpeed; + var opacity_amount = (1/1000) * blink_speed; + + var cursor_el = this.el.querySelector('.typewriter-cursor'); + + if(this._settings.opacityIncreasing == true) { + if(this._settings.currentOpacity >= 1) { + this._settings.opacityIncreasing = false; + this._settings.currentOpacity = 1; + } + + this._settings.currentOpacity += opacity_amount; + } + + if(this._settings.opacityIncreasing == false) { + if(this._settings.currentOpacity <= 0) { + this._settings.opacityIncreasing = true; + this._settings.currentOpacity = 0; + } + + this._settings.currentOpacity -= opacity_amount; + } + + cursor_el.style.opacity = this._settings.currentOpacity; + this._cursorAnimation = window.requestAnimationFrame(this._cursorAnimationFrame.bind(this)); + } + }; + + TypewriterPrototype.appendText = function(text){ + this._addToEventQue([this._appendText, [text]]) + return this + } + + TypewriterPrototype._appendText = function(text){ + var char_class_name = this.options.charSpanClassName; + var char_element = document.createElement('span'); + var char_id = this._generateUniqueID(); + char_element.id = char_id; + char_element.className = char_class_name + ' dom-appended' + char_element.innerHTML = text + var items = [] + items.push({ + id: char_id, + el: char_element + }); + + this._settings.stringToTypeHTMLArray = items + + window.requestAnimationFrame(this._typingAnimationFrame.bind(this, 1)); + + } + + TypewriterPrototype._startCursorAnimation = function() { + var cursor = this.options.cursor; + var cursor_class_name = this.options.cursorClassName; + + var cursor_element = document.createElement('span'); + cursor_element.className = cursor_class_name; + cursor_element.innerHTML = cursor; + + this.el.appendChild(cursor_element); + if(this.options.animateCursor) { + this._cursorAnimation = window.requestAnimationFrame(this._cursorAnimationFrame.bind(this)); + } + }; + + TypewriterPrototype._pauseCursorAnimation = function() { + if(!this._settings.cursorAnimationPaused) { + window.cancelAnimationFrame(this._cursorAnimation); + this._settings.cursorAnimationPaused = true; + } + }; + + TypewriterPrototype._restartCursorAnimation = function() { + if(!this._settings.cursorAnimationPaused) { + return console.error('Cursor animation is already running.') + } + + this._settings.cursorAnimationPaused = false; + this._cursorAnimation = window.requestAnimationFrame(this._cursorAnimationFrame.bind(this)); + }; + + /* Utils */ + TypewriterPrototype._randomInteger = function(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; + }; + + TypewriterPrototype._randomID = function() { + var text = ""; + var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + + for( var i=0; i < this._randomInteger(5, 15); i++ ) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; + }; + + TypewriterPrototype._generateUniqueID = function() { + var temp_id = this._randomID(); + if(this._settings.usedIDs.indexOf(temp_id) == -1) { + this._settings.usedIDs.push(temp_id); + return temp_id; + } + return this._generateUniqueID.call(this); + }; + + +})(); \ No newline at end of file diff --git a/templates/errors/error.html b/templates/errors/error.html new file mode 100644 index 00000000..8aa5ff16 --- /dev/null +++ b/templates/errors/error.html @@ -0,0 +1,59 @@ +{% extends 'main/base.html' %} +{% block title %} {{ code }} - Internal server error {% endblock %} +{% block beta_error %}{% endblock %} +{% block content %} + +
+
+
{{ error_title }}
+
+
+ + +
+
+
+
+ + + +
+ + + + +
+ +
+
+
+
+
+ +
+
+ + + + + + +{% endblock %} \ No newline at end of file diff --git a/templates/main/base.html b/templates/main/base.html index 3da4715e..50281159 100644 --- a/templates/main/base.html +++ b/templates/main/base.html @@ -14,7 +14,8 @@ {% include "main/navigation.html" %} -{% if current_page != "index" %} +{% if current_page != "index"%} + {% block beta_error %}
@@ -25,6 +26,7 @@
+ {% endblock %} {% endif %} {% block content %}{% endblock %} diff --git a/tox.ini b/tox.ini index 561a1ad4..26d06d9c 100644 --- a/tox.ini +++ b/tox.ini @@ -2,3 +2,5 @@ max-line-length=120 application_import_names=pysite ignore=P102 +exclude=__pycache__, venv, app_test.py +import-order-style=pep8 \ No newline at end of file -- cgit v1.2.3