parent
1463dca012
commit
a457f355d1
@ -0,0 +1,17 @@
|
||||
FROM python:3
|
||||
|
||||
MAINTAINER tiyn tiyn@mail-mk.eu
|
||||
|
||||
COPY src /blog
|
||||
|
||||
WORKDIR /blog
|
||||
|
||||
RUN pip3 install -r requirements.txt
|
||||
|
||||
VOLUME /blog/templates/entry
|
||||
|
||||
EXPOSE 5000
|
||||
|
||||
ENTRYPOINT [ "python3" ]
|
||||
|
||||
CMD [ "app.py" ]
|
@ -0,0 +1,68 @@
|
||||
# Container Critique
|
||||
|
||||
This is a blog based on Pythons Flask framework.
|
||||
The blog is intended to be used to review and critique things.
|
||||
|
||||
## Features/To-Dos
|
||||
|
||||
- [ ] Plain text support for blog entries
|
||||
- [ ] HTML files (.html)
|
||||
- [ ] Markdown Files (.md)
|
||||
- [ ] Infinite-scroll blog page
|
||||
- [ ] Archive page
|
||||
- [ ] Months as headings
|
||||
- [ ] Links to scrolling blog page
|
||||
- [ ] Links to standalone article
|
||||
- [ ] Standalone article page
|
||||
- [ ] Links to scrolling blog page
|
||||
- [ ] RSS feed
|
||||
- [ ] Navigation
|
||||
- [ ] Header
|
||||
- [ ] Footer
|
||||
- [ ] Switchable CSS
|
||||
- [ ] CSS dark-theme
|
||||
- [ ] CSS light-theme
|
||||
- [ ] Config file
|
||||
- [ ] Docker installation
|
||||
- [ ] Logo
|
||||
|
||||
## Usage
|
||||
|
||||
## Deployment
|
||||
|
||||
### PIP/Python
|
||||
|
||||
- `git clone https://github.com/tiyn/container-critique`
|
||||
- `cd container-critique/src`
|
||||
- edit the `config.py` file according to your needs
|
||||
- `pip3install -r requirements.txt` - install depenencies
|
||||
- run `python app.py`
|
||||
- blog is available on port 5000
|
||||
|
||||
### Docker
|
||||
|
||||
Make sure you copy an example `config.py` and edit it before running the container.
|
||||
The `config.py` can be found in the `src` folder.
|
||||
|
||||
#### Volumes
|
||||
|
||||
Set the following volumes with the -v tag.
|
||||
|
||||
| Volume-Name | Container mount | Description |
|
||||
| ------------- | --------------------------- | ------------------------------------------------------------ |
|
||||
| `config-file` | `/blog/src/config.py` | Config file |
|
||||
| `entries` | `/blog/src/templates/entry` | Directory for blog entries |
|
||||
| `css` | `/blog/src/static/css` | (optional) Directory for css files |
|
||||
| `html` | `/blog/src/templates` | (optional) Directory for templates (entry-volume not needed) |
|
||||
|
||||
#### Ports
|
||||
|
||||
Set the following ports with the -p tag.
|
||||
|
||||
| Container-Port | Recommended outside port | Protocol | Description |
|
||||
| -------------- | ------------------------ | -------- | ----------- |
|
||||
| `5000` | `80` | TCP | HTTP port |
|
||||
|
||||
#### Example run-command
|
||||
|
||||
An example run command is shown in `rebuild.sh`.
|
After Width: | Height: | Size: 3.6 KiB |
@ -0,0 +1,9 @@
|
||||
#!/bin/sh
|
||||
docker stop container-critique
|
||||
docker rm container-critique
|
||||
docker build . -t tiyn/container-critique
|
||||
docker run --name container-critique \
|
||||
--restart unless-stopped \
|
||||
-p "5000:5000" \
|
||||
-e FLASK_ENV=development \
|
||||
-d tiyn/container-critique
|
@ -0,0 +1 @@
|
||||
__pycache__/
|
@ -0,0 +1,90 @@
|
||||
from flask import Flask, flash, make_response, render_template, request, redirect, abort, url_for
|
||||
from flask_login import current_user, login_user, LoginManager
|
||||
|
||||
import content as con_gen
|
||||
import config
|
||||
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, PasswordField, SubmitField, BooleanField
|
||||
from wtforms.validators import DataRequired
|
||||
|
||||
class LoginForm(FlaskForm):
|
||||
username = StringField('Username', validators=[DataRequired()])
|
||||
password = PasswordField('Password', validators=[DataRequired()])
|
||||
remember_me = BooleanField('Remember Me')
|
||||
submit = SubmitField('Sign In')
|
||||
|
||||
|
||||
app = Flask(__name__)
|
||||
login = LoginManager(app)
|
||||
|
||||
TITLE = config.TITLE
|
||||
STYLE = config.STYLE
|
||||
DESCRIPTION = config.DESCRIPTION
|
||||
WEBSITE = config.WEBSITE
|
||||
|
||||
|
||||
class Config(object):
|
||||
SECRET_KEY = '123534'
|
||||
|
||||
app.config.from_object(Config)
|
||||
|
||||
|
||||
@app.errorhandler(404)
|
||||
def page_not_found(e):
|
||||
return render_template('error.html', title=TITLE, errorcode='404', style=STYLE), 404
|
||||
|
||||
|
||||
@app.route('/')
|
||||
@app.route('/index.html')
|
||||
def index():
|
||||
content = con_gen.gen_index_string()
|
||||
return render_template('index.html', title=TITLE, content_string=content, style=STYLE)
|
||||
|
||||
|
||||
@app.route('/archive')
|
||||
@app.route('/archive.html')
|
||||
def blog_archive():
|
||||
content = con_gen.gen_arch_string()
|
||||
return render_template('archive.html', title=TITLE, content_string=content, style=STYLE)
|
||||
|
||||
|
||||
@app.route('/entry/<path>')
|
||||
def entry(path):
|
||||
content = con_gen.gen_stand_string(path)
|
||||
if content != '':
|
||||
return render_template('standalone.html', title=TITLE, content_string=content, style=STYLE)
|
||||
abort(404)
|
||||
|
||||
|
||||
@app.route('/feed.xml')
|
||||
@app.route('/rss.xml')
|
||||
def feed():
|
||||
content = con_gen.get_rss_string()
|
||||
rss_xml = render_template('rss.xml', content_string=content, title=TITLE,
|
||||
description=DESCRIPTION, website=WEBSITE)
|
||||
response = make_response(rss_xml)
|
||||
response.headers['Content-Type'] = 'application/rss+xml'
|
||||
return response
|
||||
|
||||
@login.user_loader
|
||||
def load_user(id):
|
||||
return ""
|
||||
|
||||
@app.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('index'))
|
||||
form = LoginForm()
|
||||
if form.validate_on_submit():
|
||||
user = User.query.filter_by(username=form.username.data).first()
|
||||
if user is None or not user.check_password(form.password.data):
|
||||
flash('Invalid username or password')
|
||||
return redirect(url_for('login'))
|
||||
login_user(user, remember=form.remember_me.data)
|
||||
return redirect(url_for('index'))
|
||||
return render_template('login.html', title='Sign In', form=form)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0')
|
@ -0,0 +1,11 @@
|
||||
# Name/title of your blog
|
||||
TITLE = 'Container Critique'
|
||||
|
||||
# Description for RSS of your blog
|
||||
DESCRIPTION = 'This is your personal Container Critique.'
|
||||
|
||||
# URL for your website: e.g. https://domain.tld
|
||||
WEBSITE = 'localhost:5000'
|
||||
|
||||
# Theme for the blog: dark, light
|
||||
STYLE = 'dark'
|
@ -0,0 +1,45 @@
|
||||
ENTRY_DIR = 'templates/entry'
|
||||
|
||||
def gen_arch_string():
|
||||
"""
|
||||
Creates and returns a archive string of every file in ENTRY_DIR.
|
||||
|
||||
Returns:
|
||||
string: html-formatted archive-string
|
||||
"""
|
||||
return ""
|
||||
|
||||
|
||||
def gen_index_string():
|
||||
"""
|
||||
Create and returns a string including every file in the ENTRY_DIR as an index.
|
||||
|
||||
Returns:
|
||||
string: html-formatted index string
|
||||
"""
|
||||
return ""
|
||||
|
||||
|
||||
def gen_stand_string(path_ex):
|
||||
"""
|
||||
Creates a html-string for a file.
|
||||
If the file is markdown it will convert it.
|
||||
This functions ensures upscaling for future formats.
|
||||
|
||||
Parameters:
|
||||
path_ex: path to a file.
|
||||
|
||||
Returns:
|
||||
string: html-formatted string string equivalent to the file
|
||||
"""
|
||||
return ""
|
||||
|
||||
|
||||
def get_rss_string():
|
||||
"""
|
||||
Create a rss-string of the blog and return it.
|
||||
|
||||
Returns:
|
||||
string: rss-string of everything that is in the ENTRY_DIR.
|
||||
"""
|
||||
return ""
|
@ -0,0 +1,4 @@
|
||||
Flask==2.1.2
|
||||
flask_login==0.6.2
|
||||
flask_wtf==1.0.1
|
||||
WTForms==3.0.1
|
@ -0,0 +1,83 @@
|
||||
@import 'style.css';
|
||||
|
||||
:root {
|
||||
--bg0: rgb(29,32,33);
|
||||
--color0: rgb(220,120,0);
|
||||
--error: rgb(255,0,0);
|
||||
--footerbg0: rgb(29,32,33);
|
||||
--link0: rgb(220, 120, 0);
|
||||
--link1: rgb(255,255,255);
|
||||
--menulink0: rgb(220, 120, 0);
|
||||
--menulink1: rgb(255,255,255);
|
||||
--menubg0: rgb(29,32,33);
|
||||
--text0: rgb(235,219,178);
|
||||
--text1: rgb(220, 120, 0);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--link0);
|
||||
transition: var(--transtime);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--link1);
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg0);
|
||||
}
|
||||
|
||||
footer {
|
||||
background: var(--footerbg0);
|
||||
color: var(--text0);
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--text1);
|
||||
}
|
||||
|
||||
.container {
|
||||
color: var(--text0);
|
||||
}
|
||||
|
||||
.container h1,
|
||||
.container h2 {
|
||||
color: var(--text1);
|
||||
}
|
||||
|
||||
.container .flash {
|
||||
background-color: var(--error);
|
||||
}
|
||||
|
||||
.hide-menu:hover,
|
||||
.main-menu a:hover,
|
||||
.show-menu:hover {
|
||||
color: var(--menulink1);
|
||||
}
|
||||
|
||||
.main-menu a {
|
||||
color: var(--menulink0);
|
||||
}
|
||||
|
||||
.main-menu-dropdown {
|
||||
background: var(--menubg0);
|
||||
color: var(--menulink0);
|
||||
}
|
||||
|
||||
@media screen and (max-width:800px) {
|
||||
|
||||
.main-menu {
|
||||
background: var(--menubg0);
|
||||
}
|
||||
}
|
||||
|
||||
.entry {
|
||||
background: var(--bg0);
|
||||
border-left: 10px solid var(--color0);
|
||||
color: var(--text0);
|
||||
}
|
||||
|
||||
.entry h1,
|
||||
.entry h2 {
|
||||
color: var(--text1);
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
@import 'style.css';
|
||||
|
||||
:root {
|
||||
--bg0: rgb(255,255,255);
|
||||
--color0: rgb(0,0,120);
|
||||
--error: rgb(255,0,0);
|
||||
--footerbg0: rgb(192,192,192);
|
||||
--link0: rgb(0,0,120);
|
||||
--link1: rgb(255,255,255);
|
||||
--menulink0: rgb(0,0,120);
|
||||
--menulink1: rgb(255,255,255);
|
||||
--menubg0: rgb(192,192,192);
|
||||
--text0: rgb(0,0,0);
|
||||
--text1: rgb(0,0,120);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--link0);
|
||||
transition: var(--transtime);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--link1);
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg0);
|
||||
}
|
||||
|
||||
footer {
|
||||
background: var(--footerbg0);
|
||||
color: var(--text0);
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--text1);
|
||||
}
|
||||
|
||||
.container {
|
||||
color: var(--text0);
|
||||
}
|
||||
|
||||
.container h1,
|
||||
.container h2 {
|
||||
color: var(--text1);
|
||||
}
|
||||
|
||||
.container .flash {
|
||||
background-color: var(--error);
|
||||
}
|
||||
|
||||
.hide-menu:hover,
|
||||
.main-menu a:hover,
|
||||
.show-menu:hover {
|
||||
color: var(--menulink1);
|
||||
}
|
||||
|
||||
.main-menu a {
|
||||
color: var(--menulink0);
|
||||
}
|
||||
|
||||
.main-menu-dropdown {
|
||||
background: var(--menubg0);
|
||||
color: var(--menulink0);
|
||||
}
|
||||
|
||||
@media screen and (max-width:800px) {
|
||||
|
||||
.main-menu {
|
||||
background: var(--menubg0);
|
||||
}
|
||||
}
|
||||
|
||||
.entry {
|
||||
background: var(--bg0);
|
||||
border-left: 10px solid var(--color0);
|
||||
color: var(--text0);
|
||||
}
|
||||
|
||||
.entry h1,
|
||||
.entry h2 {
|
||||
color: var(--text1);
|
||||
}
|
@ -0,0 +1,169 @@
|
||||
:root {
|
||||
--error: rgb(255,0,0);
|
||||
--transtime: 0.7s;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
transition: var(--transtime);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body,
|
||||
html {
|
||||
font-family: sans-serif;
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
footer {
|
||||
height: 100px;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
footer .center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
min-height: 100%;
|
||||
padding-bottom: 50px;
|
||||
padding-left: 10%;
|
||||
padding-right: 10%;
|
||||
padding-top: 5%;
|
||||
}
|
||||
|
||||
.container .flash {
|
||||
padding: 10px;
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.hide-menu,
|
||||
.show-menu {
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
font-size: 30px;
|
||||
transition: var(--transtime);
|
||||
}
|
||||
|
||||
.important {
|
||||
font-size: xx-large;
|
||||
padding-left: 25vw;
|
||||
padding-right: 25vw;
|
||||
padding-top: 30vh;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.important span {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 80px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.main-menu-dropdown span {
|
||||
float: left;
|
||||
font-family: monospace;
|
||||
font-size: 30px;
|
||||
font-weight: bold;
|
||||
line-height: 100px;
|
||||
padding: 0 10px;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
transition: 0.7s;
|
||||
}
|
||||
|
||||
.main-menu {
|
||||
float: right;
|
||||
font-family: monospace;
|
||||
font-size: 30px;
|
||||
font-weight: bold;
|
||||
line-height: 100px;
|
||||
}
|
||||
|
||||
.main-menu a {
|
||||
padding: 0 10px;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
transition: 0.7s;
|
||||
}
|
||||
|
||||
.main-menu-dropdown {
|
||||
height: 100px;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.show-menu {
|
||||
float: right;
|
||||
line-height: 100px;
|
||||
}
|
||||
|
||||
#main-menu-check {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
z-index: -1111;
|
||||
}
|
||||
|
||||
@media screen and (max-width:800px) {
|
||||
.hide-menu {
|
||||
position: absolute;
|
||||
right: 40px;
|
||||
top: 40px;
|
||||
}
|
||||
|
||||
.hide-menu,
|
||||
.show-menu {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.main-menu {
|
||||
height: 100vh;
|
||||
line-height: normal;
|
||||
padding: 80px 0;
|
||||
position: fixed;
|
||||
right: -100%;
|
||||
text-align: center;
|
||||
top: 0;
|
||||
transition: var(--transtime);
|
||||
width: 100%;
|
||||
}
|
||||
.main-menu a {
|
||||
display: block;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#main-menu-check:checked ~ .main-menu {
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.entry {
|
||||
border-radius: 0 10px 30px 0;
|
||||
margin-bottom: 20px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.entry h1,
|
||||
.entry h2 {
|
||||
margin: 5px auto 2px auto;
|
||||
}
|
||||
|
||||
.entry ul {
|
||||
padding-left: 20;
|
||||
}
|
||||
|
@ -0,0 +1,11 @@
|
||||
{% extends 'template.html' %}
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="blogarchive">
|
||||
<h1>Archive</h1><br>
|
||||
{% autoescape off %}
|
||||
{{ content_string }}
|
||||
{% endautoescape %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -0,0 +1,9 @@
|
||||
{% extends "template.html" %}
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="important">
|
||||
Error<br>
|
||||
<span>{{ errorcode }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -0,0 +1,11 @@
|
||||
{% extends "template.html" %}
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="blog">
|
||||
<h1>Index</h1><br>
|
||||
{% autoescape off %}
|
||||
{{ content_string }}
|
||||
{% endautoescape %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
|
||||
<channel>
|
||||
<title>{{ title }}</title>
|
||||
<description>{{ description }}</description>
|
||||
<language>en-us</language>
|
||||
<link>{{ website }}/feed.xml</link>
|
||||
<atom:link href="/feed.xml" rel="self" type="application/rss+xml" />
|
||||
|
||||
{% autoescape off %}
|
||||
{{ content_string }}
|
||||
{% endautoescape %}
|
||||
|
||||
</channel>
|
||||
</rss>
|
@ -0,0 +1,10 @@
|
||||
{% extends 'template.html' %}
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="standalone">
|
||||
{% autoescape off %}
|
||||
{{ content_string }}
|
||||
{% endautoescape %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -0,0 +1,32 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>{{ title }}</title>
|
||||
<link href="{{ url_for('static', filename='css/' + style + '.css') }}" rel="stylesheet" type="text/css">
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width" initial-scale=1.0>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Menu -->
|
||||
<div class="main-menu-dropdown">
|
||||
<!-- <img class="logo" src="/static/images/logo.png"> -->
|
||||
<span>{{ title }}</span>
|
||||
<input type="checkbox" id="main-menu-check">
|
||||
<label for="main-menu-check" class="show-menu">☰</label>
|
||||
<div class="main-menu">
|
||||
<a href="/">Blog</a>
|
||||
<a href="/archive">Archive</a>
|
||||
<label for="main-menu-check" class="hide-menu">X</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Menu -->
|
||||
<!-- Content -->
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
<!-- Content -->
|
||||
<footer>
|
||||
<div class="center">
|
||||
Made with <a href="https://github.com/tiyn/container-critique">Container Critique </a>.
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in new issue