first commit of distribusi-verse

This commit is contained in:
crunk 2022-01-05 13:42:24 +01:00
commit 876a44633f
33 changed files with 719 additions and 0 deletions

10
.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
/.venv/
/__pycache__/
*.pyc
*.egg-info/
.eggs/
build/
dist/
pip-wheel-metadata/
*.db

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "distribusi"]
path = distribusi
url = https://git.vvvvvvaria.org/varia/distribusi

30
README.md Normal file
View File

@ -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
__init.py__ Normal file
View File

1
distribusi Submodule

@ -0,0 +1 @@
Subproject commit 63becedaf51d069744b4088253d883d08e883aa1

0
makefile Normal file
View File

25
pyproject.toml Normal file
View File

@ -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
)/
'''

39
requirements.txt Normal file
View File

@ -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
verse/__init.py__ Normal file
View File

38
verse/app.py Normal file
View File

@ -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

17
verse/deploydb.py Normal file
View File

@ -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()

23
verse/loginform.py Normal file
View File

@ -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")

1
verse/migrations/README Normal file
View File

@ -0,0 +1 @@
Single-database configuration for Flask.

View File

@ -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

91
verse/migrations/env.py Normal file
View File

@ -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()

View File

@ -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"}

32
verse/registerform.py Normal file
View File

@ -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")

139
verse/start.py Normal file
View File

@ -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)

View File

@ -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;
}

View File

@ -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))

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 666 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -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"}

View File

@ -0,0 +1,5 @@
console.log("everything is still smooth")
// function(e) {
// (e.keyCode === 13 || e.keyCode === 32) && $(this).trigger("click")
// }

24
verse/templates/base.html Normal file
View File

@ -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>

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

16
verse/usermodel.py Normal file
View File

@ -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