crunk
3 years ago
commit
876a44633f
33 changed files with 719 additions and 0 deletions
@ -0,0 +1,10 @@ |
|||
/.venv/ |
|||
/__pycache__/ |
|||
*.pyc |
|||
*.egg-info/ |
|||
.eggs/ |
|||
build/ |
|||
dist/ |
|||
pip-wheel-metadata/ |
|||
|
|||
*.db |
@ -0,0 +1,3 @@ |
|||
[submodule "distribusi"] |
|||
path = distribusi |
|||
url = https://git.vvvvvvaria.org/varia/distribusi |
@ -0,0 +1,30 @@ |
|||
#distribusi verse |
|||
|
|||
Distribusi is a content management system for the web that produces static index pages based on folders in the files system. It is inspired by the automatic index functions featured in several popular web servers. Distribusi works by traversing the file system and directory hierarchy to automatically list all the files in the directory, detect the file types and providing them with relevant html classes and tags for easy styling. |
|||
|
|||
Distribusi was first conceptualized as a tool which supported a contribution by Dennis de Bel, Danny van der Kleij and Roel Roscam Abbing to the [ruru house](http://ruruhuis.nl/) organized by [Reinaart Vanhoe](http://vanhoe.org/) and the [ruangrupa](http://ruru.ruangrupa.org/) collective during 2016 Sonsbeek Biennale in Arnhem. During the biennale time the ruru house was a lively meeting place with a programme of discussions, workshops, lectures, culinary activities, performances, pop-up markets and even karaoke evenings, where curators and Arnhemmers met. |
|||
|
|||
The contribution consisted of setting up distribusi. ruruhuis.nl (distribusi is bahasa Indonesian for 'distribution') which was a website connected to a server in the space. Rather than a hidden administrative interface, the server was present and visible and an invitation was extended to visitors to use it to publish material online. This was done by inserting a USB-drive into any of the ports. The distribusi script would then turn the contents of that stick it into a website. Once the USB-drive was removed that website was no longer on-line. Over time `distribusi.ruruhuis.nl` hosted photos, books and movies. The website is now off-line but the tool that was used to make it is still used in [Varia](https://varia.zone). |
|||
|
|||
This particular work in progress project is an attempt to make distribusi into a webinterface that can be operated remotely without any knowlegde of CLI. Trying to somehow combine the ideas of distribusi with the ideas of a [tildeverse](https://tildeverse.org/) or [Tilde club ](https://tilde.club/), but also be neither of these ideas. |
|||
|
|||
This project is made for Autonomous Practices at the WDKA in Rotterdam. |
|||
|
|||
## Start your engines! |
|||
|
|||
``` |
|||
$ python3 -m venv .venv |
|||
$ source .venv/bin/activate |
|||
$ pip install -r requirements.txt |
|||
``` |
|||
|
|||
## Database, Databass |
|||
The git doesn't come with a database of users, you have to make one. |
|||
make sure you have the virtual environment enabled. |
|||
|
|||
``` |
|||
$ python deploydb.py |
|||
``` |
|||
|
|||
## Distribusi |
|||
for now I cloned it into the distribusi folder from the varia git. |
@ -0,0 +1,25 @@ |
|||
[tool.black] |
|||
line-length = 79 |
|||
target-version = ['py37', 'py38', 'py39'] |
|||
include = '\.pyi?$' |
|||
exclude = ''' |
|||
/( |
|||
\.eggs |
|||
| \.git |
|||
| \.hg |
|||
| \.mypy_cache |
|||
| \.tox |
|||
| \.venv |
|||
| _build |
|||
| buck-out |
|||
| build |
|||
| dist |
|||
|
|||
# The following are specific to Black, you probably don't want those. |
|||
| blib2to3 |
|||
| tests/data |
|||
| profiling |
|||
)/ |
|||
''' |
|||
|
|||
|
@ -0,0 +1,39 @@ |
|||
alembic==1.7.5 |
|||
Babel==2.9.1 |
|||
bcrypt==3.2.0 |
|||
black==21.11b1 |
|||
blinker==1.4 |
|||
cffi==1.15.0 |
|||
click==8.0.3 |
|||
-e git+https://git.vvvvvvaria.org/varia/distribusi.git@63becedaf51d069744b4088253d883d08e883aa1#egg=distribusi |
|||
Flask==2.0.2 |
|||
Flask-BabelEx==0.9.4 |
|||
Flask-Bcrypt==0.7.1 |
|||
Flask-Login==0.5.0 |
|||
Flask-Mail==0.9.1 |
|||
Flask-Migrate==3.1.0 |
|||
Flask-Principal==0.4.0 |
|||
Flask-Security==3.0.0 |
|||
Flask-SQLAlchemy==2.5.1 |
|||
Flask-WTF==1.0.0 |
|||
greenlet==1.1.2 |
|||
itsdangerous==2.0.1 |
|||
Jinja2==3.0.3 |
|||
Mako==1.1.6 |
|||
MarkupSafe==2.0.1 |
|||
mypy-extensions==0.4.3 |
|||
passlib==1.7.4 |
|||
pathspec==0.9.0 |
|||
Pillow==8.3.2 |
|||
platformdirs==2.4.0 |
|||
pycparser==2.21 |
|||
python-magic==0.4.24 |
|||
pytz==2021.3 |
|||
regex==2021.11.10 |
|||
six==1.16.0 |
|||
speaklater==1.3 |
|||
SQLAlchemy==1.4.27 |
|||
tomli==1.2.2 |
|||
typing-extensions==4.0.1 |
|||
Werkzeug==2.0.2 |
|||
WTForms==3.0.0 |
@ -0,0 +1,38 @@ |
|||
from flask import Flask |
|||
from flask_sqlalchemy import SQLAlchemy |
|||
from flask_bcrypt import Bcrypt |
|||
from flask_migrate import Migrate |
|||
from flask_wtf.csrf import CSRFProtect |
|||
from flask_login import ( |
|||
LoginManager, |
|||
) |
|||
|
|||
db = SQLAlchemy() |
|||
migrate = Migrate() |
|||
bcrypt = Bcrypt() |
|||
login_manager = LoginManager() |
|||
|
|||
|
|||
def create_app(): |
|||
APP = Flask(__name__, static_folder="static") |
|||
|
|||
APP.secret_key = 'secret-key' |
|||
APP.config['SQLALCHEMY_DATABASE_URI'] = "sqlite:///data/login.db" |
|||
APP.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True |
|||
|
|||
login_manager.session_protection = "strong" |
|||
login_manager.login_view = "login" |
|||
login_manager.login_message_category = "info" |
|||
|
|||
csrf = CSRFProtect() |
|||
|
|||
APP.config['SECRET_KEY'] = 'ty4425hk54a21eee5719b9s9df7sdfklx' |
|||
APP.config['UPLOAD_FOLDER'] = 'tmpupload' |
|||
|
|||
csrf.init_app(APP) |
|||
login_manager.init_app(APP) |
|||
db.init_app(APP) |
|||
migrate.init_app(APP, db) |
|||
bcrypt.init_app(APP) |
|||
|
|||
return APP |
@ -0,0 +1,17 @@ |
|||
def deploy(): |
|||
"""Run deployment of database.""" |
|||
from app import create_app, db |
|||
from flask_migrate import upgrade, migrate, init, stamp |
|||
|
|||
app = create_app() |
|||
app.app_context().push() |
|||
db.create_all() |
|||
|
|||
# migrate database to latest revision |
|||
init() |
|||
stamp() |
|||
migrate() |
|||
upgrade() |
|||
|
|||
|
|||
deploy() |
@ -0,0 +1,23 @@ |
|||
"""Login form to validate user.""" |
|||
from wtforms import ( |
|||
StringField, |
|||
SubmitField, |
|||
PasswordField, |
|||
) |
|||
|
|||
from wtforms import validators |
|||
from wtforms.validators import Length, Email |
|||
from flask_wtf import FlaskForm |
|||
|
|||
|
|||
class LoginForm(FlaskForm): |
|||
"""Login distribusiverse form class.""" |
|||
|
|||
email = StringField( |
|||
"Email address:", |
|||
validators=[validators.InputRequired(), Email(), Length(6, 64)], |
|||
) |
|||
password = PasswordField( |
|||
"Password:", validators=[validators.InputRequired(), Length(12, 72)] |
|||
) |
|||
submit = SubmitField("Sign In") |
@ -0,0 +1 @@ |
|||
Single-database configuration for Flask. |
@ -0,0 +1,50 @@ |
|||
# A generic, single database configuration. |
|||
|
|||
[alembic] |
|||
# template used to generate migration files |
|||
# file_template = %%(rev)s_%%(slug)s |
|||
|
|||
# set to 'true' to run the environment during |
|||
# the 'revision' command, regardless of autogenerate |
|||
# revision_environment = false |
|||
|
|||
|
|||
# Logging configuration |
|||
[loggers] |
|||
keys = root,sqlalchemy,alembic,flask_migrate |
|||
|
|||
[handlers] |
|||
keys = console |
|||
|
|||
[formatters] |
|||
keys = generic |
|||
|
|||
[logger_root] |
|||
level = WARN |
|||
handlers = console |
|||
qualname = |
|||
|
|||
[logger_sqlalchemy] |
|||
level = WARN |
|||
handlers = |
|||
qualname = sqlalchemy.engine |
|||
|
|||
[logger_alembic] |
|||
level = INFO |
|||
handlers = |
|||
qualname = alembic |
|||
|
|||
[logger_flask_migrate] |
|||
level = INFO |
|||
handlers = |
|||
qualname = flask_migrate |
|||
|
|||
[handler_console] |
|||
class = StreamHandler |
|||
args = (sys.stderr,) |
|||
level = NOTSET |
|||
formatter = generic |
|||
|
|||
[formatter_generic] |
|||
format = %(levelname)-5.5s [%(name)s] %(message)s |
|||
datefmt = %H:%M:%S |
@ -0,0 +1,91 @@ |
|||
from __future__ import with_statement |
|||
|
|||
import logging |
|||
from logging.config import fileConfig |
|||
|
|||
from flask import current_app |
|||
|
|||
from alembic import context |
|||
|
|||
# this is the Alembic Config object, which provides |
|||
# access to the values within the .ini file in use. |
|||
config = context.config |
|||
|
|||
# Interpret the config file for Python logging. |
|||
# This line sets up loggers basically. |
|||
fileConfig(config.config_file_name) |
|||
logger = logging.getLogger('alembic.env') |
|||
|
|||
# add your model's MetaData object here |
|||
# for 'autogenerate' support |
|||
# from myapp import mymodel |
|||
# target_metadata = mymodel.Base.metadata |
|||
config.set_main_option( |
|||
'sqlalchemy.url', |
|||
str(current_app.extensions['migrate'].db.get_engine().url).replace( |
|||
'%', '%%')) |
|||
target_metadata = current_app.extensions['migrate'].db.metadata |
|||
|
|||
# other values from the config, defined by the needs of env.py, |
|||
# can be acquired: |
|||
# my_important_option = config.get_main_option("my_important_option") |
|||
# ... etc. |
|||
|
|||
|
|||
def run_migrations_offline(): |
|||
"""Run migrations in 'offline' mode. |
|||
|
|||
This configures the context with just a URL |
|||
and not an Engine, though an Engine is acceptable |
|||
here as well. By skipping the Engine creation |
|||
we don't even need a DBAPI to be available. |
|||
|
|||
Calls to context.execute() here emit the given string to the |
|||
script output. |
|||
|
|||
""" |
|||
url = config.get_main_option("sqlalchemy.url") |
|||
context.configure( |
|||
url=url, target_metadata=target_metadata, literal_binds=True |
|||
) |
|||
|
|||
with context.begin_transaction(): |
|||
context.run_migrations() |
|||
|
|||
|
|||
def run_migrations_online(): |
|||
"""Run migrations in 'online' mode. |
|||
|
|||
In this scenario we need to create an Engine |
|||
and associate a connection with the context. |
|||
|
|||
""" |
|||
|
|||
# this callback is used to prevent an auto-migration from being generated |
|||
# when there are no changes to the schema |
|||
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html |
|||
def process_revision_directives(context, revision, directives): |
|||
if getattr(config.cmd_opts, 'autogenerate', False): |
|||
script = directives[0] |
|||
if script.upgrade_ops.is_empty(): |
|||
directives[:] = [] |
|||
logger.info('No changes in schema detected.') |
|||
|
|||
connectable = current_app.extensions['migrate'].db.get_engine() |
|||
|
|||
with connectable.connect() as connection: |
|||
context.configure( |
|||
connection=connection, |
|||
target_metadata=target_metadata, |
|||
process_revision_directives=process_revision_directives, |
|||
**current_app.extensions['migrate'].configure_args |
|||
) |
|||
|
|||
with context.begin_transaction(): |
|||
context.run_migrations() |
|||
|
|||
|
|||
if context.is_offline_mode(): |
|||
run_migrations_offline() |
|||
else: |
|||
run_migrations_online() |
@ -0,0 +1,24 @@ |
|||
"""${message} |
|||
|
|||
Revision ID: ${up_revision} |
|||
Revises: ${down_revision | comma,n} |
|||
Create Date: ${create_date} |
|||
|
|||
""" |
|||
from alembic import op |
|||
import sqlalchemy as sa |
|||
${imports if imports else ""} |
|||
|
|||
# revision identifiers, used by Alembic. |
|||
revision = ${repr(up_revision)} |
|||
down_revision = ${repr(down_revision)} |
|||
branch_labels = ${repr(branch_labels)} |
|||
depends_on = ${repr(depends_on)} |
|||
|
|||
|
|||
def upgrade(): |
|||
${upgrades if upgrades else "pass"} |
|||
|
|||
|
|||
def downgrade(): |
|||
${downgrades if downgrades else "pass"} |
@ -0,0 +1,32 @@ |
|||
"""Register form to make a new user.""" |
|||
from wtforms import ( |
|||
StringField, |
|||
SubmitField, |
|||
PasswordField, |
|||
) |
|||
|
|||
from wtforms import validators |
|||
from wtforms.validators import Length, Email, EqualTo |
|||
from flask_wtf import FlaskForm |
|||
|
|||
|
|||
class RegisterForm(FlaskForm): |
|||
"""Register for distribusi-verse form class""" |
|||
|
|||
email = StringField( |
|||
"Email address:", |
|||
validators=[validators.InputRequired(), Email(), Length(6, 64)], |
|||
) |
|||
password = PasswordField( |
|||
"New password:", |
|||
validators=[validators.InputRequired(), Length(12, 72)], |
|||
) |
|||
confirmpassword = PasswordField( |
|||
"Confirm your password:", |
|||
validators=[ |
|||
validators.InputRequired(), |
|||
Length(12, 72), |
|||
EqualTo("password", message="Passwords must match !"), |
|||
], |
|||
) |
|||
submit = SubmitField("Register to Distribusi-verse") |
@ -0,0 +1,139 @@ |
|||
"""This is the main flask distribusi page""" |
|||
from flask import ( |
|||
render_template, |
|||
redirect, |
|||
request, |
|||
flash, |
|||
url_for, |
|||
session, |
|||
abort, |
|||
is_safe_url, |
|||
) |
|||
from sqlalchemy.exc import ( |
|||
IntegrityError, |
|||
DataError, |
|||
DatabaseError, |
|||
InterfaceError, |
|||
InvalidRequestError, |
|||
) |
|||
from flask_login import ( |
|||
login_user, |
|||
logout_user, |
|||
login_required, |
|||
) |
|||
|
|||
from werkzeug.routing import BuildError |
|||
from flask_bcrypt import generate_password_hash, check_password_hash |
|||
from flask_wtf.csrf import CSRFError |
|||
from datetime import timedelta |
|||
|
|||
from app import create_app, db, login_manager |
|||
from usermodel import User |
|||
from loginform import LoginForm |
|||
from registerform import RegisterForm |
|||
|
|||
APP = create_app() |
|||
|
|||
|
|||
@APP.before_request |
|||
def session_handler(): |
|||
session.permanent = True |
|||
APP.permanent_session_lifetime = timedelta(minutes=1) |
|||
|
|||
|
|||
@APP.route("/") |
|||
def index(): |
|||
return render_template("index.html") |
|||
|
|||
|
|||
@APP.route("/login", methods=["GET", "POST"]) |
|||
def login(): |
|||
loginform = LoginForm() |
|||
if loginform.validate_on_submit(): |
|||
try: |
|||
user = User.query.filter_by(email=loginform.email.data).first() |
|||
if check_password_hash(user.pwd, loginform.password.data): |
|||
login_user(user) |
|||
flash("Logged in successfully.", "success") |
|||
next = request.args.get("next") |
|||
if next is not None and not is_safe_url(next): |
|||
return abort(400) |
|||
return redirect(next or url_for("index")) |
|||
else: |
|||
flash("Invalid Username or password!", "danger") |
|||
except Exception as e: |
|||
flash(e, "danger") |
|||
return render_template("login.html", loginform=loginform) |
|||
|
|||
|
|||
@APP.route("/register", methods=["GET", "POST"]) |
|||
def register(): |
|||
registerform = RegisterForm() |
|||
if registerform.validate_on_submit(): |
|||
try: |
|||
email = registerform.email.data |
|||
pwd = registerform.confirmpassword.data |
|||
|
|||
newuser = User( |
|||
email=email, |
|||
pwd=generate_password_hash(pwd), |
|||
) |
|||
|
|||
db.session.add(newuser) |
|||
db.session.commit() |
|||
flash("Account Succesfully created", "success") |
|||
return redirect(url_for("login")) |
|||
|
|||
except InvalidRequestError: |
|||
db.session.rollback() |
|||
flash("Something went wrong!", "danger") |
|||
except IntegrityError: |
|||
db.session.rollback() |
|||
flash("User already exists!.", "warning") |
|||
except DataError: |
|||
db.session.rollback() |
|||
flash("Invalid Entry", "warning") |
|||
except InterfaceError: |
|||
db.session.rollback() |
|||
flash("Error connecting to the database", "danger") |
|||
except DatabaseError: |
|||
db.session.rollback() |
|||
flash("Error connecting to the database", "danger") |
|||
except BuildError: |
|||
db.session.rollback() |
|||
flash("An error occured !", "danger") |
|||
return render_template("register.html", registerform=registerform) |
|||
|
|||
|
|||
@APP.route("/distribusi") |
|||
@login_required |
|||
def distribusi(): |
|||
return "distribusi" |
|||
|
|||
|
|||
@APP.route("/admin") |
|||
@login_required |
|||
def admin(): |
|||
return "admin" |
|||
|
|||
|
|||
@APP.route("/logout") |
|||
@login_required |
|||
def logout(): |
|||
logout_user() |
|||
return redirect(url_for("login")) |
|||
|
|||
|
|||
@APP.errorhandler(CSRFError) |
|||
def handle_csrf_error(e): |
|||
return render_template("csrf_error.html", reason=e.description), 400 |
|||
|
|||
|
|||
@login_manager.user_loader |
|||
def load_user(user_id): |
|||
return User.query.get(int(user_id)) |
|||
|
|||
|
|||
if __name__ == "__main__": |
|||
APP.debug = True |
|||
APP.run(port=5000) |
@ -0,0 +1,54 @@ |
|||
body |
|||
{ |
|||
font-family: monospace; |
|||
background-color: black; |
|||
color:#E0B0FF; |
|||
} |
|||
|
|||
section#login{ |
|||
width: 30%; |
|||
margin-left: auto; |
|||
margin-right: auto; |
|||
background-color:black; |
|||
text-decoration: none; |
|||
} |
|||
|
|||
section#login form { |
|||
width: 200px; |
|||
margin: 0 auto; |
|||
padding-left: 15%; |
|||
padding-right: 15%; |
|||
} |
|||
|
|||
section#login .required{ |
|||
padding: 6px; |
|||
border: none; |
|||
float: left; |
|||
} |
|||
|
|||
section#login input[type=text], input[type=password]{ |
|||
background-color: black; |
|||
color: white; |
|||
border: 1px solid #E0B0FF; |
|||
} |
|||
|
|||
section#buttons{ |
|||
position: fixed; |
|||
top: 0.5em; |
|||
right: 0.5em; |
|||
display:flex; |
|||
flex-direction: row; |
|||
justify-content: center; |
|||
align-items: center; |
|||
} |
|||
|
|||
.signin input { |
|||
border: none; |
|||
background: #E0B0FF; |
|||
text-decoration: none; |
|||
margin: 1px; |
|||
} |
|||
|
|||
.signin input:hover { |
|||
background: #60337F; |
|||
} |
@ -0,0 +1,6 @@ |
|||
This favicon was generated using the following font: |
|||
|
|||
- Font Title: Klee One |
|||
- Font Author: Copyright 2020 The Klee Project Authors (https://github.com/fontworks-fonts/Klee) |
|||
- Font Source: http://fonts.gstatic.com/s/kleeone/v5/LDIxapCLNRc6A8oT4q4AOeekWPrP.ttf |
|||
- Font License: SIL Open Font License, 1.1 (http://scripts.sil.org/OFL)) |
After Width: | Height: | Size: 5.4 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 322 B |
After Width: | Height: | Size: 666 B |
After Width: | Height: | Size: 15 KiB |
@ -0,0 +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"} |
@ -0,0 +1,5 @@ |
|||
console.log("everything is still smooth") |
|||
|
|||
// function(e) {
|
|||
// (e.keyCode === 13 || e.keyCode === 32) && $(this).trigger("click")
|
|||
// }
|
@ -0,0 +1,24 @@ |
|||
<!DOCTYPE html> |
|||
<html lang='en'> |
|||
<head> |
|||
<meta charset="utf-8" /> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1"> |
|||
<title>Autonomous Practices X Distribusi-Verse</title> |
|||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/style.css')}}"> |
|||
<link rel="shortcut icon" href="{{ url_for('static', filename='icons/favicon.ico') }}"> |
|||
<link rel="apple-touch-icon" sizes="180x180" href="{{ url_for('static', filename='icons/apple-touch-icon.png')}}"> |
|||
<link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='icons/favicon-32x32.png')}}"> |
|||
<link rel="icon" type="image/png" sizes="16x16" href="{{ url_for('static', filename='icons/favicon-16x16.png')}}"> |
|||
<link rel="manifest" href="{{ url_for('static', filename='icons/site.webmanifest')}}"> |
|||
</head> |
|||
<body> |
|||
{% block main %} |
|||
{% endblock main %} |
|||
|
|||
<section id="summary"> |
|||
|
|||
</section> |
|||
|
|||
</body> |
|||
<script src="{{ url_for('static', filename='js/script.js')}}"></script> |
|||
</html> |
@ -0,0 +1,32 @@ |
|||
{% extends "base.html" %} |
|||
{% block main %} |
|||
<section id="buttons"> |
|||
<div class="signin"> |
|||
<a href="/login"> |
|||
<input type="button" name="button" value="Sign in"></input> |
|||
</a> |
|||
</div> |
|||
<div class="signin"> |
|||
<a href="/register"> |
|||
<input type="button" name="button" value="Register"></input> |
|||
</a> |
|||
</div> |
|||
</section> |
|||
|
|||
{% if current_user.is_authenticated %} |
|||
<h2> Hi {{ current_user.email }}! </h2> |
|||
{% endif %} |
|||
|
|||
<!-- a section with all the distribusis listed in the distribusiverse --> |
|||
<section id="distribusiverse"> |
|||
<h2>List of distribusis</h2> |
|||
<ul> |
|||
<li>CCL</li> |
|||
<li>Crunk</li> |
|||
<li>CMOS4010</li> |
|||
<li>CMOS4046</li> |
|||
<li>Other Names</li> |
|||
<li>List of stuff</li> |
|||
</ul> |
|||
</section> |
|||
{% endblock %} |
@ -0,0 +1,26 @@ |
|||
{% extends "base.html" %} |
|||
{% block main %} |
|||
<section id="login"> |
|||
<form class="form" action="{{ url_for('login') }}" method="post"> |
|||
{{ loginform.csrf_token }} |
|||
<fieldset class="required"> |
|||
{{ loginform.email.label }} |
|||
{{ loginform.email }} |
|||
{% for message in loginform.email.errors %} |
|||
<div class="error">{{ message }}</div> |
|||
{% endfor %} |
|||
</fieldset> |
|||
<fieldset class="required"> |
|||
{{ loginform.password.label }} |
|||
{{ loginform.password }} |
|||
{% for message in loginform.password.errors %} |
|||
<div class="error">{{ message }}</div> |
|||
{% endfor %} |
|||
</fieldset> |
|||
<fieldset class="signin required"> |
|||
{{ loginform.submit }} |
|||
<a href="/forgotpassword">Forgot Password?</a> |
|||
</fieldset> |
|||
</form> |
|||
</section> |
|||
{% endblock main %} |
@ -0,0 +1,32 @@ |
|||
{% extends "base.html" %} |
|||
{% block main %} |
|||
<section id="login"> |
|||
<form class="form" action="{{ url_for('register') }}" method="post"> |
|||
{{ registerform.csrf_token }} |
|||
<fieldset class="required"> |
|||
{{ registerform.email.label }} |
|||
{{ registerform.email }} |
|||
{% for message in registerform.email.errors %} |
|||
<div class="error">{{ message }}</div> |
|||
{% endfor %} |
|||
</fieldset> |
|||
<fieldset class="required"> |
|||
{{ registerform.password.label }} |
|||
{{ registerform.password }} |
|||
{% for message in registerform.password.errors %} |
|||
<div class="error">{{ message }}</div> |
|||
{% endfor %} |
|||
</fieldset> |
|||
<fieldset class="required"> |
|||
{{ registerform.confirmpassword.label }} |
|||
{{ registerform.confirmpassword }} |
|||
{% for message in registerform.confirmpassword.errors %} |
|||
<div class="error">{{ message }}</div> |
|||
{% endfor %} |
|||
</fieldset> |
|||
<fieldset class="signin required"> |
|||
{{ registerform.submit }} |
|||
</fieldset> |
|||
</form> |
|||
</section> |
|||
{% endblock main %} |
@ -0,0 +1,16 @@ |
|||
from app import db |
|||
from flask_login import UserMixin |
|||
|
|||
|
|||
class User(UserMixin, db.Model): |
|||
"""User model class for a user in distribusi-verse""" |
|||
|
|||
__tablename__ = "users" |
|||
|
|||
id = db.Column(db.Integer, primary_key=True) |
|||
distribusiname = db.Column(db.String(100), unique=True, nullable=True) |
|||
email = db.Column(db.String(150), unique=True, nullable=False) |
|||
pwd = db.Column(db.String(300), nullable=False, unique=False) |
|||
|
|||
def __repr__(self): |
|||
return "<User %r>" % self.username |
Loading…
Reference in new issue