├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── README.md ├── app ├── __init__.py ├── admin_content │ ├── __init__.py │ ├── about.html │ ├── forms.py │ └── routes.py ├── admin_image │ ├── __init__.py │ ├── forms.py │ └── routes.py ├── admin_message │ ├── __init__.py │ └── routes.py ├── admin_tag │ ├── __init__.py │ ├── forms.py │ └── routes.py ├── auth │ ├── __init__.py │ ├── email.py │ ├── forms.py │ └── routes.py ├── email.py ├── errors │ ├── __init__.py │ └── errors.py ├── main │ ├── __init__.py │ ├── email.py │ ├── forms.py │ └── routes.py ├── models.py ├── post │ ├── __init__.py │ ├── forms.py │ └── routes.py ├── search │ ├── __init__.py │ └── routes.py ├── sitemap │ ├── __init__.py │ └── routes.py ├── special │ ├── __init__.py │ └── routes.py ├── static │ ├── ads.txt │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── images │ │ ├── bt │ │ │ ├── ball.jpg │ │ │ ├── bt-drop-in-placeholder.png │ │ │ ├── cards │ │ │ │ ├── amex.svg │ │ │ │ ├── discover.svg │ │ │ │ ├── mastercard.svg │ │ │ │ └── visa.svg │ │ │ ├── cat.jpg │ │ │ ├── check.svg │ │ │ ├── fail.svg │ │ │ ├── favicon.png │ │ │ ├── flower.jpg │ │ │ ├── loading.gif │ │ │ ├── paypal-demo.svg │ │ │ ├── paypal.svg │ │ │ ├── pseudoshop.svg │ │ │ ├── shopping-cart-solid.svg │ │ │ ├── shopping-cart-solid0.svg │ │ │ └── success.svg │ │ ├── garden_smallx2.png │ │ ├── github-brands.svg │ │ ├── linkedin-brands.svg │ │ ├── robots_smallx2.png │ │ ├── snakegame_smallx2.png │ │ ├── stars_smallx2.png │ │ ├── sunmoon_smallx2.png │ │ └── wrench-solid.svg │ ├── processing │ │ ├── garden.js │ │ ├── robots.js │ │ ├── snake_game.js │ │ ├── stars.js │ │ └── sun_moon.js │ ├── scripts │ │ ├── app.js │ │ ├── bulmanav.js │ │ ├── fa │ │ │ └── all.js │ │ ├── prism.js │ │ └── test.js │ ├── site.webmanifest │ ├── styles │ │ ├── app.css │ │ ├── mybulma.css │ │ └── prism.css │ └── tinymce │ │ ├── jquery.tinymce.min.js │ │ ├── langs │ │ └── readme.md │ │ ├── license.txt │ │ ├── plugins │ │ ├── advlist │ │ │ └── plugin.min.js │ │ ├── anchor │ │ │ └── plugin.min.js │ │ ├── autolink │ │ │ └── plugin.min.js │ │ ├── autoresize │ │ │ └── plugin.min.js │ │ ├── autosave │ │ │ └── plugin.min.js │ │ ├── bbcode │ │ │ └── plugin.min.js │ │ ├── charmap │ │ │ └── plugin.min.js │ │ ├── code │ │ │ └── plugin.min.js │ │ ├── codesample │ │ │ └── plugin.min.js │ │ ├── colorpicker │ │ │ └── plugin.min.js │ │ ├── contextmenu │ │ │ └── plugin.min.js │ │ ├── directionality │ │ │ └── plugin.min.js │ │ ├── emoticons │ │ │ ├── js │ │ │ │ ├── emojis.js │ │ │ │ └── emojis.min.js │ │ │ └── plugin.min.js │ │ ├── fullpage │ │ │ └── plugin.min.js │ │ ├── fullscreen │ │ │ └── plugin.min.js │ │ ├── help │ │ │ └── plugin.min.js │ │ ├── hr │ │ │ └── plugin.min.js │ │ ├── image │ │ │ └── plugin.min.js │ │ ├── imagetools │ │ │ └── plugin.min.js │ │ ├── importcss │ │ │ └── plugin.min.js │ │ ├── insertdatetime │ │ │ └── plugin.min.js │ │ ├── legacyoutput │ │ │ └── plugin.min.js │ │ ├── link │ │ │ └── plugin.min.js │ │ ├── lists │ │ │ └── plugin.min.js │ │ ├── media │ │ │ └── plugin.min.js │ │ ├── nonbreaking │ │ │ └── plugin.min.js │ │ ├── noneditable │ │ │ └── plugin.min.js │ │ ├── pagebreak │ │ │ └── plugin.min.js │ │ ├── paste │ │ │ └── plugin.min.js │ │ ├── preview │ │ │ └── plugin.min.js │ │ ├── print │ │ │ └── plugin.min.js │ │ ├── quickbars │ │ │ └── plugin.min.js │ │ ├── save │ │ │ └── plugin.min.js │ │ ├── searchreplace │ │ │ └── plugin.min.js │ │ ├── spellchecker │ │ │ └── plugin.min.js │ │ ├── tabfocus │ │ │ └── plugin.min.js │ │ ├── table │ │ │ └── plugin.min.js │ │ ├── template │ │ │ └── plugin.min.js │ │ ├── textcolor │ │ │ └── plugin.min.js │ │ ├── textpattern │ │ │ └── plugin.min.js │ │ ├── toc │ │ │ └── plugin.min.js │ │ ├── visualblocks │ │ │ └── plugin.min.js │ │ ├── visualchars │ │ │ └── plugin.min.js │ │ └── wordcount │ │ │ └── plugin.min.js │ │ ├── skins │ │ ├── content │ │ │ ├── default │ │ │ │ └── content.min.css │ │ │ ├── document │ │ │ │ └── content.min.css │ │ │ └── writer │ │ │ │ └── content.min.css │ │ └── ui │ │ │ ├── oxide-dark │ │ │ ├── content.inline.min.css │ │ │ ├── content.min.css │ │ │ └── skin.min.css │ │ │ └── oxide │ │ │ ├── content.inline.min.css │ │ │ ├── content.min.css │ │ │ ├── content.mobile.min.css │ │ │ ├── fonts │ │ │ └── tinymce-mobile.woff │ │ │ ├── skin.min.css │ │ │ └── skin.mobile.min.css │ │ ├── themes │ │ ├── mobile │ │ │ └── theme.min.js │ │ └── silver │ │ │ └── theme.min.js │ │ └── tinymce.min.js ├── templates │ ├── admin_content │ │ ├── edit_content.html │ │ └── manage_content.html │ ├── admin_image │ │ ├── del_image.html │ │ └── manage_images.html │ ├── admin_message │ │ └── manage_message.html │ ├── admin_tag │ │ ├── del_tag.html │ │ ├── edit_tag.html │ │ └── manage_tags.html │ ├── auth │ │ ├── email_reset_password.html │ │ ├── email_reset_password.txt │ │ ├── forgot_password.html │ │ ├── login.html │ │ ├── register.html │ │ └── reset_password.html │ ├── base.html │ ├── errors │ │ ├── 403.html │ │ ├── 404.html │ │ └── 500.html │ ├── main │ │ ├── about.html │ │ ├── contact.html │ │ ├── email_comment.html │ │ ├── email_comment.txt │ │ ├── email_contact.html │ │ ├── email_contact.txt │ │ ├── index bak.html │ │ ├── index.html │ │ ├── post_det.html │ │ ├── processing.html │ │ └── projects.html │ ├── post │ │ ├── _tinymce.html │ │ ├── add_post.html │ │ ├── del_post.html │ │ └── edit_post.html │ ├── search │ │ └── search.html │ ├── sitemap │ │ └── sitemap_template.xml │ ├── special │ │ └── read.html │ └── test │ │ ├── cart.html │ │ ├── checkout.html │ │ ├── rental.html │ │ ├── rental_results.html │ │ ├── result_checkout.html │ │ ├── shop.html │ │ └── test_vue.html └── test │ ├── __init__.py │ ├── forms.py │ ├── gateway.py │ ├── payment.py │ ├── rental.py │ └── test_vue.py ├── blog.py ├── config.py ├── migrations ├── README ├── alembic.ini ├── env.py └── script.py.mako ├── requirements.txt ├── requirements_old.txt └── sql.sql /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.db 3 | env 4 | secret.txt 5 | *.zip 6 | *.sql 7 | *.tar.gz 8 | runenv.bat 9 | uploads 10 | test.py 11 | test.html 12 | app/static/uploads/ 13 | logs/ 14 | .env 15 | migrations 16 | __pycache__ 17 | app/static/rental/ 18 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python: Flask", 9 | "type": "python", 10 | "request": "launch", 11 | "module": "flask", 12 | "env": { 13 | "FLASK_APP": "blog.py", 14 | "FLASK_ENV": "development", 15 | "FLASK_DEBUG": "1" 16 | }, 17 | "args": [ 18 | "run" 19 | ], 20 | "subProcess": true, 21 | "jinja": true 22 | } 23 | ] 24 | } 25 | 26 | /* 27 | { 28 | // Use IntelliSense to learn about possible attributes. 29 | // Hover to view descriptions of existing attributes. 30 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 31 | "version": "0.2.0", 32 | "configurations": [ 33 | { 34 | "name": "Python: Flask", 35 | "type": "python", 36 | "request": "launch", 37 | "module": "flask", 38 | "env": { 39 | "FLASK_APP": "app.py", 40 | "FLASK_ENV": "development", 41 | "FLASK_DEBUG": "0" 42 | }, 43 | "args": [ 44 | "run", 45 | "--no-debugger", 46 | "--no-reload" 47 | ], 48 | "jinja": true 49 | } 50 | ] 51 | }*/ -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "env\\Scripts\\python.exe" 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | "# blog" 2 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import Flask 3 | from flask_sqlalchemy import SQLAlchemy 4 | from config import Config 5 | from flask_migrate import Migrate 6 | from flask_login import LoginManager 7 | import logging 8 | from logging.handlers import RotatingFileHandler 9 | from flask_moment import Moment 10 | 11 | 12 | db = SQLAlchemy() 13 | # compare_type = true - this is so that flask migrate detect changes to columns like size 14 | migrate = Migrate(compare_type=True) 15 | moment = Moment() 16 | login_manager = LoginManager() 17 | 18 | def create_app(config_class=Config): 19 | app = Flask(__name__) 20 | app.config.from_object(config_class) 21 | 22 | with app.app_context(): 23 | db.init_app(app) 24 | login_manager.init_app(app) 25 | migrate.init_app(app,db) 26 | moment.init_app(app) 27 | 28 | from app.errors import bp as errors_bp 29 | app.register_blueprint(errors_bp) 30 | 31 | from app.auth import bp as auth_bp 32 | app.register_blueprint(auth_bp, url_prefix='/auth') 33 | 34 | from app.main import bp as main_bp 35 | app.register_blueprint(main_bp) 36 | 37 | from app.post import bp as post_bp 38 | app.register_blueprint(post_bp, url_prefix='/admin') 39 | 40 | from app.search import bp as search_bp 41 | app.register_blueprint(search_bp) 42 | 43 | from app.sitemap import bp as sitemap_bp 44 | app.register_blueprint(sitemap_bp) 45 | 46 | from app.admin_content import bp as admin_content_bp 47 | app.register_blueprint(admin_content_bp, url_prefix='/admin') 48 | 49 | from app.admin_image import bp as admin_image_bp 50 | app.register_blueprint(admin_image_bp, url_prefix='/admin') 51 | 52 | from app.admin_tag import bp as admin_tag_bp 53 | app.register_blueprint(admin_tag_bp, url_prefix='/admin') 54 | 55 | from app.admin_message import bp as admin_message_bp 56 | app.register_blueprint(admin_message_bp, url_prefix='/admin') 57 | 58 | from app.special import bp as special_bp 59 | app.register_blueprint(special_bp) 60 | 61 | from app.test import bp as test_bp 62 | app.register_blueprint(test_bp, url_prefix='/test') 63 | 64 | #setup log files 65 | if not app.debug: 66 | if not os.path.exists('logs'): 67 | os.mkdir('logs') 68 | #Log size rotates at 100KB so file size doesn't grow too big 69 | #Keeps the last 5 log files as backup 70 | file_handler = RotatingFileHandler('logs/blog.log', maxBytes=102400, backupCount=5) 71 | #Provide custom formatting for log messages 72 | file_handler.setFormatter(logging.Formatter( \ 73 | '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]' \ 74 | )) 75 | #set logging level to INFO in file logger 76 | file_handler.setLevel(logging.INFO) 77 | app.logger.addHandler(file_handler) 78 | #set logging level to INFO in application logger 79 | app.logger.setLevel(logging.INFO) 80 | app.logger.info('Blog startup') 81 | 82 | return app 83 | 84 | #from app import models 85 | -------------------------------------------------------------------------------- /app/admin_content/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | bp = Blueprint('admin_content', __name__) 4 | 5 | from app.admin_content import routes 6 | -------------------------------------------------------------------------------- /app/admin_content/about.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}About Me{% endblock %} 3 | 4 | {% block app_content %} 5 | {{ about_html.content|safe }} 6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /app/admin_content/forms.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import SubmitField, TextAreaField 3 | 4 | class ContentManageForm(FlaskForm): 5 | #post is actually content, just call it post because tinymce uses post id selector 6 | post = TextAreaField('Write something') 7 | submit = SubmitField('Submit') 8 | -------------------------------------------------------------------------------- /app/admin_content/routes.py: -------------------------------------------------------------------------------- 1 | from flask import render_template, redirect, url_for, flash 2 | from flask_login import login_required 3 | from app import db 4 | from app.admin_content import bp 5 | from app.admin_content.forms import ContentManageForm 6 | from datetime import datetime 7 | from app.models import Content 8 | 9 | ############################################################################## 10 | # Admin Content Management blueprint 11 | ############################################################################## 12 | 13 | @bp.route('/manage_content',methods=['GET']) 14 | @login_required 15 | def manage_content(): 16 | contents = Content.query.all() 17 | return render_template('admin_content/manage_content.html',contents=contents) 18 | 19 | @bp.route('/edit_content/',methods=['GET','POST']) 20 | @login_required 21 | def edit_content(id): 22 | content = Content.query.filter_by(id=id).first() 23 | if content is None: 24 | flash('No such content exists.','danger') 25 | return redirect(url_for('main.index')) 26 | form=ContentManageForm() 27 | if form.validate_on_submit(): 28 | # update db 29 | content.content = form.post.data 30 | content.update_date = datetime.utcnow() 31 | db.session.add(content) 32 | db.session.commit() 33 | flash('The content has been updated!','success') 34 | return redirect(url_for('admin_content.manage_content')) 35 | return render_template('admin_content/edit_content.html',form=form,content=content) 36 | -------------------------------------------------------------------------------- /app/admin_image/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | bp = Blueprint('admin_image', __name__) 4 | 5 | from app.admin_image import routes 6 | -------------------------------------------------------------------------------- /app/admin_image/forms.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import SubmitField 3 | 4 | class DeleteImageForm(FlaskForm): 5 | submit = SubmitField('Delete') 6 | -------------------------------------------------------------------------------- /app/admin_image/routes.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import (current_app, flash, redirect, render_template, request, url_for) 3 | from flask_login import login_required 4 | from app import db 5 | from app.admin_image import bp 6 | from app.admin_image.forms import DeleteImageForm 7 | from app.models import Images 8 | 9 | ############################################################################## 10 | # Admin Images blueprint 11 | ############################################################################## 12 | 13 | @bp.route('/manage_images',methods=['GET']) 14 | @login_required 15 | def manage_images(): 16 | # get page number from url. If no page number use page 1 17 | page = request.args.get('page',1,type=int) 18 | # True means 404 error is returned if page is out of range. False means an empty list is returned 19 | images = Images.query.order_by(Images.create_date.desc()) \ 20 | .paginate(page,current_app.config['IMAGES_PER_PAGE'],False) 21 | return render_template('admin_image/manage_images.html',images=images) 22 | 23 | @bp.route('/del_image/',methods=['GET','POST']) 24 | @login_required 25 | def del_image(id): 26 | form = DeleteImageForm() 27 | image = Images.getImage(id) 28 | # id is wrong 29 | if image is None: 30 | flash('No such image.','danger') 31 | return redirect(url_for('main.index')) 32 | if form.validate_on_submit(): 33 | img_fullpath = os.path.join(current_app.config['UPLOADED_PATH'], image.filename) 34 | tmb_fullpath = os.path.join(current_app.config['UPLOADED_PATH_THUMB'], image.thumbnail) 35 | try: 36 | # delete image and thumbnail from file system 37 | os.remove(img_fullpath) 38 | os.remove(tmb_fullpath) 39 | # delete from db 40 | db.session.delete(image) 41 | db.session.commit() 42 | flash('The image has been successfully deleted','success') 43 | return redirect(url_for('admin_image.manage_images')) 44 | except OSError: 45 | flash('System error deleting image.','danger') 46 | return redirect(url_for('admin_image.manage_images')) 47 | return render_template('admin_image/del_image.html',form=form,img=image) 48 | -------------------------------------------------------------------------------- /app/admin_message/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | bp = Blueprint('admin_message', __name__) 4 | 5 | from app.admin_message import routes 6 | -------------------------------------------------------------------------------- /app/admin_message/routes.py: -------------------------------------------------------------------------------- 1 | from flask import render_template, redirect, request, current_app 2 | from flask_login import login_required 3 | from app import db 4 | from app.admin_message import bp 5 | from app.models import Contact 6 | 7 | ############################################################################## 8 | # Admin Messages blueprint 9 | ############################################################################## 10 | 11 | @bp.route('/manage_messages',methods=['GET','POST']) 12 | @login_required 13 | def manage_messages(): 14 | page = request.args.get('page',1,type=int) 15 | contacts = Contact.query.order_by(Contact.create_date.desc()) \ 16 | .paginate(page,current_app.config['MESSAGES_PER_PAGE'],False) 17 | 18 | return render_template('admin_message/manage_message.html',contacts=contacts) 19 | -------------------------------------------------------------------------------- /app/admin_tag/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | bp = Blueprint('admin_tag', __name__) 4 | 5 | from app.admin_tag import routes 6 | -------------------------------------------------------------------------------- /app/admin_tag/forms.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import StringField, SubmitField 3 | from wtforms.validators import InputRequired, Length 4 | 5 | class EditTagForm(FlaskForm): 6 | tag_name = StringField('Tag Name', validators=[InputRequired(), Length(max=20)]) 7 | submit = SubmitField('Submit') 8 | 9 | class DeleteTagForm(FlaskForm): 10 | submit = SubmitField('Delete') 11 | -------------------------------------------------------------------------------- /app/admin_tag/routes.py: -------------------------------------------------------------------------------- 1 | from flask import render_template, redirect, flash, url_for 2 | from flask_login import login_required 3 | from app import db 4 | from app.admin_tag import bp 5 | from app.admin_tag.forms import EditTagForm, DeleteTagForm 6 | from app.models import Tag, Tagged 7 | from datetime import datetime 8 | 9 | ############################################################################## 10 | # Admin Tags blueprint 11 | ############################################################################## 12 | 13 | @bp.route('/manage_tags',methods=['GET']) 14 | @login_required 15 | def manage_tags(): 16 | # get tags being are used only 17 | tag_used = db.session.query(Tag).join(Tagged).all() 18 | # get tags not being used 19 | tag_all = Tag.query.all() 20 | tag_notused = [] 21 | for t in tag_all: 22 | if t not in tag_used: 23 | tag_notused.append(t) 24 | 25 | return render_template('admin_tag/manage_tags.html',tag_used=tag_used, tag_notused=tag_notused) 26 | 27 | @bp.route('/edit_tag/',methods=['GET','POST']) 28 | @login_required 29 | def edit_tag(id): 30 | form = EditTagForm() 31 | tag = Tag.getTag(id) 32 | if form.validate_on_submit(): 33 | tag.name = form.tag_name.data 34 | tag.update_date = datetime.utcnow() 35 | db.session.add(tag) 36 | db.session.commit() 37 | flash('The tag has been successfully updated','success') 38 | return redirect(url_for('admin_tag.manage_tags')) 39 | return render_template('admin_tag/edit_tag.html',form=form,tag=tag) 40 | 41 | @bp.route('/del_tag/',methods=['GET','POST']) 42 | @login_required 43 | def del_tag(id): 44 | form = DeleteTagForm() 45 | tag = Tag.getTag(id) 46 | if tag is None: 47 | flash('No such tag.','danger') 48 | return redirect(url_for('main.index')) 49 | if form.validate_on_submit(): 50 | db.session.delete(tag) 51 | db.session.commit() 52 | flash('The tag has been successfully deleted','success') 53 | return redirect(url_for('admin_tag.manage_tags')) 54 | return render_template('admin_tag/del_tag.html',form=form,tag=tag) 55 | -------------------------------------------------------------------------------- /app/auth/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | bp = Blueprint('auth', __name__) 4 | 5 | from app.auth import routes 6 | -------------------------------------------------------------------------------- /app/auth/email.py: -------------------------------------------------------------------------------- 1 | from flask import render_template, current_app 2 | from app.email import send_email 3 | 4 | def send_password_reset_email(user): 5 | token = user.get_reset_password_token() 6 | return send_email('Reset your password from kevin7.net', 7 | sender=current_app.config['MAIL_FROM'], 8 | recipients=[user.email], 9 | text_body=render_template('auth/email_reset_password.txt', 10 | user=user, token=token), 11 | html_body=render_template('auth/email_reset_password.html', 12 | user=user, token=token)) 13 | -------------------------------------------------------------------------------- /app/auth/forms.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm, RecaptchaField 2 | from wtforms import StringField, PasswordField, BooleanField, SubmitField, TextAreaField 3 | from wtforms.validators import InputRequired, Email, EqualTo, ValidationError, Length 4 | from flask_login import UserMixin 5 | from app.models import User 6 | 7 | # usermixin provides some handy functions for user class 8 | class LoginForm(FlaskForm, UserMixin): 9 | email = StringField('Username (email)', validators=[InputRequired()]) 10 | password = PasswordField('Password', validators=[InputRequired()]) 11 | remember_me = BooleanField('Remember Me') 12 | submit = SubmitField('Sign In') 13 | 14 | class RegistrationForm(FlaskForm): 15 | email = StringField('Email', validators=[InputRequired(), Email()]) 16 | firstname = StringField('First Name', validators=[InputRequired(), Length(max=20)]) 17 | lastname = StringField('Last Name', validators=[InputRequired(), Length(max=50)]) 18 | password = PasswordField('Password', validators=[InputRequired()]) 19 | password2 = PasswordField('Repeat Password', validators=[InputRequired(), EqualTo('password')]) 20 | submit = SubmitField('Register') 21 | recaptcha = RecaptchaField() 22 | 23 | # wtforms takes validate_ as custom validators 24 | # so the below validator gets invoked on username 25 | def validate_email(self, email): 26 | user = User.query.filter_by(email=email.data).first() 27 | if user is not None: 28 | raise ValidationError('Email has previously registered') 29 | 30 | class ForgotPasswordForm(FlaskForm): 31 | email = StringField('Email', validators=[InputRequired(), Email()]) 32 | submit = SubmitField('Submit') 33 | 34 | class ResetPasswordForm(FlaskForm): 35 | password = PasswordField('Enter new password', validators=[InputRequired()]) 36 | password2 = PasswordField( 37 | 'Repeat password', validators=[InputRequired(), EqualTo('password')]) 38 | submit = SubmitField('Submit') 39 | -------------------------------------------------------------------------------- /app/auth/routes.py: -------------------------------------------------------------------------------- 1 | from flask import render_template, redirect, url_for, flash, request, session 2 | from flask_login import current_user, login_user, logout_user, login_required 3 | from app import db, login_manager 4 | from app.auth import bp 5 | from app.auth.forms import LoginForm, RegistrationForm, ResetPasswordForm, ForgotPasswordForm 6 | from app.auth.email import send_password_reset_email 7 | from app.models import User 8 | from werkzeug.urls import url_parse 9 | 10 | ############################################################################## 11 | # Authentication blueprint 12 | ############################################################################## 13 | 14 | @bp.route('/login', methods=['GET','POST']) 15 | def login(): 16 | if current_user.is_authenticated: 17 | return redirect(url_for('main.index')) 18 | next_page = request.args.get('next') 19 | form=LoginForm() 20 | if form.validate_on_submit(): 21 | user = User.query.filter_by(email=form.email.data).first() 22 | if user is None or not user.check_password(form.password.data): 23 | flash('Invalid username or password','danger') 24 | return redirect(url_for('auth.login',next=next_page)) 25 | 26 | # username/password is valid. sets current_user to the user 27 | login_user(user, remember=form.remember_me.data) 28 | flash('You are now logged in.','success') 29 | 30 | # in case url is absolute we will ignore, we only want a relative url 31 | # netloc returns the www.website.com part 32 | if not next_page: 33 | return redirect(url_for('main.index')) 34 | return redirect(url_for(next_page)) 35 | 36 | return render_template('auth/login.html',form=form) 37 | 38 | 39 | @bp.route('/logout') 40 | @login_required 41 | def logout(): 42 | session.pop('edit_post',None) 43 | logout_user() 44 | flash('You are now logged out.','success') 45 | return redirect(url_for('main.index')) 46 | 47 | 48 | @bp.route('/register', methods=['GET','POST']) 49 | def register(): 50 | if current_user.is_authenticated: 51 | return redirect(url_for('main.index')) 52 | form = RegistrationForm() 53 | if form.validate_on_submit(): 54 | user = User(email=form.email.data, firstname=form.firstname.data, \ 55 | lastname=form.lastname.data, ) 56 | user.set_password(form.password.data) 57 | db.session.add(user) 58 | db.session.commit() 59 | flash('Registration successful!','success') 60 | return redirect(url_for('auth.login')) 61 | return render_template('auth/register.html', form=form) 62 | 63 | 64 | # User to enter email address to send forgot password link to 65 | @bp.route('/forgot_password',methods=['GET','POST']) 66 | def forgot_password(): 67 | if current_user.is_authenticated: 68 | return redirect(url_for('main.index')) 69 | form = ForgotPasswordForm() 70 | if form.validate_on_submit(): 71 | user = User.query.filter_by(email=form.email.data).first() 72 | if user: 73 | if send_password_reset_email(user): 74 | flash('Check your email for instructions to reset your password','success') 75 | else: 76 | flash('Sorry system error','danger') 77 | else: 78 | flash('That email does not exist in our database','danger') 79 | return redirect(url_for('auth.forgot_password')) 80 | return render_template('auth/forgot_password.html',form=form) 81 | 82 | 83 | # Allow users to create new password 84 | @bp.route('/reset_password/', methods=['GET', 'POST']) 85 | def reset_password(token): 86 | if current_user.is_authenticated: 87 | return redirect(url_for('main.index')) 88 | user = User.verify_reset_password_token(token) 89 | if not user: 90 | flash('Token has expired or is no longer valid','danger') 91 | return redirect(url_for('main.index')) 92 | form = ResetPasswordForm() 93 | if form.validate_on_submit(): 94 | user.set_password(form.password.data) 95 | db.session.commit() 96 | flash('Your password has been reset','success') 97 | return redirect(url_for('main.index')) 98 | return render_template('auth/reset_password.html', form=form) 99 | 100 | 101 | # handler when you are trying to access a page but you are not logged in 102 | @login_manager.unauthorized_handler 103 | def unauthorized(): 104 | flash('You must be logged in to view this page.','danger') 105 | return redirect(url_for('auth.login',next=request.endpoint)) 106 | -------------------------------------------------------------------------------- /app/email.py: -------------------------------------------------------------------------------- 1 | from flask import render_template, current_app 2 | from sendgrid import SendGridAPIClient 3 | from sendgrid.helpers.mail import Mail, Content, Personalization, Email 4 | 5 | ############################################################################## 6 | # Sendgrid 7 | ############################################################################## 8 | 9 | def send_email(subject, sender, recipients, text_body, html_body): 10 | message = Mail( 11 | from_email=sender, 12 | # to_emails=recipients, 13 | subject=subject, 14 | html_content=Content('text/html',html_body)) 15 | txt_content=Content('text/txt',text_body) 16 | message.add_content(txt_content) 17 | 18 | #for personalization so you can't see other people sent email 19 | for r in recipients: 20 | person = Personalization() 21 | person.add_to(Email(r)) 22 | message.add_personalization(person) 23 | 24 | try: 25 | sg = SendGridAPIClient(current_app.config['SENDGRID_API_KEY']) 26 | response = sg.send(message) 27 | print(response.status_code) 28 | print(response.body) 29 | print(response.headers) 30 | return True 31 | except Exception as e: 32 | print(e) 33 | return False 34 | -------------------------------------------------------------------------------- /app/errors/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | bp = Blueprint('errors', __name__) 4 | 5 | from app.errors import errors 6 | -------------------------------------------------------------------------------- /app/errors/errors.py: -------------------------------------------------------------------------------- 1 | from flask import render_template 2 | from . import bp 3 | 4 | # 404 - Not Found and 500 - Internal server error are errors generated by Flask 5 | # So we give these errors a nice template 6 | @bp.app_errorhandler(404) 7 | def page_not_found(e): 8 | return render_template('errors/404.html'), 404 9 | 10 | @bp.app_errorhandler(500) 11 | def internal_server_error(e): 12 | return render_template('errors/500.html'), 500 13 | -------------------------------------------------------------------------------- /app/main/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | bp = Blueprint('main', __name__) 4 | 5 | from app.main import routes 6 | -------------------------------------------------------------------------------- /app/main/email.py: -------------------------------------------------------------------------------- 1 | from app.email import send_email 2 | from flask import render_template, current_app 3 | 4 | def send_contact_email(contact): 5 | create_date = contact.create_date.strftime('%d/%m/%y %H:%M') 6 | return send_email('You have a new message from kevin7.net via the contact form', 7 | sender=current_app.config['MAIL_FROM'], 8 | recipients=current_app.config['MAIL_ADMINS'], 9 | text_body=render_template('main/email_contact.txt', 10 | contact=contact, create_date=create_date), 11 | html_body=render_template('main/email_contact.html', 12 | contact=contact, create_date=create_date)) 13 | 14 | def send_comment_email(post, comment): 15 | create_date = comment.create_date.strftime('%d/%m/%y %H:%M') 16 | return send_email('Someone has made a comment on kevin7.net', 17 | sender=current_app.config['MAIL_FROM'], 18 | recipients=current_app.config['MAIL_ADMINS'], 19 | text_body=render_template('main/email_comment.txt', 20 | comment=comment, post=post, create_date=create_date), 21 | html_body=render_template('main/email_comment.html', 22 | comment=comment, post=post, create_date=create_date)) 23 | -------------------------------------------------------------------------------- /app/main/forms.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm, RecaptchaField 2 | from wtforms import StringField, SubmitField, TextAreaField 3 | from wtforms.validators import Email, InputRequired, Length 4 | 5 | 6 | class ContactForm(FlaskForm): 7 | name = StringField('Please enter your details', validators=[InputRequired(), Length(max=20)]) 8 | email = StringField('Email', validators=[InputRequired(), Email(), Length(max=50)]) 9 | message = TextAreaField('Say something', validators=[InputRequired(), Length(max=1000)]) 10 | submit = SubmitField('Submit') 11 | recaptcha = RecaptchaField() 12 | -------------------------------------------------------------------------------- /app/post/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | bp = Blueprint('post', __name__) 4 | 5 | from app.post import routes 6 | -------------------------------------------------------------------------------- /app/post/forms.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm, RecaptchaField 2 | from wtforms import StringField, SubmitField, TextAreaField 3 | from wtforms.validators import InputRequired, Email, Length, Optional 4 | 5 | 6 | class PostForm(FlaskForm): 7 | heading = StringField('Title', validators=[InputRequired(), Length(max=100)]) 8 | post = TextAreaField('Write something') 9 | tags = StringField('Tags') 10 | submit = SubmitField('Submit') 11 | 12 | 13 | # Two forms for commenting, one if user is not logged in 14 | class CommentFormAnon(FlaskForm): 15 | name = StringField('Name', validators=[InputRequired(), Length(max=20)]) 16 | email = StringField('Email', validators=[Length(max=50), Email()]) 17 | comment = TextAreaField('Comment', validators=[InputRequired(), Length(max=1000)]) 18 | submit = SubmitField('Submit') 19 | recaptcha = RecaptchaField() 20 | 21 | # Another form if user is logged in 22 | class CommentFormReg(FlaskForm): 23 | comment = TextAreaField('Comment', validators=[InputRequired(), Length(max=1000)]) 24 | submit = SubmitField('Submit') 25 | recaptcha = RecaptchaField() 26 | 27 | class DeletePostForm(FlaskForm): 28 | submit = SubmitField('Delete') 29 | -------------------------------------------------------------------------------- /app/search/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | bp = Blueprint('search', __name__) 4 | 5 | from app.search import routes 6 | -------------------------------------------------------------------------------- /app/search/routes.py: -------------------------------------------------------------------------------- 1 | from flask import render_template, request, current_app 2 | from app.search import bp 3 | from operator import itemgetter 4 | from app.models import Post 5 | from sqlalchemy import or_, and_ 6 | 7 | ############################################################################## 8 | # Search blueprint 9 | ############################################################################## 10 | 11 | @bp.route('/search', methods=['POST']) 12 | def search(): 13 | # get page number from url. If no page number use page 1 14 | page = request.args.get('page', 1, type=int) 15 | if request.method == 'POST': 16 | search_str = request.form['search_txt'] 17 | '''posts = Post.query.filter(Post.current==True). \ 18 | filter(Post.post.like('%'+search_str+'%') | \ 19 | Post.heading.like('%'+search_str+'%')).all() 20 | ''' 21 | posts = Post.query.filter(and_(Post.current==True), 22 | (or_(Post.post.like('%'+search_str+'%'), 23 | Post.heading.like('%'+search_str+'%')))).all() 24 | 25 | results = [] 26 | # create a list of dictionaries of posts and number of occurrence of search string 27 | for p in posts: 28 | # only keep if search text is in content not tags 29 | if p.is_txtinHTML(search_str) or search_str.lower() in p.heading.lower(): 30 | # add to results 31 | d = {"post":p,"count":p.occurrences(search_str)} 32 | results.append(d) 33 | # now sort the results and only keep the first n elements 34 | results = sorted(results, key=itemgetter('count'), reverse=True)[:current_app.config['SEARCH_RESULTS_RETURN']] 35 | 36 | return render_template('search/search.html',results=results) 37 | -------------------------------------------------------------------------------- /app/sitemap/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | bp = Blueprint('sitemap', __name__) 4 | 5 | from app.sitemap import routes 6 | -------------------------------------------------------------------------------- /app/sitemap/routes.py: -------------------------------------------------------------------------------- 1 | from flask import render_template, current_app, make_response, url_for 2 | from app.sitemap import bp 3 | from datetime import datetime, timedelta 4 | from app.models import Post 5 | from os import listdir 6 | 7 | 8 | @bp.route('/sitemap.xml', methods=['GET']) 9 | def sitemap(): 10 | pages = [] 11 | 12 | # get static routes 13 | # use arbitary 10 days ago as last modified date 14 | lastmod = datetime.now() - timedelta(days=10) 15 | lastmod = lastmod.strftime('%Y-%m-%d') 16 | for rule in current_app.url_map.iter_rules(): 17 | # omit auth and admin routes and if route has parameters. Only include if route has GET method 18 | if 'GET' in rule.methods and len(rule.arguments) == 0 \ 19 | and not rule.rule.startswith('/admin') \ 20 | and not rule.rule.startswith('/auth') \ 21 | and not rule.rule.startswith('/test'): 22 | pages.append(['https://www.kevin7.net' + rule.rule, lastmod]) 23 | 24 | # get dynamic routes 25 | posts = Post.query.filter(Post.current.is_(True)).all() 26 | for post in posts: 27 | url = 'https://www.kevin7.net' + url_for('main.post_detail', slug=post.slug) 28 | last_updated = post.update_date.strftime('%Y-%m-%d') 29 | pages.append([url, last_updated]) 30 | 31 | # get Processing files 32 | files = [f[:-3] for f in listdir(current_app.config['PROCESSING_FOLDER'])] 33 | for f in files: 34 | pages.append(['https://www.kevin7.net/processing/' + f, last_updated]) 35 | 36 | sitemap_template = render_template('sitemap/sitemap_template.xml', pages=pages) 37 | response = make_response(sitemap_template) 38 | response.headers['Content-Type'] = 'application/xml' 39 | return response 40 | -------------------------------------------------------------------------------- /app/special/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | bp = Blueprint('special', __name__) 4 | 5 | from app.special import routes 6 | -------------------------------------------------------------------------------- /app/special/routes.py: -------------------------------------------------------------------------------- 1 | from app import db 2 | from app.models import Content, Page 3 | from flask import render_template, flash, redirect, url_for 4 | from flask_login import current_user, login_required 5 | from app.special import bp 6 | 7 | 8 | ############################################################################## 9 | # Special blueprint 10 | ############################################################################## 11 | 12 | @bp.route('/read', methods=['GET']) 13 | @login_required 14 | def about(): 15 | if current_user.role.name != 'admin' and current_user.role.name != 'special': 16 | flash("You are not authorised to access this page",'danger') 17 | return redirect(url_for('main.index')) 18 | special_html = db.session.query(Content).join(Page).filter(Page.name=='special',Content.name=='content1').first() 19 | return render_template('special/read.html',special_html=special_html) -------------------------------------------------------------------------------- /app/static/ads.txt: -------------------------------------------------------------------------------- 1 | google.com, pub-3176153830149274, DIRECT, f08c47fec0942fa0 -------------------------------------------------------------------------------- /app/static/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kevinf7/blog/859b58d2828b91b4400c730d52493b3a1f14e95d/app/static/android-chrome-192x192.png -------------------------------------------------------------------------------- /app/static/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kevinf7/blog/859b58d2828b91b4400c730d52493b3a1f14e95d/app/static/android-chrome-512x512.png -------------------------------------------------------------------------------- /app/static/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kevinf7/blog/859b58d2828b91b4400c730d52493b3a1f14e95d/app/static/apple-touch-icon.png -------------------------------------------------------------------------------- /app/static/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kevinf7/blog/859b58d2828b91b4400c730d52493b3a1f14e95d/app/static/favicon-16x16.png -------------------------------------------------------------------------------- /app/static/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kevinf7/blog/859b58d2828b91b4400c730d52493b3a1f14e95d/app/static/favicon-32x32.png -------------------------------------------------------------------------------- /app/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kevinf7/blog/859b58d2828b91b4400c730d52493b3a1f14e95d/app/static/favicon.ico -------------------------------------------------------------------------------- /app/static/images/bt/ball.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kevinf7/blog/859b58d2828b91b4400c730d52493b3a1f14e95d/app/static/images/bt/ball.jpg -------------------------------------------------------------------------------- /app/static/images/bt/bt-drop-in-placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kevinf7/blog/859b58d2828b91b4400c730d52493b3a1f14e95d/app/static/images/bt/bt-drop-in-placeholder.png -------------------------------------------------------------------------------- /app/static/images/bt/cards/visa.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | visa 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/static/images/bt/cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kevinf7/blog/859b58d2828b91b4400c730d52493b3a1f14e95d/app/static/images/bt/cat.jpg -------------------------------------------------------------------------------- /app/static/images/bt/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | check 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/static/images/bt/fail.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | check copy 5 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/static/images/bt/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kevinf7/blog/859b58d2828b91b4400c730d52493b3a1f14e95d/app/static/images/bt/favicon.png -------------------------------------------------------------------------------- /app/static/images/bt/flower.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kevinf7/blog/859b58d2828b91b4400c730d52493b3a1f14e95d/app/static/images/bt/flower.jpg -------------------------------------------------------------------------------- /app/static/images/bt/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kevinf7/blog/859b58d2828b91b4400c730d52493b3a1f14e95d/app/static/images/bt/loading.gif -------------------------------------------------------------------------------- /app/static/images/bt/pseudoshop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PSEUDOSHOP Copy 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | PS 12 | E 13 | U 14 | D 15 | O 16 | S 17 | H 18 | O 19 | P 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/static/images/bt/shopping-cart-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/static/images/bt/shopping-cart-solid0.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/static/images/bt/success.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | check copy 3 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/static/images/garden_smallx2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kevinf7/blog/859b58d2828b91b4400c730d52493b3a1f14e95d/app/static/images/garden_smallx2.png -------------------------------------------------------------------------------- /app/static/images/github-brands.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/static/images/linkedin-brands.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/static/images/robots_smallx2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kevinf7/blog/859b58d2828b91b4400c730d52493b3a1f14e95d/app/static/images/robots_smallx2.png -------------------------------------------------------------------------------- /app/static/images/snakegame_smallx2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kevinf7/blog/859b58d2828b91b4400c730d52493b3a1f14e95d/app/static/images/snakegame_smallx2.png -------------------------------------------------------------------------------- /app/static/images/stars_smallx2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kevinf7/blog/859b58d2828b91b4400c730d52493b3a1f14e95d/app/static/images/stars_smallx2.png -------------------------------------------------------------------------------- /app/static/images/sunmoon_smallx2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kevinf7/blog/859b58d2828b91b4400c730d52493b3a1f14e95d/app/static/images/sunmoon_smallx2.png -------------------------------------------------------------------------------- /app/static/images/wrench-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/static/processing/garden.js: -------------------------------------------------------------------------------- 1 | let flowers = []; 2 | 3 | function setup() { 4 | createCanvas(windowWidth,windowHeight); 5 | } 6 | 7 | 8 | function draw() { 9 | background(175,203,222); 10 | noStroke(); 11 | fill(175,222,183); 12 | rect(0,height/4*3,width,height/4*3); 13 | for(let i=0;i height/4*3) { 20 | flowers.push(new Flower(mouseX, mouseY)); 21 | } 22 | } 23 | 24 | 25 | class Flower { 26 | constructor(x,y) { 27 | //stem 28 | this.steps = 100; 29 | this.count = 0; 30 | 31 | //flower 32 | this.growStem = true; 33 | this.petals = 8; 34 | this.stepAngle=TWO_PI/this.petals; 35 | this.motion=0; 36 | this.accel=random(-0.004, 0.004); 37 | this.cr=random(0,255); 38 | this.cg=random(0,255); 39 | this.cb=random(0,255); 40 | this.fradius=random(5,50); 41 | this.fshape=random(15,80); 42 | 43 | this.startX = x; 44 | this.startY = y; 45 | this.prevX = []; 46 | this.prevY = []; 47 | this.prevX[0] = x; 48 | this.prevY[0] = y; 49 | this.endX = x; 50 | this.endY = random(20,height/2); 51 | this.cx1 = random(x-1000,x+1000); 52 | this.cy1 = random(y+100,y+1000); 53 | this.cx2 = random(this.endX-1000,this.endX+1000); 54 | this.cy2 = random(this.endY-100, this.endY-1000); 55 | } 56 | 57 | display() { 58 | if (this.growStem) { 59 | //grow the stem 60 | this.t = this.count / this.steps; 61 | this.x = curvePoint(this.cx1, this.startX, this.endX, this.cx2, this.t); 62 | this.y = curvePoint(this.cy1, this.startY, this.endY, this.cy2, this.t); 63 | 64 | stroke("#5D3300"); 65 | noFill(); 66 | for(let i=0;ithis.steps-1) { 75 | this.growStem = false; 76 | } else { 77 | this.prevX[this.count] = this.x; 78 | this.prevY[this.count] = this.y; 79 | } 80 | } else { 81 | //draw the flower 82 | stroke("#5D3300"); 83 | noFill(); 84 | curve(this.cx1, this.cy1, this.startX, this.startY, this.endX, this.endY, this.cx2, this.cy2); 85 | 86 | push(); 87 | translate(this.endX,this.endY); 88 | noStroke(); 89 | fill(this.cr,this.cg,this.cb); 90 | 91 | beginShape(); 92 | for(let i=0;i<=this.petals;i++) { 93 | this.x = cos(this.stepAngle*i+this.motion)*this.fradius; 94 | this.y = sin(this.stepAngle*i+this.motion)*this.fradius; 95 | this.cx = cos((this.stepAngle*i)-(this.stepAngle/2)+this.motion)*this.fshape; 96 | this.cy = sin((this.stepAngle*i)-(this.stepAngle/2)+this.motion)*this.fshape; 97 | 98 | if(i==0) { 99 | vertex(this.x,this.y); 100 | } else { 101 | bezierVertex(this.cx,this.cy,this.cx,this.cy,this.x,this.y); 102 | } 103 | } 104 | endShape(CLOSE); 105 | this.motion+=this.accel; 106 | pop(); 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /app/static/processing/robots.js: -------------------------------------------------------------------------------- 1 | var rbod = []; 2 | var rhead = []; 3 | var rarms = []; 4 | var numrobots_x; 5 | var numrobots_y; 6 | 7 | function setup() { 8 | createCanvas(windowWidth,windowHeight); 9 | 10 | numrobots_x = int(width/80); 11 | numrobots_y = int(height/80); 12 | counter=0; 13 | for(let i=0;ithis.num_seg) { 90 | return this.x[0]; 91 | } else { 92 | return this.x[seg-1]; 93 | } 94 | } 95 | 96 | get_y(seg) { 97 | if (seg<1||seg>this.num_seg) { 98 | return this.y[0]; 99 | } else { 100 | return this.y[seg-1]; 101 | } 102 | } 103 | 104 | get_h(seg) { 105 | if (seg<1||seg>this.num_seg) { 106 | return this.h[0]; 107 | } else { 108 | return this.h[seg-1]; 109 | } 110 | } 111 | 112 | get_w(seg) { 113 | if (seg<1||seg>this.num_seg) { 114 | return this.w[0]; 115 | } else { 116 | return this.w[seg-1]; 117 | } 118 | } 119 | 120 | get_num_seg() { 121 | return this.num_seg; 122 | } 123 | } 124 | 125 | class robotHead { 126 | 127 | constructor(rbod) { 128 | this.x=rbod.get_x(0); 129 | this.y=rbod.get_y(0)-rbod.get_h(0); 130 | this.w=rbod.get_w(0)*0.7; 131 | this.h=rbod.get_h(0); 132 | this.rotate=random(-6,6); 133 | 134 | fill(202,205,175); 135 | stroke(0); 136 | rectMode(CENTER); 137 | } 138 | 139 | display() { 140 | push(); 141 | translate(this.x,this.y); 142 | rotate(radians(this.rotate)); 143 | rect(0,0,this.w,this.h,16); 144 | fill(0); 145 | ellipse(-this.w/4,-this.h/4,3,3); 146 | ellipse(this.w/4,-this.h/4,3,3); 147 | line(0,-this.h/2,-this.w/6,-this.h); 148 | line(0,-this.h/2,this.w/6,-this.h); 149 | fill(202,205,175); 150 | pop(); 151 | } 152 | } 153 | 154 | class robotArms { 155 | 156 | constructor(rbod) { 157 | this.w = rbod.get_w(0); 158 | this.left_x=rbod.get_x(0)-this.w/2; 159 | this.right_x=rbod.get_x(0)+this.w/2; 160 | this.left_y=rbod.get_y(0); 161 | this.right_y=rbod.get_y(0); 162 | 163 | this.jleft_x = this.left_x - 20 + random(-4,4); 164 | this.jleft_y = this.left_y + 30 + random(-6,6); 165 | this.jright_x = this.right_x + 20 + random(-4,4); 166 | this.jright_y = this.right_y + 30 + random(-6,6); 167 | 168 | this.fleft_x = this.jleft_x - 6 + random(-2,2); 169 | this.fleft_y = (this.jleft_y + 40 + random(-4,4)); 170 | this.fright_x = this.jright_x + 6 + random(-2,2); 171 | this.fright_y = (this.jright_y + 40 + random(-4,4)) ; 172 | } 173 | 174 | display() { 175 | line(this.left_x,this.left_y,this.jleft_x,this.jleft_y); 176 | line(this.jleft_x,this.jleft_y,this.fleft_x,this.fleft_y); 177 | 178 | line(this.right_x,this.right_y,this.jright_x,this.jright_y); 179 | line(this.jright_x,this.jright_y,this.fright_x,this.fright_y); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /app/static/processing/stars.js: -------------------------------------------------------------------------------- 1 | var NUM_STARS; 2 | var stars = []; 3 | var speed; 4 | 5 | function setup() { 6 | NUM_STARS=2000; 7 | createCanvas(windowWidth,windowHeight); 8 | for (var i=0; i=this.distspin){ 110 | this.startspin=true; 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /app/static/scripts/app.js: -------------------------------------------------------------------------------- 1 | // Function for tabbing 2 | function openTab(evt, tabName) { 3 | var i, tabcontent, tablinks; 4 | 5 | // Get all elements with class="tabcontent" and hide them 6 | tabcontent = document.getElementsByClassName("tabcontent"); 7 | for (i = 0; i < tabcontent.length; i++) { 8 | tabcontent[i].style.display = "none"; 9 | } 10 | 11 | // Get all elements with class="tablinks" and remove the class "active" 12 | tablinks = document.getElementsByClassName("tablinks"); 13 | for (i = 0; i < tablinks.length; i++) { 14 | tablinks[i].className = tablinks[i].className.replace(" active", ""); 15 | } 16 | 17 | // Show the current tab, and add an "active" class to the button that opened the tab 18 | document.getElementById(tabName).style.display = "block"; 19 | evt.currentTarget.className += " active"; 20 | } 21 | 22 | 23 | // Function to copy image gallery link to clipboard 24 | function copyText(id) { 25 | var txtid = "copyclip-" + id; 26 | var copyText = document.getElementById(txtid); 27 | 28 | // Create dummy textare 29 | var temp = document.createElement("textarea"); 30 | document.body.appendChild(temp); 31 | temp.value= copyText.innerHTML 32 | temp.select(); 33 | 34 | try { 35 | document.execCommand('copy'); 36 | } catch (err) { 37 | console.log('Copy to clipboard was unsuccessful'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/static/scripts/bulmanav.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', () => { 2 | 3 | // Get all "navbar-burger" elements 4 | const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0); 5 | 6 | // Check if there are any navbar burgers 7 | if ($navbarBurgers.length > 0) { 8 | 9 | // Add a click event on each of them 10 | $navbarBurgers.forEach( el => { 11 | el.addEventListener('click', () => { 12 | 13 | // Get the target from the "data-target" attribute 14 | const target = el.dataset.target; 15 | const $target = document.getElementById(target); 16 | 17 | // Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu" 18 | el.classList.toggle('is-active'); 19 | $target.classList.toggle('is-active'); 20 | 21 | }); 22 | }); 23 | } 24 | 25 | }); 26 | -------------------------------------------------------------------------------- /app/static/scripts/test.js: -------------------------------------------------------------------------------- 1 | var name = document.getElementById("zap").getAttribute("data-test"); 2 | 3 | new Vue({ 4 | el: '#this-block', 5 | delimiters: ['[[', ']]'], 6 | data() { 7 | return { 8 | output: "Hello World from Vue!! " + name 9 | } 10 | }, 11 | methods: { 12 | doTest() { 13 | this.output = 'ayah!!' 14 | } 15 | } 16 | }) 17 | 18 | 19 | /* 20 | 25 | */ -------------------------------------------------------------------------------- /app/static/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /app/static/styles/app.css: -------------------------------------------------------------------------------- 1 | .site { 2 | display: flex; 3 | min-height: 100vh; 4 | flex-direction: column; 5 | } 6 | .site-main { 7 | flex: 1 0 auto; 8 | } 9 | .site-title { 10 | font-family: 'Merriweather Sans', sans-serif; 11 | } 12 | .footer a { 13 | color: #F4F4F4; 14 | } 15 | .footer a:hover { 16 | color: #0E0E0E; 17 | } 18 | .normal-links a { 19 | color: #1F57BC; 20 | } 21 | .normal-links a:hover { 22 | color: #00217B; 23 | } 24 | .content { 25 | overflow-wrap: break-word; 26 | } 27 | .tag_num { 28 | background-color: #A8D4D4; 29 | border-radius: 3px; 30 | padding: 0px 3px 0px 3px; 31 | } 32 | @media only screen and (min-width: 769px) { 33 | .add-gap { 34 | padding-left: 1.8rem; 35 | } 36 | } 37 | 38 | 39 | /*fixes Prism Bulma bug */ 40 | pre { 41 | background-color: #002b36; 42 | } 43 | pre code { 44 | background: unset; 45 | color: unset; 46 | } 47 | pre code .tag, pre code .number, pre code .label { 48 | display: inline; 49 | padding: inherit; 50 | font-size: inherit; 51 | line-height: inherit; 52 | text-align: inherit; 53 | vertical-align: inherit; 54 | border-radius: inherit; 55 | font-weight: inherit; 56 | white-space: inherit; 57 | background: inherit; 58 | margin: inherit; 59 | } 60 | /* make the font size in prism smaller */ 61 | code[class*="language-"], 62 | pre[class*="language-"] { 63 | font-size: 0.9em; 64 | line-height: 1.3; 65 | } 66 | -------------------------------------------------------------------------------- /app/static/styles/prism.css: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.20.0 2 | https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+css+clike+javascript+processing+python+regex+scss+sql */ 3 | /** 4 | * prism.js tomorrow night eighties for JavaScript, CoffeeScript, CSS and HTML 5 | * Based on https://github.com/chriskempson/tomorrow-theme 6 | * @author Rose Pritchard 7 | */ 8 | 9 | code[class*="language-"], 10 | pre[class*="language-"] { 11 | color: #ccc; 12 | background: none; 13 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 14 | font-size: 1em; 15 | text-align: left; 16 | white-space: pre; 17 | word-spacing: normal; 18 | word-break: normal; 19 | word-wrap: normal; 20 | line-height: 1.5; 21 | 22 | -moz-tab-size: 4; 23 | -o-tab-size: 4; 24 | tab-size: 4; 25 | 26 | -webkit-hyphens: none; 27 | -moz-hyphens: none; 28 | -ms-hyphens: none; 29 | hyphens: none; 30 | 31 | } 32 | 33 | /* Code blocks */ 34 | pre[class*="language-"] { 35 | padding: 1em; 36 | margin: .5em 0; 37 | overflow: auto; 38 | } 39 | 40 | :not(pre) > code[class*="language-"], 41 | pre[class*="language-"] { 42 | background: #2d2d2d; 43 | } 44 | 45 | /* Inline code */ 46 | :not(pre) > code[class*="language-"] { 47 | padding: .1em; 48 | border-radius: .3em; 49 | white-space: normal; 50 | } 51 | 52 | .token.comment, 53 | .token.block-comment, 54 | .token.prolog, 55 | .token.doctype, 56 | .token.cdata { 57 | color: #999; 58 | } 59 | 60 | .token.punctuation { 61 | color: #ccc; 62 | } 63 | 64 | .token.tag, 65 | .token.attr-name, 66 | .token.namespace, 67 | .token.deleted { 68 | color: #e2777a; 69 | } 70 | 71 | .token.function-name { 72 | color: #6196cc; 73 | } 74 | 75 | .token.boolean, 76 | .token.number, 77 | .token.function { 78 | color: #f08d49; 79 | } 80 | 81 | .token.property, 82 | .token.class-name, 83 | .token.constant, 84 | .token.symbol { 85 | color: #f8c555; 86 | } 87 | 88 | .token.selector, 89 | .token.important, 90 | .token.atrule, 91 | .token.keyword, 92 | .token.builtin { 93 | color: #cc99cd; 94 | } 95 | 96 | .token.string, 97 | .token.char, 98 | .token.attr-value, 99 | .token.regex, 100 | .token.variable { 101 | color: #7ec699; 102 | } 103 | 104 | .token.operator, 105 | .token.entity, 106 | .token.url { 107 | color: #67cdcc; 108 | } 109 | 110 | .token.important, 111 | .token.bold { 112 | font-weight: bold; 113 | } 114 | .token.italic { 115 | font-style: italic; 116 | } 117 | 118 | .token.entity { 119 | cursor: help; 120 | } 121 | 122 | .token.inserted { 123 | color: green; 124 | } 125 | 126 | -------------------------------------------------------------------------------- /app/static/tinymce/langs/readme.md: -------------------------------------------------------------------------------- 1 | This is where language files should be placed. 2 | 3 | Please DO NOT translate these directly use this service: https://www.transifex.com/projects/p/tinymce/ 4 | -------------------------------------------------------------------------------- /app/static/tinymce/plugins/advlist/plugin.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Tiny Technologies, Inc. All rights reserved. 3 | * Licensed under the LGPL or a commercial license. 4 | * For LGPL see License.txt in the project root for license information. 5 | * For commercial licenses see https://www.tiny.cloud/ 6 | * 7 | * Version: 5.0.3 (2019-03-19) 8 | */ 9 | !function(){"use strict";var n,t,e,r,u=tinymce.util.Tools.resolve("tinymce.PluginManager"),v=tinymce.util.Tools.resolve("tinymce.util.Tools"),O=function(n,t,e){var r="UL"===t?"InsertUnorderedList":"InsertOrderedList";n.execCommand(r,!1,!1===e?null:{"list-style-type":e})},i=function(e){e.addCommand("ApplyUnorderedListStyle",function(n,t){O(e,"UL",t["list-style-type"])}),e.addCommand("ApplyOrderedListStyle",function(n,t){O(e,"OL",t["list-style-type"])})},o=function(n){var t=n.getParam("advlist_number_styles","default,lower-alpha,lower-greek,lower-roman,upper-alpha,upper-roman");return t?t.split(/[ ,]/):[]},l=function(n){var t=n.getParam("advlist_bullet_styles","default,circle,square");return t?t.split(/[ ,]/):[]},c=function(n){return function(){return n}},s=c(!1),f=c(!0),a=s,d=f,g=function(){return p},p=(r={fold:function(n,t){return n()},is:a,isSome:a,isNone:d,getOr:e=function(n){return n},getOrThunk:t=function(n){return n()},getOrDie:function(n){throw new Error(n||"error: getOrDie called on none.")},getOrNull:function(){return null},getOrUndefined:function(){return undefined},or:e,orThunk:t,map:g,ap:g,each:function(){},bind:g,flatten:g,exists:a,forall:d,filter:g,equals:n=function(n){return n.isNone()},equals_:n,toArray:function(){return[]},toString:c("none()")},Object.freeze&&Object.freeze(r),r),m=function(e){var n=function(){return e},t=function(){return u},r=function(n){return n(e)},u={fold:function(n,t){return t(e)},is:function(n){return e===n},isSome:d,isNone:a,getOr:n,getOrThunk:n,getOrDie:n,getOrNull:n,getOrUndefined:n,or:t,orThunk:t,map:function(n){return m(n(e))},ap:function(n){return n.fold(g,function(n){return m(n(e))})},each:function(n){n(e)},bind:r,flatten:n,exists:r,forall:r,filter:function(n){return n(e)?u:p},equals:function(n){return n.is(e)},equals_:function(n,t){return n.fold(a,function(n){return t(e,n)})},toArray:function(){return[e]},toString:function(){return"some("+e+")"}};return u},y=function(n){return null===n||n===undefined?p:m(n)},h=function(n){return n&&/^(TH|TD)$/.test(n.nodeName)},L=function(r){return function(n){return n&&/^(OL|UL|DL)$/.test(n.nodeName)&&(e=n,(t=r).$.contains(t.getBody(),e));var t,e}},b=function(n){var t=n.dom.getParent(n.selection.getNode(),"ol,ul"),e=n.dom.getStyle(t,"listStyleType");return y(e)},S=function(n,t,e){var r=function(n,t){for(var e=0;ed(e)&&(i=o+g);var l=h(e);l&&l]*>((\xa0| |[ \t]|]*>)+?|)|
$","i").test(e)},l=function(t){var e=parseInt(i.getItem(f(t)+"time"),10)||0;return!((new Date).getTime()-e>s(t.settings.autosave_retention,"20m")&&(m(t,!1),1))},m=function(t,e){var r=f(t);i.removeItem(r+"draft"),i.removeItem(r+"time"),!1!==e&&t.fire("RemoveDraft")},v=function(t){var e=f(t);!c(t)&&t.isDirty()&&(i.setItem(e+"draft",t.getContent({format:"raw",no_events:!0})),i.setItem(e+"time",(new Date).getTime().toString()),t.fire("StoreDraft"))},d=function(t){var e=f(t);l(t)&&(t.setContent(i.getItem(e+"draft"),{format:"raw"}),t.fire("RestoreDraft"))},g=function(t,e){var r=s(t.settings.autosave_interval,"30s");e.get()||(n.setInterval(function(){t.removed||v(t)},r),e.set(!0))},y=function(t){t.undoManager.transact(function(){d(t),m(t)}),t.focus()};function p(n){for(var o=[],t=1;t(.*?)<\/a>/gi,"[url=$1]$2[/url]"),o(/(.*?)<\/font>/gi,"[code][color=$1]$2[/color][/code]"),o(/(.*?)<\/font>/gi,"[quote][color=$1]$2[/color][/quote]"),o(/(.*?)<\/font>/gi,"[code][color=$1]$2[/color][/code]"),o(/(.*?)<\/font>/gi,"[quote][color=$1]$2[/color][/quote]"),o(/(.*?)<\/span>/gi,"[color=$1]$2[/color]"),o(/(.*?)<\/font>/gi,"[color=$1]$2[/color]"),o(/(.*?)<\/span>/gi,"[size=$1]$2[/size]"),o(/(.*?)<\/font>/gi,"$1"),o(//gi,"[img]$1[/img]"),o(/(.*?)<\/span>/gi,"[code]$1[/code]"),o(/(.*?)<\/span>/gi,"[quote]$1[/quote]"),o(/(.*?)<\/strong>/gi,"[code][b]$1[/b][/code]"),o(/(.*?)<\/strong>/gi,"[quote][b]$1[/b][/quote]"),o(/(.*?)<\/em>/gi,"[code][i]$1[/i][/code]"),o(/(.*?)<\/em>/gi,"[quote][i]$1[/i][/quote]"),o(/(.*?)<\/u>/gi,"[code][u]$1[/u][/code]"),o(/(.*?)<\/u>/gi,"[quote][u]$1[/u][/quote]"),o(/<\/(strong|b)>/gi,"[/b]"),o(/<(strong|b)>/gi,"[b]"),o(/<\/(em|i)>/gi,"[/i]"),o(/<(em|i)>/gi,"[i]"),o(/<\/u>/gi,"[/u]"),o(/(.*?)<\/span>/gi,"[u]$1[/u]"),o(//gi,"[u]"),o(/]*>/gi,"[quote]"),o(/<\/blockquote>/gi,"[/quote]"),o(/
/gi,"\n"),o(//gi,"\n"),o(/
/gi,"\n"),o(/

/gi,""),o(/<\/p>/gi,"\n"),o(/ |\u00a0/gi," "),o(/"/gi,'"'),o(/</gi,"<"),o(/>/gi,">"),o(/&/gi,"&"),e},i=function(e){e=t.trim(e);var o=function(o,t){e=e.replace(o,t)};return o(/\n/gi,"
"),o(/\[b\]/gi,""),o(/\[\/b\]/gi,""),o(/\[i\]/gi,""),o(/\[\/i\]/gi,""),o(/\[u\]/gi,""),o(/\[\/u\]/gi,""),o(/\[url=([^\]]+)\](.*?)\[\/url\]/gi,'$2'),o(/\[url\](.*?)\[\/url\]/gi,'$1'),o(/\[img\](.*?)\[\/img\]/gi,''),o(/\[color=(.*?)\](.*?)\[\/color\]/gi,'$2'),o(/\[code\](.*?)\[\/code\]/gi,'$1 '),o(/\[quote.*?\](.*?)\[\/quote\]/gi,'$1 '),e};o.add("bbcode",function(){return{init:function(o){o.on("beforeSetContent",function(o){o.content=i(o.content)}),o.on("postProcess",function(o){o.set&&(o.content=i(o.content)),o.get&&(o.content=e(o.content))})}}}),function n(){}}(); -------------------------------------------------------------------------------- /app/static/tinymce/plugins/code/plugin.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Tiny Technologies, Inc. All rights reserved. 3 | * Licensed under the LGPL or a commercial license. 4 | * For LGPL see License.txt in the project root for license information. 5 | * For commercial licenses see https://www.tiny.cloud/ 6 | * 7 | * Version: 5.0.3 (2019-03-19) 8 | */ 9 | !function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),t=function(e,n){e.focus(),e.undoManager.transact(function(){e.setContent(n)}),e.selection.setCursorLocation(),e.nodeChanged()},o=function(e){return e.getContent({source_view:!0})},n=function(n){var e=o(n);n.windowManager.open({title:"Source Code",size:"large",body:{type:"panel",items:[{type:"textarea",name:"code"}]},buttons:[{type:"cancel",name:"cancel",text:"Cancel"},{type:"submit",name:"save",text:"Save",primary:!0}],initialData:{code:e},onSubmit:function(e){t(n,e.getData().code),e.close()}})},c=function(e){e.addCommand("mceCodeEditor",function(){n(e)})},i=function(e){e.ui.registry.addButton("code",{icon:"sourcecode",tooltip:"Source code",onAction:function(){return n(e)}}),e.ui.registry.addMenuItem("code",{icon:"sourcecode",text:"Source code",onAction:function(){return n(e)}})};e.add("code",function(e){return c(e),i(e),{}}),function u(){}}(); -------------------------------------------------------------------------------- /app/static/tinymce/plugins/colorpicker/plugin.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Tiny Technologies, Inc. All rights reserved. 3 | * Licensed under the LGPL or a commercial license. 4 | * For LGPL see License.txt in the project root for license information. 5 | * For commercial licenses see https://www.tiny.cloud/ 6 | * 7 | * Version: 5.0.3 (2019-03-19) 8 | */ 9 | !function(o){"use strict";tinymce.util.Tools.resolve("tinymce.PluginManager").add("colorpicker",function(){o.console.warn("Color picker plugin is now built in to the core editor, please remove it from your editor configuration")}),function i(){}}(window); -------------------------------------------------------------------------------- /app/static/tinymce/plugins/contextmenu/plugin.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Tiny Technologies, Inc. All rights reserved. 3 | * Licensed under the LGPL or a commercial license. 4 | * For LGPL see License.txt in the project root for license information. 5 | * For commercial licenses see https://www.tiny.cloud/ 6 | * 7 | * Version: 5.0.3 (2019-03-19) 8 | */ 9 | !function(n){"use strict";tinymce.util.Tools.resolve("tinymce.PluginManager").add("contextmenu",function(){n.console.warn("Context menu plugin is now built in to the core editor, please remove it from your editor configuration")}),function o(){}}(window); -------------------------------------------------------------------------------- /app/static/tinymce/plugins/directionality/plugin.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Tiny Technologies, Inc. All rights reserved. 3 | * Licensed under the LGPL or a commercial license. 4 | * For LGPL see License.txt in the project root for license information. 5 | * For commercial licenses see https://www.tiny.cloud/ 6 | * 7 | * Version: 5.0.3 (2019-03-19) 8 | */ 9 | !function(c){"use strict";var n,t,e,r,o,i,u=tinymce.util.Tools.resolve("tinymce.PluginManager"),f=tinymce.util.Tools.resolve("tinymce.util.Tools"),d=function(n,t){var e,r=n.dom,o=n.selection.getSelectedBlocks();o.length&&(e=r.getAttrib(o[0],"dir"),f.each(o,function(n){r.getParent(n.parentNode,'*[dir="'+t+'"]',r.getRoot())||r.setAttrib(n,"dir",e!==t?t:null)}),n.nodeChanged())},l=function(n){n.addCommand("mceDirectionLTR",function(){d(n,"ltr")}),n.addCommand("mceDirectionRTL",function(){d(n,"rtl")})},a=function(n){return function(){return n}},m=a(!1),N=a(!0),s=m,T=N,g=function(){return E},E=(r={fold:function(n,t){return n()},is:s,isSome:s,isNone:T,getOr:e=function(n){return n},getOrThunk:t=function(n){return n()},getOrDie:function(n){throw new Error(n||"error: getOrDie called on none.")},getOrNull:function(){return null},getOrUndefined:function(){return undefined},or:e,orThunk:t,map:g,ap:g,each:function(){},bind:g,flatten:g,exists:s,forall:T,filter:g,equals:n=function(n){return n.isNone()},equals_:n,toArray:function(){return[]},toString:a("none()")},Object.freeze&&Object.freeze(r),r),O=function(e){var n=function(){return e},t=function(){return o},r=function(n){return n(e)},o={fold:function(n,t){return t(e)},is:function(n){return e===n},isSome:T,isNone:s,getOr:n,getOrThunk:n,getOrDie:n,getOrNull:n,getOrUndefined:n,or:t,orThunk:t,map:function(n){return O(n(e))},ap:function(n){return n.fold(g,function(n){return O(n(e))})},each:function(n){n(e)},bind:r,flatten:n,exists:r,forall:r,filter:function(n){return n(e)?o:E},equals:function(n){return n.is(e)},equals_:function(n,t){return n.fold(s,function(n){return t(e,n)})},toArray:function(){return[e]},toString:function(){return"some("+e+")"}};return o},y=function(n){return null===n||n===undefined?E:O(n)},D=function(n){if(null===n||n===undefined)throw new Error("Node cannot be null or undefined");return{dom:a(n)}},p={fromHtml:function(n,t){var e=(t||c.document).createElement("div");if(e.innerHTML=n,!e.hasChildNodes()||1")})},t=function(n){n.ui.registry.addButton("hr",{icon:"horizontal-rule",tooltip:"Horizontal line",onAction:function(){return n.execCommand("InsertHorizontalRule")}}),n.ui.registry.addMenuItem("hr",{icon:"horizontal-rule",text:"Horizontal line",onAction:function(){return n.execCommand("InsertHorizontalRule")}})};n.add("hr",function(n){o(n),t(n)}),function e(){}}(); -------------------------------------------------------------------------------- /app/static/tinymce/plugins/importcss/plugin.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Tiny Technologies, Inc. All rights reserved. 3 | * Licensed under the LGPL or a commercial license. 4 | * For LGPL see License.txt in the project root for license information. 5 | * For commercial licenses see https://www.tiny.cloud/ 6 | * 7 | * Version: 5.0.3 (2019-03-19) 8 | */ 9 | !function(){"use strict";var t,e,n,r,i,o=tinymce.util.Tools.resolve("tinymce.PluginManager"),d=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),f=tinymce.util.Tools.resolve("tinymce.EditorManager"),m=tinymce.util.Tools.resolve("tinymce.Env"),v=tinymce.util.Tools.resolve("tinymce.util.Tools"),c=function(t){return t.getParam("importcss_merge_classes")},u=function(t){return t.getParam("importcss_exclusive")},h=function(t){return t.getParam("importcss_selector_converter")},l=function(t){return t.getParam("importcss_selector_filter")},p=function(t){return t.getParam("importcss_groups")},_=function(t){return t.getParam("importcss_append")},O=function(t){return t.getParam("importcss_file_filter")},s=function(t){return function(){return t}},a=s(!1),y=s(!0),g=function(){return x},x=(r={fold:function(t,e){return t()},is:a,isSome:a,isNone:y,getOr:n=function(t){return t},getOrThunk:e=function(t){return t()},getOrDie:function(t){throw new Error(t||"error: getOrDie called on none.")},getOrNull:function(){return null},getOrUndefined:function(){return undefined},or:n,orThunk:e,map:g,ap:g,each:function(){},bind:g,flatten:g,exists:a,forall:y,filter:g,equals:t=function(t){return t.isNone()},equals_:t,toArray:function(){return[]},toString:s("none()")},Object.freeze&&Object.freeze(r),r),T=(i="function",function(t){return function(t){if(null===t)return"null";var e=typeof t;return"object"===e&&Array.prototype.isPrototypeOf(t)?"array":"object"===e&&String.prototype.isPrototypeOf(t)?"string":e}(t)===i}),b=Array.prototype.push,k=function(t,e){return function(t){for(var e=[],n=0,r=t.length;n'+n+"")}else e.insertContent(f(e,t));var i,o,u,c,m},g=f,y=function(e){e.addCommand("mceInsertDate",function(){p(e,t(e))}),e.addCommand("mceInsertTime",function(){p(e,a(e))})},M=tinymce.util.Tools.resolve("tinymce.util.Tools"),S=function(e){var t=e,n=function(){return t};return{get:n,set:function(e){t=e},clone:function(){return S(n())}}},v=function(n){var t=i(n),r=S(o(n));n.ui.registry.addSplitButton("insertdatetime",{icon:"insert-time",tooltip:"Insert date/time",select:function(e){return e===r.get()},fetch:function(e){e(M.map(t,function(e){return{type:"choiceitem",text:g(n,e),value:e}}))},onAction:function(){for(var e=[],t=0;t ':" ";n.insertContent(function(n,e){for(var o="",t=0;t"===r){var a=o.lastIndexOf("<",e);if(-1!==a&&-1!==o.substring(a,e).indexOf('contenteditable="false"'))return t}return''+i.dom.encode("string"==typeof n[1]?n[1]:n[0])+""}},n=function(n){var t,e,r="contenteditable";t=" "+c.trim(u(n))+" ",e=" "+c.trim(l(n))+" ";var a=s(t),i=s(e),o=f(n);n.on("PreInit",function(){0'},o=function(o){var c=a(o),n=new RegExp(c.replace(/[\?\.\*\[\]\(\)\{\}\+\^\$\:]/g,function(e){return"\\"+e}),"gi");o.on("BeforeSetContent",function(e){e.content=e.content.replace(n,r())}),o.on("PreInit",function(){o.serializer.addNodeFilter("img",function(e){for(var n,a,t=e.length;t--;)if((a=(n=e[t]).attr("class"))&&-1!==a.indexOf("mce-pagebreak")){var r=n.parent;if(o.schema.getBlockElements()[r.name]&&i(o)){r.type=3,r.value=c,r.raw=!0,n.remove();continue}n.type=3,n.value=c,n.raw=!0}})})},c=r,u=t,g=function(e){e.addCommand("mcePageBreak",function(){e.settings.pagebreak_split_block?e.insertContent("

"+c()+"

"):e.insertContent(c())})},m=function(n){n.on("ResolveName",function(e){"IMG"===e.target.nodeName&&n.dom.hasClass(e.target,u())&&(e.name="pagebreak")})},s=function(e){e.ui.registry.addButton("pagebreak",{icon:"page-break",tooltip:"Page break",onAction:function(){return e.execCommand("mcePageBreak")}}),e.ui.registry.addMenuItem("pagebreak",{text:"Page break",icon:"page-break",onAction:function(){return e.execCommand("mcePageBreak")}})};e.add("pagebreak",function(e){g(e),s(e),o(e),m(e)}),function l(){}}(); -------------------------------------------------------------------------------- /app/static/tinymce/plugins/preview/plugin.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Tiny Technologies, Inc. All rights reserved. 3 | * Licensed under the LGPL or a commercial license. 4 | * For LGPL see License.txt in the project root for license information. 5 | * For commercial licenses see https://www.tiny.cloud/ 6 | * 7 | * Version: 5.0.3 (2019-03-19) 8 | */ 9 | !function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),c=tinymce.util.Tools.resolve("tinymce.util.Tools"),s=function(e){return e.getParam("content_style","")},i=function(t){var n="",i=t.dom.encode,e=s(t);n+='',e&&(n+='"),c.each(t.contentCSS,function(e){n+=''});var o=t.settings.body_id||"tinymce";-1!==o.indexOf("=")&&(o=(o=t.getParam("body_id","","hash"))[t.id]||o);var r=t.settings.body_class||"";-1!==r.indexOf("=")&&(r=(r=t.getParam("body_class","","hash"))[t.id]||"");var a=t.settings.directionality?' dir="'+t.settings.directionality+'"':"";return""+n+'"+t.getContent()+' 84 | {% endblock %} 85 | -------------------------------------------------------------------------------- /app/templates/admin_message/manage_message.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Admin | Manage Messages{% endblock %} 3 | 4 | {% block app_content %} 5 |
6 |
7 |

Manage Messages

8 |

Here are all the messages you have received via the contact form.

9 | 10 | {% for msg in contacts.items %} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | {% endfor %} 29 |
Message on {{ moment(msg.create_date).format('DD/MM/YY LT') }}
From:{{ msg.name }}
Email:{{ msg.email }}
Message:{{ msg.message }}
30 |
31 |
32 | 33 | {% if contacts.pages > 1 %} 34 |
35 |
36 | 63 |
64 |
65 | {% endif %} 66 | 67 | {% endblock %} 68 | -------------------------------------------------------------------------------- /app/templates/admin_tag/del_tag.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Admin | Delete Tag{% endblock %} 3 | 4 | {% block app_content %} 5 |
6 |
7 |

Delete Tag

8 | 9 |
10 | {{ form.csrf_token }} 11 |

Are you sure you want to delete the tag 12 | {{ tag.name }}? 13 |

14 |
15 |
16 | {{ form.submit(class_="button is-link") }} 17 |
18 |
19 |
20 |
21 |
22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /app/templates/admin_tag/edit_tag.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Admin | Edit Tag{% endblock %} 3 | 4 | {% block app_content %} 5 |
6 |
7 |

Edit Tag

8 | 9 |
10 | {{ form.csrf_token }} 11 |
12 | {{ form.tag_name.label(class_="label") }} 13 |

14 | {% set t = form.tag_name.process_data(tag.name) %} 15 | {{ form.tag_name(class_="input") }} 16 |

17 | {% for error in form.tag_name.errors %} 18 |

{{ error }}

19 | {% endfor %} 20 |
21 | 22 |
23 |
24 | {{ form.submit(class_="button is-link") }} 25 |
26 |
27 |
28 |
29 |
30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /app/templates/admin_tag/manage_tags.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Admin | Manage Tags{% endblock %} 3 | 4 | {% block app_content %} 5 |
6 |
7 |
8 |

Manage Tags

9 |
10 | 11 |
12 | 24 |
25 | 26 |
27 |

To delete these tags, first remove them from all blog postings

28 | 29 | {% if tag_used|length > 0 %} 30 | 31 | 32 | 33 | 34 | 35 | 36 | {% endif %} 37 | 38 | {% for tu in tag_used %} 39 | 40 | 41 | 42 | 43 | {% endfor %} 44 | 45 |
Name of TagAction
{{ tu.name }}edit
46 |
47 | 48 | 49 | {% if tag_notused|length > 0 %} 50 | 51 | 52 | 53 | 54 | 55 | 56 | {% endif %} 57 | 58 | {% for tnu in tag_notused %} 59 | 60 | 61 | 63 | 64 | {% endfor %} 65 | 66 |
Name of TagAction
{{ tnu.name }}edit 62 | delete
67 | 68 |
69 |
70 | {% endblock %} 71 | 72 | {% block scripts %} 73 | 81 | {% endblock %} 82 | -------------------------------------------------------------------------------- /app/templates/auth/email_reset_password.html: -------------------------------------------------------------------------------- 1 |

Dear {{ user.firstname }}

2 |

3 | To reset your password 4 | 5 | click here 6 | . 7 |

8 |

Alternatively, you can paste the following link in your browser's address bar:

9 |

{{ url_for('auth.reset_password', token=token, _external=True) }}

10 |

If you have not requested a password reset simply ignore this message.

11 |

Regards

12 |

Kevin
13 | (kevin7.net) 14 |

15 | -------------------------------------------------------------------------------- /app/templates/auth/email_reset_password.txt: -------------------------------------------------------------------------------- 1 | Dear {{ user.firstname }} 2 | 3 | To reset your password click on the following link: 4 | 5 | {{ url_for('auth.reset_password', token=token, _external=True) }} 6 | 7 | If you have not requested a password reset simply ignore this message. 8 | 9 | Regards 10 | 11 | Kevin 12 | (kevin7.net) 13 | -------------------------------------------------------------------------------- /app/templates/auth/forgot_password.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Forgot Password{% endblock %} 3 | 4 | {% block app_content %} 5 |
6 |
7 |
8 |
9 |

Forgot Password

10 |
11 | {{ form.csrf_token }} 12 |
13 | {{ form.email.label(class_="label") }} 14 |

15 | {{ form.email(class_="input") }} 16 |

17 | {% for error in form.email.errors %} 18 |

{{ error }}

19 | {% endfor %} 20 |
21 | 22 |
23 |
24 | {{ form.submit(class_="button is-link") }} 25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | {% endblock %} 33 | -------------------------------------------------------------------------------- /app/templates/auth/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Login{% endblock %} 3 | 4 | {% block app_content %} 5 |
6 | 7 |
8 |
9 |
10 |

Login

11 |
12 |
13 | {{ form.csrf_token }} 14 |
15 | {{ form.email.label(class_='label') }} 16 |

17 | {{ form.email(class_='input') }} 18 | 19 | 20 | 21 |

22 | {% for error in form.email.errors %} 23 |

{{ error }} ]

24 | {% endfor %} 25 |
26 | 27 |
28 | {{ form.password.label(class_='label') }} 29 |

30 | {{ form.password(class_='input') }} 31 | 32 | 33 | 34 |

35 | {% for error in form.password.errors %} 36 |

{{ error }} ]

37 | {% endfor %} 38 |
39 | 40 |
41 |
42 | 46 |
47 |
48 | 49 |
50 |
51 | {{ form.submit(class_="button is-link") }} 52 |
53 |
54 |
55 |

56 | Forgot Password | 57 | Register 58 |

59 |
60 |
61 |
62 | 63 | {% endblock %} 64 | -------------------------------------------------------------------------------- /app/templates/auth/register.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Register{% endblock %} 3 | 4 | {% block app_content %} 5 |
6 |
7 |

Register

8 |
9 |
10 | {{ form.csrf_token }} 11 | 12 |
13 |
14 |
15 | {{ form.email.label(class_='label') }} 16 |

17 | {{ form.email(class_='input') }} 18 | 19 | 20 | 21 |

22 | {% for error in form.email.errors %} 23 |

{{ error }} ]

24 | {% endfor %} 25 |

Your email will be your username

26 |
27 |
28 |
29 | 30 |
31 |
32 |
33 | {{ form.firstname.label(class_='label') }} 34 |

35 | {{ form.firstname(class_='input') }} 36 |

37 | {% for error in form.firstname.errors %} 38 |

{{ error }} ]

39 | {% endfor %} 40 |
41 |
42 |
43 |
44 | {{ form.lastname.label(class_='label') }} 45 |

46 | {{ form.lastname(class_='input') }} 47 |

48 | {% for error in form.lastname.errors %} 49 |

{{ error }} ]

50 | {% endfor %} 51 |
52 |
53 |
54 | 55 |
56 |
57 |
58 | {{ form.password.label(class_='label') }} 59 |

60 | {{ form.password(class_='input') }} 61 |

62 | {% for error in form.password.errors %} 63 |

{{ error }} ]

64 | {% endfor %} 65 |
66 |
67 |
68 |
69 | {{ form.password2.label(class_='label') }} 70 |

71 | {{ form.password2(class_='input') }} 72 |

73 | {% for error in form.password2.errors %} 74 |

{{ error }} ]

75 | {% endfor %} 76 |
77 |
78 |
79 | 80 |
81 |

82 | {{ form.recaptcha }} 83 |

84 | {% for error in form.recaptcha.errors %} 85 |

{{ error }} ]

86 | {% endfor %} 87 |
88 | 89 |
90 |
91 | {{ form.submit(class_="button is-link") }} 92 |
93 |
94 | 95 |
96 |
97 | {% endblock %} 98 | -------------------------------------------------------------------------------- /app/templates/auth/reset_password.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Reset Password{% endblock %} 3 | 4 | {% block app_content %} 5 |
6 |
7 |
8 |
9 |

Reset Password

10 |
11 | {{ form.csrf_token }} 12 |
13 | {{ form.password.label(class_="label") }} 14 |

15 | {{ form.password(class_="input") }} 16 |

17 | {% for error in form.password.errors %} 18 |

{{ error }}

19 | {% endfor %} 20 |
21 | 22 |
23 | {{ form.password2.label(class_="label") }} 24 |

25 | {{ form.password2(class_="input") }} 26 |

27 | {% for error in form.password2.errors %} 28 |

{{ error }}

29 | {% endfor %} 30 |
31 | 32 |
33 |
34 | {{ form.submit(class_="button is-link") }} 35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | {% endblock %} 43 | -------------------------------------------------------------------------------- /app/templates/errors/403.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}403 Error{% endblock %} 3 | 4 | {% block app_content %} 5 |
6 |
7 |

403 forbidden error

8 |

Back

9 |
10 |
11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /app/templates/errors/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}404 Error{% endblock %} 3 | 4 | {% block app_content %} 5 |
6 |
7 |

404 not found error

8 |

Back

9 |
10 |
11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /app/templates/errors/500.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}500 Error{% endblock %} 3 | 4 | {% block app_content %} 5 |
6 |
7 |

500 internal server error

8 |

Back

9 |
10 |
11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /app/templates/main/about.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}About Me{% endblock %} 3 | 4 | {% block app_content %} 5 |
6 | 9 |
10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /app/templates/main/contact.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Contact Me{% endblock %} 3 | 4 | {% block app_content %} 5 |
6 | 85 |
86 | {% endblock %} 87 | -------------------------------------------------------------------------------- /app/templates/main/email_comment.html: -------------------------------------------------------------------------------- 1 |

Someone has commented on one of your post on kevin7.net

2 | {% if current_user.is_authenticated %} 3 |

From: {{ current_user.firstname }} {{ current_user.lastname }}

4 |

Email: {{ current_user.email }}

5 | {% else %} 6 |

From: {{ comment.name }}

7 |

Email: {{ comment.email }}

8 | {% endif %} 9 |

Date: {{ create_date }}

10 |

Post:

11 | {{ post.id }} - {{ post.heading }} 12 |

Message:

13 | {{ comment.comment }} 14 | -------------------------------------------------------------------------------- /app/templates/main/email_comment.txt: -------------------------------------------------------------------------------- 1 | Someone has commented on one of your post on kevin7.net 2 | 3 | {% if current_user.is_authenticated %} 4 | From: {{ current_user.firstname }} {{ current_user.lastname }} 5 | 6 | Email: {{ current_user.email }} 7 | {% else %} 8 | From: {{ comment.name }} 9 | 10 | Email: {{ comment.email }} 11 | {% endif %} 12 | 13 | Date: {{ create_date }} 14 | 15 | Post: 16 | {{ post.id }} - {{ post.heading }} 17 | 18 | Message: 19 | {{ comment.comment }} 20 | -------------------------------------------------------------------------------- /app/templates/main/email_contact.html: -------------------------------------------------------------------------------- 1 |

You have received a message submitted from the contact form on kevin7.net

2 |

From: {{ contact.name }}

3 |

Email: {{ contact.email }}

4 |

Date: {{ create_date }}

5 |

Message:

6 | {{ contact.message }} 7 | -------------------------------------------------------------------------------- /app/templates/main/email_contact.txt: -------------------------------------------------------------------------------- 1 | You have received a message submitted from the contact form on kevin7.net 2 | 3 | From: {{ contact.name }} 4 | 5 | Email: {{ contact.email }} 6 | 7 | Date: {{ create_date }} 8 | 9 | Message: 10 | {{ contact.message }} 11 | -------------------------------------------------------------------------------- /app/templates/main/index bak.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Kevin Foong | Web & App Developer{% endblock %} 3 | 4 | {% block app_content %} 5 |
6 |
7 | 8 | {% for post in posts.items %} 9 |
10 |
11 |

{{ post.heading }}

12 |

Posted on {{ moment(post.create_date).format('DD/MM/YYYY') }} by {{ post.author.firstname }} {{ post.author.lastname }}

13 | 16 | {% if post.getTagNames() %} 17 |

Tags: 18 | {% for tag_name in post.getTagNames() %} 19 | 20 | 21 | {{ tag_name }} 22 | 23 | 24 | {% endfor %} 25 |

26 | {% endif %} 27 | {% if current_user.is_authenticated and current_user == post.author %} 28 | 29 | edit 30 | 31 | 32 | delete 33 | 34 | {% endif %} 35 |
36 |
37 | {% endfor %} 38 | 39 | {% if posts.pages > 1 %} 40 |
41 |
42 | 69 |
70 |
71 | {% endif %} 72 | 73 |
74 |
75 |
76 |
77 |

Tags

78 | {% for tag_tup in tag_list %} 79 | 80 | 81 | {{ tag_tup[0].name }} ({{ tag_tup[1] }}) 82 | 83 | 84 | {% endfor %} 85 |
86 |
87 | 88 |
89 |
90 |

Play

91 |
92 |

Sketches created using Processing.. enjoy!

93 | 94 | 95 | 96 | 97 | 98 |
99 |
100 |
101 |
102 |
103 | {% endblock %} 104 | -------------------------------------------------------------------------------- /app/templates/main/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Kevin Foong | Web & App Developer{% endblock %} 3 | 4 | {% block app_content %} 5 |
6 |
7 |
8 | {% for post in posts.items %} 9 |
10 |

{{ post.heading }}

11 |

Posted on {{ moment(post.create_date).format('DD/MM/YYYY') }} by {{ post.author.firstname }} {{ post.author.lastname }}

12 | 15 | {% if post.getTagNames() %} 16 |

Tags: 17 | {% for tag_name in post.getTagNames() %} 18 | 19 | 20 | {{ tag_name }} 21 | 22 | 23 | {% endfor %} 24 |

25 | {% endif %} 26 | {% if current_user.is_authenticated and current_user == post.author %} 27 | 28 | edit 29 | 30 | 31 | delete 32 | 33 | {% endif %} 34 |
35 | {% endfor %} 36 | 37 | {% if posts.pages > 1 %} 38 | 77 | {% endif %} 78 |
79 | 80 |
81 | 82 |
83 |
84 |

Tags

85 |
86 | {% for tag_tup in tag_list %} 87 | 88 | 89 | {{ tag_tup[0].name }} {{ tag_tup[1] }} 90 | 91 | 92 | {% endfor %} 93 |
94 |
95 | 96 |
97 |

Play

98 |
99 |

Sketches I created using Processing. Enjoy!

100 | 101 | 102 | 103 | 104 | 105 |
106 |
107 |
108 | 109 |
110 |
111 |
112 | {% endblock %} 113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /app/templates/main/post_det.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}{{ post.heading }}{% endblock %} 3 | 4 | {% block app_content %} 5 |
6 |
7 | 12 | 13 | {% if post.getTagNames() %} 14 |

Tags: 15 | {% for tag_name in post.getTagNames() %} 16 | 17 | 18 | {{ tag_name }} 19 | 20 | 21 | {% endfor %} 22 |

23 | {% endif %} 24 | {% if current_user.is_authenticated and current_user == post.author %} 25 | 26 | edit 27 | 28 | 29 | delete 30 | 31 | {% endif %} 32 | 33 |
34 |
35 | 36 |
37 |
38 |

Comments

39 |
40 | 41 | 42 | {% for comment in comments %} 43 | 55 | {% endfor %} 56 |
44 | {% if comment.name is not none %} 45 |

{{ comment.name }} 46 | on {{ moment(comment.create_date).format('DD/MM/YYYY LT') }}

47 | {% else %} 48 | {% if comment.commenter is not none %} 49 |

{{ comment.commenter.firstname }} {{ comment.commenter.lastname }} 50 | on {{ moment(comment.create_date).format('DD/MM/YYYY LT') }}

51 | {% endif %} 52 | {% endif %} 53 |

{{ comment.comment }}

54 |
57 | 58 |
59 |

Leave a Comment

60 |
61 |
62 | {{ form.csrf_token }} 63 | 64 | {% if current_user.is_anonymous %} 65 |
66 | {{ form.name.label(class_='label') }} 67 |
68 | {{ form.name(class_='input') }} 69 |
70 | {% for error in form.name.errors %} 71 |

{{ error }}

72 | {% endfor %} 73 |
74 | 75 |
76 | {{ form.email.label(class_='label') }} 77 |
78 | {{ form.email(class_='input') }} 79 |
80 | {% for error in form.email.errors %} 81 |

{{ error }}

82 | {% endfor %} 83 |

Email is optional

84 |
85 | {% endif %} 86 | 87 |
88 | {{ form.comment.label(class_='label') }} 89 |
90 | {{ form.comment(rows='4', class_='textarea') }} 91 |
92 | {% for error in form.comment.errors %} 93 |

{{ error }}

94 | {% endfor %} 95 |
96 | 97 |
98 |
99 | {{ form.recaptcha }} 100 | {% for error in form.recaptcha.errors %} 101 |

{{ error }}

102 | {% endfor %} 103 |
104 |
105 | 106 |
107 |
108 | {{ form.submit(class_="button is-link") }} 109 |
110 |
111 |
112 |
113 | 114 | {% endblock %} 115 | -------------------------------------------------------------------------------- /app/templates/main/processing.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ script_name }} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/templates/main/projects.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Projects{% endblock %} 3 | 4 | {% block app_content %} 5 |
6 |
7 | {{ projects_html.content|safe }} 8 |
9 |
10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /app/templates/post/_tinymce.html: -------------------------------------------------------------------------------- 1 | 2 | 28 | -------------------------------------------------------------------------------- /app/templates/post/add_post.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Admin | Add Post{% endblock %} 3 | 4 | {% block app_content %} 5 |
6 |
7 |

Add Post

8 |
9 | {{ form.csrf_token }} 10 |
11 | {{ form.heading.label(class_='label') }} 12 |

13 | {{ form.heading(class_="input") }} 14 |

15 | {% for error in form.heading.errors %} 16 |

{{ error }} ]

17 | {% endfor %} 18 |
19 | 20 |
21 | {{ form.post.label(class_='label') }} 22 |

23 | Hint: Add a summary break by adding <p>br<a id="br"></a></p> 24 |

25 |

26 | {{ form.post(rows=16) }} 27 |

28 | {% for error in form.post.errors %} 29 |

{{ error }} ]

30 | {% endfor %} 31 |
32 | 33 |
34 | {{ form.tags.label(class_='label') }} 35 |

Hint: Separate tags by , (comma)

36 |

37 | {{ form.tags(class_="input") }} 38 |

39 | {% for error in form.tags.errors %} 40 |

{{ error }} ]

41 | {% endfor %} 42 |
43 | 44 |
45 |
46 | {{ form.submit(class_="button is-link") }} 47 |
48 |
49 |
50 |
51 |
52 | {% endblock %} 53 | 54 | {% block scripts %} 55 | {{ super() }} 56 | {% include 'post/_tinymce.html' %} 57 | {% endblock %} 58 | -------------------------------------------------------------------------------- /app/templates/post/del_post.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Admin | Delete Post{% endblock %} 3 | 4 | {% block app_content %} 5 |
6 |
7 |

Delete Post

8 |

Are you sure you want to delete this post?

9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 31 | 32 | 33 | 34 | 35 | 36 |
FieldValue
Author: {{ post.author.firstname }} {{ post.author.lastname }}
Created: {{ moment(post.create_date).format('DD/MM/YYYY') }}
Title:{{ post.heading | safe }}
28 |

Content:

29 |

{{ post.post | safe }}

30 |
Tags:{{ tags }}
37 |
38 | 39 |
40 | {{ form.csrf_token }} 41 |
42 |
43 | {{ form.submit(class_="button is-link") }} 44 |
45 |
46 |
47 |
48 | {% endblock %} 49 | -------------------------------------------------------------------------------- /app/templates/post/edit_post.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Admin | Edit Post{% endblock %} 3 | 4 | {% block app_content %} 5 |
6 | 54 |
55 | {% endblock %} 56 | 57 | {% block scripts %} 58 | {{ super() }} 59 | {% include 'post/_tinymce.html' %} 60 | {% endblock %} 61 | -------------------------------------------------------------------------------- /app/templates/search/search.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Search Results{% endblock %} 3 | 4 | {% block app_content %} 5 |
6 |
7 |

Search Results

8 | {% if not results %} 9 |

No results returned

10 | {% else %} 11 | {% for r in results %} 12 |

{{ r['post'].heading }}

13 |

{{ r['post'].getTextSummary() }}

14 |
Number of matches: {{ r['count'] }}
15 | {% endfor %} 16 | {% endif %} 17 |
18 |
19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /app/templates/sitemap/sitemap_template.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% for page in pages %} 4 | 5 | {{ page[0]|safe }} 6 | {{ page[1] }} 7 | 8 | {% endfor %} 9 | 10 | -------------------------------------------------------------------------------- /app/templates/special/read.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Read{% endblock %} 3 | 4 | {% block app_content %} 5 |
6 | 9 |
10 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/test/cart.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Testing only - payment{% endblock %} 3 | 4 | {% block app_content %} 5 |

Cart

6 | 7 |
8 | {% for d in data %} 9 |
10 | ${{ d['price'] }}
11 | Qty: 12 | 13 |

14 | {% endfor %} 15 |
16 | 17 |

Total number of items: {{ total_no_items }}

18 |

Total ${{ total }}

19 | 20 | Checkout 21 | 22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /app/templates/test/checkout.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Testing only - payment{% endblock %} 3 | 4 | {% block scripts %} 5 | 6 | {% endblock %} 7 | 8 | {% block app_content %} 9 |

Payment details

10 | 11 |

Drop in UI

12 |

Make a test payment with Braintree using PayPal or a card

13 | 14 |

15 | 16 |

Enter your address....

17 | 18 | {% for d in data %} 19 | {{ d['title'] }} Qty: {{ d['qty'] }} Price: ${{ d['price'] }}
20 | {% endfor %} 21 | {{ total_no_items }} number of items, total: {{ total }} 22 |

23 | 24 |
25 |
26 |
27 | 28 |
29 | 30 | 31 | 32 |
33 | 34 | 35 | 36 | 60 | 61 | 62 | {% endblock %} 63 | -------------------------------------------------------------------------------- /app/templates/test/rental.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Rental property generator{% endblock %} 3 | 4 | {% block app_content %} 5 |

Domain.com.au - Excel

6 | 7 |
8 | {{ form.csrf_token }} 9 | 10 | {{ form.min_bedrooms.label }} 11 | {% for error in form.min_bedrooms.errors %} 12 | [ {{ error }} ] 13 | {% endfor %} 14 | {{ form.min_bedrooms }}
15 | 16 | {{ form.max_bedrooms.label }} 17 | {% for error in form.max_bedrooms.errors %} 18 | [ {{ error }} ] 19 | {% endfor %} 20 | {{ form.max_bedrooms }}
21 | 22 | {{ form.max_price.label }} 23 | {% for error in form.max_price.errors %} 24 | [ {{ error }} ] 25 | {% endfor %} 26 | {{ form.max_price }}
27 | 28 | {{ form.postcode.label }} 29 | {% for error in form.postcode.errors %} 30 | [ {{ error }} ] 31 | {% endfor %} 32 | {{ form.postcode }}
33 | {{ form.submit }} 34 |
35 | 36 | {% endblock %} 37 | -------------------------------------------------------------------------------- /app/templates/test/rental_results.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Rental property generator - results{% endblock %} 3 | 4 | {% block app_content %} 5 |

Results

6 | 7 |

For this test shows up to 100 rental listings

8 | 9 |

Link to Excel (csv)

10 | 11 | {% for d in data %} 12 | http://www.domain.com.au/{{ d['listing']['listingSlug'] }}
13 | {% endfor %} 14 | 15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /app/templates/test/result_checkout.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Testing only - payment{% endblock %} 3 | 4 | {% block app_content %} 5 |

Braintree results

6 | 7 | 8 |

{{ result['header'] }}

9 |

{{ result['message'] }}

10 | 11 |

API Response

12 |
Transaction
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
id{{ transaction.id }}
type{{ transaction.type }}
amount{{ transaction.amount }}
status{{ transaction.status }}
created_at{{ transaction.created_at }}
updated_at{{ transaction.updated_at }}
41 | 42 |
Payment
43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 |
token{{ transaction.credit_card_details.token }}
bin{{ transaction.credit_card_details.bin }}
last_4{{ transaction.credit_card_details.last_4 }}
card_type{{ transaction.credit_card_details.card_type }}
expiration_date{{ transaction.credit_card_details.expiration_date }}
cardholder_name{{ transaction.credit_card_details.cardholder_name }}
customer_location{{ transaction.credit_card_details.customer_location }}
76 | 77 | {% if transaction.customer_details.id %} 78 |
79 |
Customer Details
80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 |
id{{ transaction.customer_details.id }}
first_name{{ transaction.customer_details.first_name }}
last_name{{ transaction.customer_details.last_name }}
email{{ transaction.customer_details.email }}
company{{ transaction.customer_details.company }}
website{{ transaction.customer_details.website }}
phone{{ transaction.customer_details.phone }}
fax{{ transaction.customer_details.fax }}
116 |
117 | {% endif %} 118 | 119 |
120 |

Integrate with the Braintree SDK for a secure and seamless checkout

121 |
122 | 123 |
124 | 125 | See the Docs 126 | 127 |
128 | 129 | {% endblock %} 130 | -------------------------------------------------------------------------------- /app/templates/test/shop.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Testing only - payment{% endblock %} 3 | 4 | {% block scripts %} 5 | 6 | 20 | {% endblock %} 21 | 22 | {% block app_content %} 23 |

Catalog

24 | 25 |

26 | 27 | 28 |

29 | 30 | {% for d in data %} 31 |
32 | ${{ d['price'] }}
33 | Add to cart

36 | {% endfor %} 37 | 38 | View cart 39 |
40 | Clear cart 41 | 42 | {% endblock %} 43 | -------------------------------------------------------------------------------- /app/templates/test/test_vue.html: -------------------------------------------------------------------------------- 1 |
2 |

Testing vue.js

3 |
4 |
10 | 11 |
12 |
13 |
14 | 15 | 27 | -------------------------------------------------------------------------------- /app/test/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | bp = Blueprint('test', __name__, static_folder='../../dist', 4 | static_url_path='/test') 5 | 6 | from app.test import payment, rental, test_vue 7 | -------------------------------------------------------------------------------- /app/test/forms.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import IntegerField, SubmitField 3 | from wtforms.validators import InputRequired 4 | 5 | 6 | class SearchForm(FlaskForm): 7 | min_bedrooms = IntegerField('Minimum Bedrooms', validators=[InputRequired()]) 8 | max_bedrooms = IntegerField('Maximum Bedrooms', validators=[InputRequired()]) 9 | max_price = IntegerField('Maximum Rental Price per week', validators=[InputRequired()]) 10 | postcode = IntegerField('Postcode', validators=[InputRequired()]) 11 | submit = SubmitField('Submit') 12 | -------------------------------------------------------------------------------- /app/test/gateway.py: -------------------------------------------------------------------------------- 1 | import braintree 2 | import os 3 | 4 | gateway = braintree.BraintreeGateway( 5 | braintree.Configuration( 6 | braintree.Environment.Sandbox, 7 | merchant_id=os.environ.get('BT_MERCHANT_ID'), 8 | public_key=os.environ.get('BT_PUBLIC_KEY'), 9 | private_key=os.environ.get('BT_PRIVATE_KEY') 10 | ) 11 | ) 12 | 13 | def generate_client_token(): 14 | return gateway.client_token.generate() 15 | 16 | def transact(options): 17 | return gateway.transaction.sale(options) 18 | 19 | def find_transaction(id): 20 | return gateway.transaction.find(id) 21 | -------------------------------------------------------------------------------- /app/test/payment.py: -------------------------------------------------------------------------------- 1 | from flask import render_template, redirect, url_for, request, jsonify, session 2 | from app.test import bp 3 | import braintree 4 | from app.test.gateway import generate_client_token, transact, find_transaction 5 | from flask_login import login_required 6 | 7 | 8 | # Card number: 4111 1111 1111 1111 9 | # Expiry: 09/20 10 | # CVV: 400 11 | # Postal Code: 40000 12 | 13 | 14 | dummy_db = [ 15 | {'id':'0','title':'Flower','image':'flower.jpg','price':12.99}, 16 | {'id':'1','title':'Cat','image':'cat.jpg','price':24.00}, 17 | {'id':'2','title':'Ball','image':'ball.jpg','price':19.15} 18 | ] 19 | 20 | 21 | TRANSACTION_SUCCESS_STATUSES = [ 22 | braintree.Transaction.Status.Authorized, 23 | braintree.Transaction.Status.Authorizing, 24 | braintree.Transaction.Status.Settled, 25 | braintree.Transaction.Status.SettlementConfirmed, 26 | braintree.Transaction.Status.SettlementPending, 27 | braintree.Transaction.Status.Settling, 28 | braintree.Transaction.Status.SubmittedForSettlement 29 | ] 30 | 31 | def get_item(id): 32 | for d in dummy_db: 33 | if d['id'] == id: 34 | return d 35 | return None 36 | 37 | 38 | def update_cart_qty(id,qty): 39 | for count, c in enumerate(session['cart']): 40 | if c['id'] == id: 41 | c['qty'] += 1 42 | session['cart'][count] = c 43 | session.modified = True 44 | return True 45 | return False 46 | 47 | 48 | def get_data(): 49 | data = [] 50 | for c in session['cart']: 51 | item = get_item(c['id']) 52 | if item: 53 | item['qty'] = c['qty'] 54 | data.append(item) 55 | return data 56 | 57 | 58 | def get_total(data): 59 | total = 0 60 | for d in data: 61 | total = total + (d['qty'] * d['price']) 62 | return round(total,2) 63 | 64 | 65 | def get_total_no_items(data): 66 | total_no_items = 0 67 | for d in data: 68 | total_no_items += d['qty'] 69 | return total_no_items 70 | 71 | 72 | @bp.route('/shop', methods=['GET']) 73 | @login_required 74 | def shop(): 75 | return render_template('test/shop.html', data=dummy_db) 76 | 77 | 78 | @bp.route('/cart', methods=['GET']) 79 | @login_required 80 | def cart(): 81 | if 'cart' not in session: 82 | session['cart'] = [] 83 | data = get_data() 84 | 85 | return render_template('test/cart.html', data=data, total=get_total(data), total_no_items=get_total_no_items(data)) 86 | 87 | 88 | # AJAX call to add item to cart 89 | @bp.route('/cart/add', methods=['POST']) 90 | @login_required 91 | def add_cart(): 92 | if 'cart' not in session: 93 | session['cart'] = [] 94 | id = request.form['id'] 95 | if not update_cart_qty(id,1): 96 | item = { 97 | 'id' : id, 98 | 'qty' : 1, 99 | } 100 | session['cart'].append(item) 101 | session.modified = True 102 | total_qty = 0 103 | for c in session['cart']: 104 | total_qty += c['qty'] 105 | 106 | return jsonify({'count': total_qty}) 107 | 108 | 109 | @bp.route('/cart/remove/', methods=['POST']) 110 | @login_required 111 | def remove_cart(id): 112 | # id in url is index of the session variable cart 113 | del session['cart'][int(id)] 114 | session.modified=True 115 | return redirect(url_for('test.cart')) 116 | 117 | 118 | @bp.route('/cart/update/', methods=['POST']) 119 | @login_required 120 | def update_cart(id): 121 | # id in url is index of the session variable cart 122 | item = session['cart'][int(id)] 123 | item['qty'] = int(request.form['qty'+id]) 124 | session['cart'][int(id)] = item 125 | session.modified=True 126 | return redirect(url_for('test.cart')) 127 | 128 | 129 | @bp.route('/clear_cart', methods=['GET']) 130 | @login_required 131 | def clear_cart(): 132 | session.pop('cart', None) 133 | return ('', 204) 134 | 135 | 136 | @bp.route('/checkout', methods=['GET']) 137 | @login_required 138 | def checkout(): 139 | client_token = generate_client_token() 140 | data = get_data() 141 | return render_template('test/checkout.html', client_token=client_token, data=data, total=get_total(data), total_no_items=get_total_no_items(data)) 142 | 143 | 144 | @bp.route('/checkout/result/', methods=['GET']) 145 | @login_required 146 | def result_checkout(transaction_id): 147 | transaction = find_transaction(transaction_id) 148 | result = {} 149 | if transaction.status in TRANSACTION_SUCCESS_STATUSES: 150 | result = { 151 | 'header': 'Sweet Success!', 152 | 'icon': 'success', 153 | 'message': 'Your test transaction has been successfully processed. See the Braintree API response and try again.' 154 | } 155 | else: 156 | result = { 157 | 'header': 'Transaction Failed', 158 | 'icon': 'fail', 159 | 'message': 'Your test transaction has a status of ' + transaction.status + '. See the Braintree API response and try again.' 160 | } 161 | return render_template('test/result_checkout.html', transaction=transaction, result=result) 162 | 163 | 164 | @bp.route('/checkout/create', methods=['POST']) 165 | @login_required 166 | def create_checkout(): 167 | result = transact({ 168 | 'amount': request.form['total_amt'], 169 | 'payment_method_nonce': request.form['payment_method_nonce'], 170 | 'options': { 171 | "submit_for_settlement": True 172 | } 173 | }) 174 | 175 | if result.is_success or result.transaction: 176 | return redirect(url_for('test.result_checkout',transaction_id=result.transaction.id)) 177 | else: 178 | for x in result.errors.deep_errors: 179 | flash('Error: %s: %s' % (x.code, x.message)) 180 | return redirect(url_for('test.checkout')) 181 | -------------------------------------------------------------------------------- /app/test/rental.py: -------------------------------------------------------------------------------- 1 | from flask import render_template, redirect, url_for, flash, request, current_app 2 | from app.test import bp 3 | from app.test.forms import SearchForm 4 | from authlib.client import OAuth2Session 5 | import pandas as pd 6 | 7 | 8 | # oauth2 domain 9 | scope='api_listings_read' 10 | domain = OAuth2Session(current_app.config['DOMAIN_CLIENT_ID'], current_app.config['DOMAIN_CLIENT_SECRET'], scope=scope) 11 | token = domain.fetch_access_token('https://auth.domain.com.au/v1/connect/token', grant_type='client_credentials') 12 | 13 | 14 | @bp.route('/rental', methods=['GET','POST']) 15 | def rental(): 16 | form=SearchForm() 17 | if form.validate_on_submit(): 18 | resp=domain.post('https://api.domain.com.au/v1/listings/residential/_search',\ 19 | json={\ 20 | 'page':1,\ 21 | 'pageSize':100,\ 22 | 'listingType':'Rent',\ 23 | 'minBedrooms':int(form.min_bedrooms.data),\ 24 | 'maxBedrooms':int(form.max_bedrooms.data),\ 25 | 'maxPrice':int(form.max_price.data),\ 26 | 'locations':[\ 27 | {\ 28 | 'postcode':form.postcode.data\ 29 | }\ 30 | ]\ 31 | }) 32 | data = [] 33 | json_resp = resp.json() 34 | for j in json_resp: 35 | data.append(j['listing']['listingSlug']) 36 | df = pd.DataFrame(data) 37 | df.to_csv(current_app.config['RENTAL_FOLDER'] / 'test.csv', index=False) 38 | return render_template('test/rental_results.html',data=resp.json()) 39 | return render_template('test/rental.html',form=form) 40 | -------------------------------------------------------------------------------- /app/test/test_vue.py: -------------------------------------------------------------------------------- 1 | from flask import render_template, request, jsonify 2 | from app.test import bp 3 | 4 | 5 | @bp.route('/test_vue', methods=['GET','POST']) 6 | def test_vue(): 7 | if request.method == 'POST': 8 | data = {'message': 'ok'} 9 | return jsonify(data) 10 | return render_template('test/test_vue.html') 11 | 12 | 13 | @bp.route('/') 14 | def index(): 15 | return bp.send_static_file('index.html') 16 | -------------------------------------------------------------------------------- /blog.py: -------------------------------------------------------------------------------- 1 | from app import create_app, db 2 | 3 | app = create_app() 4 | 5 | #when you run flask shell the model objects will be instantiated for you 6 | with app.app_context(): 7 | #it seems you need to wrap it inside with app.app_context otherwise current_app is null? 8 | from app.models import User, Role, Tagged, Post, Tag, Comment, Contact, Images, Content, Page 9 | @app.shell_context_processor 10 | def make_shell_context(): 11 | return {'db': db, 'User': User, 'Post': Post, 'Tagged': Tagged, 'Role': Role, 12 | 'Tag': Tag, 'Comment' : Comment, 'Contact' : Contact, 'Images' : Images, 13 | 'Content' : Content, 'Page' : Page} 14 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | from pathlib import Path 4 | 5 | basedir = Path(__file__).parent 6 | load_dotenv(basedir / '.env') 7 | 8 | 9 | class Config(object): 10 | # Flask settings 11 | SECRET_KEY = os.environ.get('SECRET_KEY') or 'random string' 12 | 13 | # Flask-SQLAlchemy settings 14 | SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') 15 | # recommended by Pythonanywhere otherwise you get mysql timeout 16 | SQLALCHEMY_POOL_RECYCLE = 299 17 | SQLALCHEMY_TRACK_MODIFICATIONS = False 18 | # number of blog posts to show per page 19 | POSTS_PER_PAGE = 6 20 | # auto reload template without needing to restart Flask 21 | TEMPLATES_AUTO_RELOAD = True 22 | 23 | # tinyMCE settings 24 | UPLOADED_PATH = basedir / 'app/static/uploads' 25 | UPLOADED_PATH_THUMB = basedir / 'app/static/uploads/thumbnails' 26 | 27 | # Sendgrid settings 28 | SENDGRID_API_KEY = os.environ.get('SENDGRID_API_KEY') 29 | MAIL_FROM = os.environ.get('MAIL_FROM') 30 | MAIL_ADMINS = os.environ.get('MAIL_ADMINS').split(' ') 31 | 32 | # Google recaptcha 33 | RECAPTCHA_USE_SSL = False 34 | RECAPTCHA_PUBLIC_KEY = os.environ.get('RECAPTCHA_PUBLIC_KEY') 35 | RECAPTCHA_PRIVATE_KEY = os.environ.get('RECAPTCHA_PRIVATE_KEY') 36 | RECAPTCHA_OPTIONS = {'theme': 'black'} 37 | 38 | # Processing folder 39 | PROCESSING_FOLDER = basedir / 'app/static/processing' 40 | 41 | # Rental folder 42 | RENTAL_FOLDER = basedir / 'app/static/rental' 43 | 44 | # custom app settings 45 | FORGOT_PASSWORD_TOKEN_EXPIRE = 3600 # in seconds, 3600 = 1 hour 46 | SEARCH_RESULTS_RETURN = 12 47 | # admin number of messages 48 | MESSAGES_PER_PAGE = 10 49 | # admin number of images 50 | IMAGES_PER_PAGE = 12 51 | 52 | # Dev only so browser doesnt cache for CSS 53 | if (os.environ.get('FLASK_ENV') == 'development'): 54 | SEND_FILE_MAX_AGE_DEFAULT = 0 55 | 56 | DOMAIN_CLIENT_ID = os.environ.get('DOMAIN_CLIENT_ID') 57 | DOMAIN_CLIENT_SECRET = os.environ.get('DOMAIN_CLIENT_SECRET') 58 | 59 | # Banned keywords in comments 60 | BANNED_LIST = os.environ.get('BANNED_LIST').split(' ') 61 | -------------------------------------------------------------------------------- /migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /migrations/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | # file_template = %%(rev)s_%%(slug)s 6 | 7 | # set to 'true' to run the environment during 8 | # the 'revision' command, regardless of autogenerate 9 | # revision_environment = false 10 | 11 | 12 | # Logging configuration 13 | [loggers] 14 | keys = root,sqlalchemy,alembic 15 | 16 | [handlers] 17 | keys = console 18 | 19 | [formatters] 20 | keys = generic 21 | 22 | [logger_root] 23 | level = WARN 24 | handlers = console 25 | qualname = 26 | 27 | [logger_sqlalchemy] 28 | level = WARN 29 | handlers = 30 | qualname = sqlalchemy.engine 31 | 32 | [logger_alembic] 33 | level = INFO 34 | handlers = 35 | qualname = alembic 36 | 37 | [handler_console] 38 | class = StreamHandler 39 | args = (sys.stderr,) 40 | level = NOTSET 41 | formatter = generic 42 | 43 | [formatter_generic] 44 | format = %(levelname)-5.5s [%(name)s] %(message)s 45 | datefmt = %H:%M:%S 46 | -------------------------------------------------------------------------------- /migrations/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | import logging 4 | from logging.config import fileConfig 5 | 6 | from sqlalchemy import engine_from_config 7 | from sqlalchemy import pool 8 | 9 | from alembic import context 10 | 11 | # this is the Alembic Config object, which provides 12 | # access to the values within the .ini file in use. 13 | config = context.config 14 | 15 | # Interpret the config file for Python logging. 16 | # This line sets up loggers basically. 17 | fileConfig(config.config_file_name) 18 | logger = logging.getLogger('alembic.env') 19 | 20 | # add your model's MetaData object here 21 | # for 'autogenerate' support 22 | # from myapp import mymodel 23 | # target_metadata = mymodel.Base.metadata 24 | from flask import current_app 25 | config.set_main_option('sqlalchemy.url', 26 | current_app.config.get('SQLALCHEMY_DATABASE_URI')) 27 | target_metadata = current_app.extensions['migrate'].db.metadata 28 | 29 | # other values from the config, defined by the needs of env.py, 30 | # can be acquired: 31 | # my_important_option = config.get_main_option("my_important_option") 32 | # ... etc. 33 | 34 | 35 | def run_migrations_offline(): 36 | """Run migrations in 'offline' mode. 37 | 38 | This configures the context with just a URL 39 | and not an Engine, though an Engine is acceptable 40 | here as well. By skipping the Engine creation 41 | we don't even need a DBAPI to be available. 42 | 43 | Calls to context.execute() here emit the given string to the 44 | script output. 45 | 46 | """ 47 | url = config.get_main_option("sqlalchemy.url") 48 | context.configure( 49 | url=url, target_metadata=target_metadata, literal_binds=True 50 | ) 51 | 52 | with context.begin_transaction(): 53 | context.run_migrations() 54 | 55 | 56 | def run_migrations_online(): 57 | """Run migrations in 'online' mode. 58 | 59 | In this scenario we need to create an Engine 60 | and associate a connection with the context. 61 | 62 | """ 63 | 64 | # this callback is used to prevent an auto-migration from being generated 65 | # when there are no changes to the schema 66 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html 67 | def process_revision_directives(context, revision, directives): 68 | if getattr(config.cmd_opts, 'autogenerate', False): 69 | script = directives[0] 70 | if script.upgrade_ops.is_empty(): 71 | directives[:] = [] 72 | logger.info('No changes in schema detected.') 73 | 74 | connectable = engine_from_config( 75 | config.get_section(config.config_ini_section), 76 | prefix='sqlalchemy.', 77 | poolclass=pool.NullPool, 78 | ) 79 | 80 | with connectable.connect() as connection: 81 | context.configure( 82 | connection=connection, 83 | target_metadata=target_metadata, 84 | process_revision_directives=process_revision_directives, 85 | **current_app.extensions['migrate'].configure_args 86 | ) 87 | 88 | with context.begin_transaction(): 89 | context.run_migrations() 90 | 91 | 92 | if context.is_offline_mode(): 93 | run_migrations_offline() 94 | else: 95 | run_migrations_online() 96 | -------------------------------------------------------------------------------- /migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==1.13.2 2 | beautifulsoup4==4.12.3 3 | blinker==1.8.2 4 | click==8.1.7 5 | Flask==3.0.3 6 | Flask-Login==0.6.3 7 | Flask-Migrate==4.0.7 8 | Flask-Moment==1.0.6 9 | Flask-SQLAlchemy==3.1.1 10 | Flask-WTF==1.2.1 11 | greenlet==3.0.3 12 | itsdangerous==2.2.0 13 | Jinja2==3.1.4 14 | Mako==1.3.5 15 | MarkupSafe==2.1.5 16 | packaging==24.1 17 | pillow==10.4.0 18 | PyJWT==2.9.0 19 | PyMySQL==1.1.1 20 | python-dotenv==1.0.1 21 | python-http-client==3.3.7 22 | python-slugify==8.0.4 23 | sendgrid==6.11.0 24 | soupsieve==2.6 25 | SQLAlchemy==2.0.34 26 | starkbank-ecdsa==2.2.0 27 | text-unidecode==1.3 28 | typing_extensions==4.12.2 29 | Werkzeug==3.0.3 30 | WTForms==3.1.2 31 | -------------------------------------------------------------------------------- /requirements_old.txt: -------------------------------------------------------------------------------- 1 | alembic==1.0.8 2 | asn1crypto==0.24.0 3 | Authlib==0.12.1 4 | autopep8==1.5.2 5 | backcall==0.1.0 6 | beautifulsoup4==4.7.1 7 | blinker==1.4 8 | braintree==3.56.0 9 | certifi==2019.6.16 10 | cffi==1.12.3 11 | chardet==3.0.4 12 | Click==7.0 13 | colorama==0.4.1 14 | cryptography==3.3.1 15 | decorator==4.4.0 16 | Flask==1.0.2 17 | Flask-DebugToolbar==0.10.1 18 | Flask-Login==0.4.1 19 | Flask-Mail==0.9.1 20 | Flask-Migrate==2.4.0 21 | Flask-Moment==0.7.0 22 | Flask-Sitemap==0.3.0 23 | Flask-SQLAlchemy==2.4.0 24 | Flask-WTF==0.14.2 25 | idna==2.8 26 | ipdb==0.12.2 27 | ipython==7.7.0 28 | ipython-genutils==0.2.0 29 | itsdangerous==1.1.0 30 | jedi==0.15.1 31 | Jinja2==2.10.1 32 | Mako==1.0.8 33 | MarkupSafe==1.1.1 34 | numpy==1.17.2 35 | pandas==0.25.1 36 | parso==0.5.1 37 | pbr==3.1.1 38 | pickleshare==0.7.5 39 | Pillow==8.0.1 40 | prompt-toolkit==2.0.9 41 | pycodestyle==2.6.0 42 | pycparser==2.19 43 | Pygments==2.4.2 44 | PyJWT==1.7.1 45 | PyMySQL==0.9.3 46 | python-dateutil==2.8.0 47 | python-dotenv==0.10.3 48 | python-editor==1.0.4 49 | python-http-client==3.1.0 50 | python-slugify==4.0.0 51 | pytz==2019.2 52 | requests==2.22.0 53 | sendgrid==6.0.5 54 | six==1.12.0 55 | soupsieve==1.9.1 56 | SQLAlchemy==1.3.3 57 | text-unidecode==1.3 58 | traitlets==4.3.2 59 | urllib3==1.25.3 60 | wcwidth==0.1.7 61 | Werkzeug==0.16.0 62 | WTForms==2.2.1 63 | -------------------------------------------------------------------------------- /sql.sql: -------------------------------------------------------------------------------- 1 | #The below 2 values are mandatory 2 | INSERT INTO role (name) 3 | VALUES ('admin'),('user'); 4 | 5 | UPDATE role 6 | SET `default` = 1 7 | WHERE name = 'user'; 8 | 9 | INSERT INTO content (name,content,page_id,update_date) 10 | VALUES ('content1','test',1,now()),('content1','test',2,now()); 11 | 12 | INSERT INTO page (name) VALUES ('about'),('contact'); 13 | 14 | INSERT INTO role (name, `default`) 15 | VALUES ('special',0); 16 | 17 | INSERT INTO user 18 | (email, role_id, last_seen,create_date, firstname, lastname) 19 | VALUES 20 | ('mfoong109@gmail.com',3, now(),now(),'Michael', 'Foong'); 21 | 22 | from app import db 23 | from app.models import User 24 | u = User.query.filter_by(email='mfoong109@gmail.com').first() 25 | u.set_password('abc') 26 | db.session.add(u) 27 | db.session.commit() 28 | 29 | insert into page(name) value('special'); 30 | 31 | INSERT INTO content (name,content,page_id,update_date) 32 | VALUES ('content1','

Hiya Mikey!

',11,now()); 33 | --------------------------------------------------------------------------------