commit 73f23a591ce64d4b64ebee7a70b4edd576079f23 Author: gd Date: Tue Mar 23 23:11:57 2021 +0300 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4f04a84 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ +*~ +*.swp \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..696ed1d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,30 @@ +# Changelog + +## v1.0 2021.03.23 + +### Added + +- Bootstrap 5. Brand new frontend. +- Bootstrap Icons. +- `Flask.session` based authentication. Can be enabled in `config.py`. Password encrypted by `bcrypt`. +- `pass.py` to set password. +- Normal 404 error page. +- `CONTENTS.md` parser. You can navigate between articles. +- Article title parser. The title is now displayed in the title of the page. +- New shitcode. It will be refactored in next versions. + +### Changed + +- `contents.md` and `home.md` renamed to `CONTENTS.md` and `HOME.md`. +- `native` Pygments theme by default. +- File search algorithm changed. Now the viewing of files nested in folders works. +- The main application code has been moved to the `owl.py` module. The launch point of the application is now also the `owl.py`, and not the `wsgi.py`. It may not be the best architectural solution, but it seems to be the most concise now. + +### Removed + +- Old shitcode removed. See Changed. +- Old frontend and templates. + +## v0.1 2020.08.15 + +First version released. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..68a49da --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/README.md b/README.md new file mode 100644 index 0000000..def1b19 --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +# owl + +![](https://img.shields.io/badge/owl-v1.0-%2300f) + +**owl** — is the minimalistic kn**owl**edge base web app written in Python. + +See full docs and demo here: [https://owl.gch.icu/docs/](https://owl.gch.icu/docs/). + +Run **owl** in five lines: + +```bash +python3 -m venv env +source env/bin/activate +git clone https://github.com/gechandesu/owl.git && cd owl +pip install -r requirements.txt +python owl.py +``` + +App is now available at [http://localhost:5000/](http://localhost:5000/). + +**owl** doesn't use a database, all files are stored in plain text. + +This solution is suitable for creating documentation or maintaining a personal knowledge base. + +New in `v1.0`: +- This is brand new owl! +- New frontend and refactored backend. +- Bootstrap 5 +- Optional authentication. + +See [CHANGELOG.md](CHANGELOG.md) + +# License + +This software is licensed under The Unlicense. See [LICENSE](LICENSE). diff --git a/config.py b/config.py new file mode 100644 index 0000000..e564299 --- /dev/null +++ b/config.py @@ -0,0 +1,16 @@ +class Config(object): + DEBUG = False + SECRET_KEY = 'top_secret' + PASSWORD_FILE = '.pw' + SIGN_IN = False # Enable or disable authentication + MARKDOWN_ROOT = 'docs/' # Path to your .md files + MARKDOWN_DOWLOADS = True + # See https://github.com/trentm/python-markdown2/wiki/Extras + MARKDOWN2_EXTRAS = [ + 'fenced-code-blocks', + 'markdown-in-html', + 'code-friendly', + 'header-ids', + 'strike', + 'tables' + ] \ No newline at end of file diff --git a/docs/CONTENTS.md b/docs/CONTENTS.md new file mode 100644 index 0000000..5587640 --- /dev/null +++ b/docs/CONTENTS.md @@ -0,0 +1,3 @@ +### Contents + +- [Home](/) diff --git a/docs/HOME.md b/docs/HOME.md new file mode 100644 index 0000000..9de7ccf --- /dev/null +++ b/docs/HOME.md @@ -0,0 +1,6 @@ +# @v@ owl took off! + +Read the [Docs](https://owl.gch.icu/docs/) to get started. + +Also there is project's [git repository](https://gitea.gch.icu/gd/owl) ([mirror](https://github.com/gechandesu/owl)). + diff --git a/owl.py b/owl.py new file mode 100644 index 0000000..2c244df --- /dev/null +++ b/owl.py @@ -0,0 +1,157 @@ +import os +import re +from functools import wraps +from datetime import timedelta + +import pygments +from markdown2 import Markdown + +from flask import Flask +from flask import request +from flask import session +from flask import redirect +from flask import render_template +from flask import send_from_directory +from flask_bcrypt import Bcrypt + +from config import Config + + +app = Flask(__name__) +app.config.from_object(Config) +app.permanent_session_lifetime = timedelta(hours=24) + +bcrypt = Bcrypt(app) + +def read_file(filepath: str) -> str: + try: + with open(filepath, 'r') as file: + return file.read() + except IOError: + return 'Error: Cannot read file: {}'.format(filepath) + +def render_html(filepath: str) -> str: + markdown = Markdown(extras=app.config['MARKDOWN2_EXTRAS']) + return markdown.convert( + read_file( + app.config['MARKDOWN_ROOT'] + filepath + ) + ) + +def parse_title_from_markdown(filepath: str) -> str: + # This function parses article title from first level heading. + # It returns the occurrence of the first heading, and there + # can be nothing before it except empty lines and spaces. + article = read_file(app.config['MARKDOWN_ROOT'] + filepath) + pattern = re.compile(r'^\s*#\s.*') + if pattern.search(article): + return pattern.search(article).group().strip()[2:] + else: + return 'Error: Cannot parse title from file:'.format(filepath) + +def parse_content_links(filepath: str) -> list: + # This function returns a list of links from a Markdown file. + # Only links contained in the list (ul ol li) are parsed. + r = re.compile(r'(.*(-|\+|\*|\d).?\[.*\])(\(.*\))', re.MULTILINE) + links = [] + for tpl in r.findall(read_file(app.config['MARKDOWN_ROOT'] + filepath)): + for item in tpl: + if re.match(r'\(.*\)', item): + if item == '(/)': + item = '/' # This is a crutch for fixing the root url + # which for some reason continues to contain + # parentheses after re.match(r''). + if not item[1:-1].endswith('/'): + item = item[1:-1] + '/' + links.append(item) + return links + +def check_password(password: str) -> bool: + if os.path.exists('.pw'): + pw_hash = read_file('.pw') + return bcrypt.check_password_hash(pw_hash, password) + else: + return False + +@app.errorhandler(404) +def page_not_found(e): + return render_template('404.j2'), 404 + +@app.context_processor +def utility_processor(): + def get_len(list: list) -> int: + return len(list) + def get_title(path: str) -> str: + return parse_title_from_markdown(path[:-1] + '.md') + return dict(get_title = get_title, get_len = get_len) + +def login_required(func): + @wraps(func) + def wrap(*args, **kwargs): + if app.config['SIGN_IN']: + if 'logged_in' in session: + return func(*args, **kwargs) + else: + return redirect('/signin/') + else: + return func(*args, **kwargs) + return wrap + +@app.route('/signin/', methods = ['GET', 'POST']) +def signin(): + if request.method == 'POST': + if check_password(request.form['password']): + session['logged_in'] = True + return redirect('/', 302) + else: + return render_template('signin.j2', wrong_pw = True) + else: + return render_template('signin.j2') + +@app.route('/signout/') +@login_required +def signout(): + session.pop('logged_in', None) + return redirect('/signin/') + +@app.route('/') +@login_required +def index(): + return render_template( + 'index.j2', + title = parse_title_from_markdown('HOME.md'), + article = render_html('HOME.md'), + contents = render_html('CONTENTS.md'), + current_path = '/', + links = parse_content_links('CONTENTS.md') + ) + +@app.route('//') +@login_required +def get_article(path): + if os.path.exists(app.config['MARKDOWN_ROOT'] + path + '.md'): + return render_template( + 'index.j2', + title = parse_title_from_markdown(path + '.md'), + article = render_html(path + '.md'), + contents = render_html('CONTENTS.md'), + current_path = request.path, + links = parse_content_links('CONTENTS.md') + ) + else: + return page_not_found(404) + +@app.route('/.md') +@login_required +def download_article(path): + if os.path.exists(app.config['MARKDOWN_ROOT'] + path + '.md') \ + and app.config['MARKDOWN_DOWLOADS']: + return send_from_directory( + app.config['MARKDOWN_ROOT'], + path + '.md' + ) + else: + return page_not_found(404) + +if __name__ == '__main__': + app.run() \ No newline at end of file diff --git a/pass.py b/pass.py new file mode 100644 index 0000000..95ea04e --- /dev/null +++ b/pass.py @@ -0,0 +1,26 @@ +from getpass import getpass + +from owl import app +from flask_bcrypt import Bcrypt + +bcrypt = Bcrypt(app) + +def generate_pw_hash(password, file): + pw_hash = bcrypt.generate_password_hash(password).decode('utf-8') + with open(file, 'w') as pwfile: + pwfile.write(pw_hash) + +if __name__ == '__main__': + with app.app_context(): + file = input('Enter password file name (default: .pw): ') + if not file: + file = '.pw' + password = getpass('Enter new password: ') + confirm = getpass('Confirm password: ') + if password != confirm: + print('Abort! Password mismatch.') + exit() + generate_pw_hash(password, file) + print('Success! New password file created: {}'.format(file)) + if file != '.pw': + print('Don\'t forgot change password file name in `config.py`.') \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c8afa2f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +flask>=1.1 +flask-bcrypt>=0.7 +markdown2>=2.3 +pygments>=2.6 \ No newline at end of file diff --git a/static/css/codehilite.css b/static/css/codehilite.css new file mode 100644 index 0000000..01ab791 --- /dev/null +++ b/static/css/codehilite.css @@ -0,0 +1,82 @@ +pre { line-height: 125%; } +td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +.hll { background-color: #404040 } +.c { color: #999999; font-style: italic } /* Comment */ +.err { color: #a61717; background-color: #e3d2d2 } /* Error */ +.esc { color: #d0d0d0 } /* Escape */ +.g { color: #d0d0d0 } /* Generic */ +.k { color: #6ab825; font-weight: bold } /* Keyword */ +.l { color: #d0d0d0 } /* Literal */ +.n { color: #d0d0d0 } /* Name */ +.o { color: #d0d0d0 } /* Operator */ +.x { color: #d0d0d0 } /* Other */ +.p { color: #d0d0d0 } /* Punctuation */ +.ch { color: #999999; font-style: italic } /* Comment.Hashbang */ +.cm { color: #999999; font-style: italic } /* Comment.Multiline */ +.cp { color: #cd2828; font-weight: bold } /* Comment.Preproc */ +.cpf { color: #999999; font-style: italic } /* Comment.PreprocFile */ +.c1 { color: #999999; font-style: italic } /* Comment.Single */ +.cs { color: #e50808; font-weight: bold; background-color: #520000 } /* Comment.Special */ +.gd { color: #d22323 } /* Generic.Deleted */ +.ge { color: #d0d0d0; font-style: italic } /* Generic.Emph */ +.gr { color: #d22323 } /* Generic.Error */ +.gh { color: #ffffff; font-weight: bold } /* Generic.Heading */ +.gi { color: #589819 } /* Generic.Inserted */ +.go { color: #cccccc } /* Generic.Output */ +.gp { color: #aaaaaa } /* Generic.Prompt */ +.gs { color: #d0d0d0; font-weight: bold } /* Generic.Strong */ +.gu { color: #ffffff; text-decoration: underline } /* Generic.Subheading */ +.gt { color: #d22323 } /* Generic.Traceback */ +.kc { color: #6ab825; font-weight: bold } /* Keyword.Constant */ +.kd { color: #6ab825; font-weight: bold } /* Keyword.Declaration */ +.kn { color: #6ab825; font-weight: bold } /* Keyword.Namespace */ +.kp { color: #6ab825 } /* Keyword.Pseudo */ +.kr { color: #6ab825; font-weight: bold } /* Keyword.Reserved */ +.kt { color: #6ab825; font-weight: bold } /* Keyword.Type */ +.ld { color: #d0d0d0 } /* Literal.Date */ +.m { color: #3677a9 } /* Literal.Number */ +.s { color: #ed9d13 } /* Literal.String */ +.na { color: #bbbbbb } /* Name.Attribute */ +.nb { color: #24909d } /* Name.Builtin */ +.nc { color: #447fcf; text-decoration: underline } /* Name.Class */ +.no { color: #40ffff } /* Name.Constant */ +.nd { color: #ffa500 } /* Name.Decorator */ +.ni { color: #d0d0d0 } /* Name.Entity */ +.ne { color: #bbbbbb } /* Name.Exception */ +.nf { color: #447fcf } /* Name.Function */ +.nl { color: #d0d0d0 } /* Name.Label */ +.nn { color: #447fcf; text-decoration: underline } /* Name.Namespace */ +.nx { color: #d0d0d0 } /* Name.Other */ +.py { color: #d0d0d0 } /* Name.Property */ +.nt { color: #6ab825; font-weight: bold } /* Name.Tag */ +.nv { color: #40ffff } /* Name.Variable */ +.ow { color: #6ab825; font-weight: bold } /* Operator.Word */ +.w { color: #666666 } /* Text.Whitespace */ +.mb { color: #3677a9 } /* Literal.Number.Bin */ +.mf { color: #3677a9 } /* Literal.Number.Float */ +.mh { color: #3677a9 } /* Literal.Number.Hex */ +.mi { color: #3677a9 } /* Literal.Number.Integer */ +.mo { color: #3677a9 } /* Literal.Number.Oct */ +.sa { color: #ed9d13 } /* Literal.String.Affix */ +.sb { color: #ed9d13 } /* Literal.String.Backtick */ +.sc { color: #ed9d13 } /* Literal.String.Char */ +.dl { color: #ed9d13 } /* Literal.String.Delimiter */ +.sd { color: #ed9d13 } /* Literal.String.Doc */ +.s2 { color: #ed9d13 } /* Literal.String.Double */ +.se { color: #ed9d13 } /* Literal.String.Escape */ +.sh { color: #ed9d13 } /* Literal.String.Heredoc */ +.si { color: #ed9d13 } /* Literal.String.Interpol */ +.sx { color: #ffa500 } /* Literal.String.Other */ +.sr { color: #ed9d13 } /* Literal.String.Regex */ +.s1 { color: #ed9d13 } /* Literal.String.Single */ +.ss { color: #ed9d13 } /* Literal.String.Symbol */ +.bp { color: #24909d } /* Name.Builtin.Pseudo */ +.fm { color: #447fcf } /* Name.Function.Magic */ +.vc { color: #40ffff } /* Name.Variable.Class */ +.vg { color: #40ffff } /* Name.Variable.Global */ +.vi { color: #40ffff } /* Name.Variable.Instance */ +.vm { color: #40ffff } /* Name.Variable.Magic */ +.il { color: #3677a9 } /* Literal.Number.Integer.Long */ diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..a09f927 --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,310 @@ +body { + font-family: 'Ubuntu', sans-serif; + font-size: 19px; +} + +h1, +h2, +h3, +h4 { margin: 1rem 0; } + +.header-link { + position: absolute; + margin-left: .2em; + opacity: 0; +} + +h1:hover .header-link, +h2:hover .header-link, +h3:hover .header-link, +h4:hover .header-link { + opacity: 100; +} + +.sidebar h1 .header-link, +.sidebar h2 .header-link, +.sidebar h3 .header-link, +.sidebar h4 .header-link { + display: none; +} + +a { color: #212529; } + +a:hover { color: #707275; } + +blockquote { + border-left: 4px solid #212529; + margin: 1rem 0; + padding: 0.2rem 1rem; + color: #212529; +} + +blockquote p { margin: 0; } + +/* Details and summary */ + +details, summary { + display: block; + margin: 1rem 0; + transition: 200ms linear; +} + +summary { + cursor: pointer; + transition: .3s; +} + +/* Hide defaul marker */ +details > summary { list-style: none; } +details summary::-webkit-details-marker { display: none; } + +details[open] summary ~ * { + animation: open 0.3s ease-in-out; +} + +@keyframes open { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +details summary:before { + content: '+'; + font-family: 'Ubuntu Mono'; + font-size: 20px; + display: inline-flex; + padding: 0 0.3rem; +} + +details[open] summary:before { + content: '-'; + font-family: 'Ubuntu Mono'; + font-size: 20px; + display: inline-flex; + padding: 0 0.3rem; +} + +/* Code styling */ + +code, +pre { + font-family: 'Ubuntu Mono', monospace; + font-size: 19px; + color: #d0d0d0; +} + +pre code { background: unset; padding: unset; } + +pre { + background: #1c1d21; + border-radius: 6px; + padding: 1rem; +} + +code { + color: #1c1d21; + background: #ffeff0; + padding: 4px; + border-radius: 6px; +} + +.raw-pre { + color: unset; + background: unset; +} + +/* Large headings */ +.large-h { font-size: 42px; } +.title-h { font-size: 72px; line-height: 1.1; } + +/* Blank spaces */ +.blank-1 { display: block; height: 1rem; } +.blank-2 { display: block; height: 2rem; } +.blank-5 { display: block; height: 5rem; } + +/* Sign in form */ + +.form-signin { + position: absolute; + left: 50%; + top: 45%; + transform: translate(-50%,-50%); + min-width: 360px; +} + +#inputPassword { margin-bottom: 8px; } + +/* Sign out button */ + +.signout-btn { + z-index: 1001; + position: fixed; + top: 16px; + right: 1rem; + height: 46px; + width: 46px; + text-align: center; + border-radius: 3px; + cursor: pointer; +} + +.signout-btn i { + font-size: 30px; + line-height: 46px; +} + +.signout-btn a { color: #212529; } + +/* 404 page */ + +.page_not_found { + position: absolute; + left: 50%; + top: 45%; + transform: translate(-50%,-50%); + text-align: center; +} + +/* Header bar */ + +.header { + display: block; + position: fixed; + height: 5rem; + background: unset; +} + +/* Sidebar */ + +.sidebar-toggle-btn { + z-index: 1001; + position: fixed; + top: 1rem; + left: 316px; + height: 46px; + width: 46px; + text-align: center; + border-radius: 3px; + cursor: pointer; + transition: left 0.4s ease; +} + +.sidebar-toggle-btn.click { left: 1rem; } + +.sidebar-toggle-btn i { + font-size: 26px; + line-height: 46px; +} + +.sidebar { + z-index: 1000; + position: fixed; + width: 300px; + height: 100%; + left: 0px; + padding: 1rem; + overflow: auto; + transition: left 0.4s ease; + box-shadow: 0 0 10px rgba(0,0,0,0.1); + background: #ffffff; +} + +.sidebar.hide { left: -300px; } + +/* Side menu */ + +.sidebar a { + display: block; + width: auto; + padding: 2px 0; + text-decoration: none; + transition: 0.2s linear; + color: #212529; +} + +.sidebar a:hover { text-decoration: underline; } + +.sidebar ul, +.sidebar ol, +.sidebar li { + list-style-type: none; + list-style-position: inside; + position: relative; + padding: 3px 0 3px 10px; + margin: 0; + color: #6c757d; +} + +.mark { + display: inline; + left: -10px; + bottom: 1px; + width: 100%; + padding: 4px; + border-radius: 6px; + position: absolute; + color: #ffffff; + background: unset; +} + +.mark:hover { color: #212529; } +.mark::before { content: '•'; } + +/* Content container toggle */ + +.content { + margin-left: 300px; + transition: margin-left 0.5s; +} + +.content.wide { margin-left: 0px; } + +/* Back to top button */ + +.to-top-btn { + z-index: 1001; + display: none; + position: fixed; + height: 100%; + width: 3rem; + top: 5rem; + left: 315px; + cursor: pointer; + text-align: center; + transition: left 0.4s ease; +} + +.to-top-btn i { + font-size: 26px; + line-height: 46px; +} + +.to-top-btn.wide { left: 15px; } +.to-top-btn.show { display: block; } + +/* Content block */ + +article { + display: block; + margin: auto; + padding: 1rem; + max-width: 840px; +} + +article.wide { max-width: 980px; } + +/* Responsivity */ + +@media (max-width: 1200px) { + .header { background: #ffffff; } + .sidebar { left: -300px; } + .sidebar-toggle-btn { left: 1rem; } + .content { margin-left: 0px; } + .sidebar.hide { left: 0px; } + .sidebar-toggle-btn.click { left: 316px; } + .to-top-btn.show { display: none; } + article.wide { max-width: 840px; } +} \ No newline at end of file diff --git a/static/images/favicon.ico b/static/images/favicon.ico new file mode 100644 index 0000000..88fec17 Binary files /dev/null and b/static/images/favicon.ico differ diff --git a/static/js/script.js b/static/js/script.js new file mode 100644 index 0000000..2b5fe11 --- /dev/null +++ b/static/js/script.js @@ -0,0 +1,63 @@ +// Here vanilla JavaScript is mixed with JQuery (3.6.0 slim). +// This is very bad. But works. TODO: delete jQuery. + +// Toggle sidebar and change elements width. +$('.sidebar-toggle-btn').click(function(){ + $(this).toggleClass("click"); + $('.sidebar').toggleClass("hide"); + $('#to-top-btn').toggleClass("wide"); + $('article').toggleClass("wide"); + if (screen.width > 1200 ) { + $('.content').toggleClass("wide"); + } +}); + +// Add styling for tables. +$('table').toggleClass("table table-bordered") + +// Back to top button. +var btn = $('#to-top-btn'); + +$(window).scroll(function() { + if ($(window).scrollTop() > 300) { + btn.addClass('show'); + } else { + btn.removeClass('show'); + } +}); + +btn.on('click', function(e) { + e.preventDefault(); + window.scrollTo({ top: 0, behavior: "smooth"}); +}); + +// Add marker to sidebar links. +$('.sidebar a').append('
'); + +// Highlight current page link in sidebar - +// toggle marker on current page link. +var pathname = window.location.pathname; +var links = document.getElementsByTagName("a"); + +for (var element of links) { + var ref = element.getAttribute('href'); + if (ref.substr(-1) !== "/") { + ref = ref + '/'; + } + if (ref == pathname) { + $(element).children('.mark').css('color', '#212529'); + } +} + +// Add paragraph button aside of headings +$(function() { + return $("h1, h2, h3, h4").each(function(i, el) { + var $el, icon, id; + $el = $(el); + id = $el.attr('id'); + icon = ''; + if (id) { + return $el.append($("").addClass("header-link").attr("href", "#" + id).html(icon)); + } + }); +}); \ No newline at end of file diff --git a/templates/404.j2 b/templates/404.j2 new file mode 100644 index 0000000..83c25e1 --- /dev/null +++ b/templates/404.j2 @@ -0,0 +1,15 @@ +{% extends 'base.j2' %} + +{% block title %} + Page not found +{% endblock %} + +{% block content %} + +
+

Page not found

+
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/templates/base.j2 b/templates/base.j2 new file mode 100644 index 0000000..612b559 --- /dev/null +++ b/templates/base.j2 @@ -0,0 +1,47 @@ + + + + + + + {% block title %} + owl + {% endblock %} + + + + + + + + + + + + + + + + + {% if session['logged_in'] %} +
+ +
+ {% endif %} + + {% block content %} + No content here + {% endblock %} + +
+ +
+ + + + + + + + + \ No newline at end of file diff --git a/templates/index.j2 b/templates/index.j2 new file mode 100644 index 0000000..50f830e --- /dev/null +++ b/templates/index.j2 @@ -0,0 +1,69 @@ +{% extends 'base.j2' %} + +{% block title %} + {{ title }} +{% endblock %} + +{% block content %} + + +
+ + +
+ + + +
+ +
+
+ {# CURRENT PATH {{ current_path }} #} +
+ {{ article | safe }} +
+ + + {% for link in links %} + {% if link == current_path %} + + {% endif %} + {% endfor %} +
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/templates/signin.j2 b/templates/signin.j2 new file mode 100644 index 0000000..4660e99 --- /dev/null +++ b/templates/signin.j2 @@ -0,0 +1,22 @@ +{% extends 'base.j2' %} + +{% block title %} + Sign in +{% endblock %} + +{% block content %} + +
+
+

@v@

+
+ {% if wrong_pw %} +

Wrong password.

+ {% endif %} + + + +
+
+ +{% endblock %} \ No newline at end of file