commit 876a44633f79c911221f455e2137f0fd05b98c85 Author: crunk Date: Wed Jan 5 13:42:24 2022 +0100 first commit of distribusi-verse diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d6e2191 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +/.venv/ +/__pycache__/ +*.pyc +*.egg-info/ +.eggs/ +build/ +dist/ +pip-wheel-metadata/ + +*.db diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..ed55fb8 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "distribusi"] + path = distribusi + url = https://git.vvvvvvaria.org/varia/distribusi diff --git a/README.md b/README.md new file mode 100644 index 0000000..60889c8 --- /dev/null +++ b/README.md @@ -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. diff --git a/__init.py__ b/__init.py__ new file mode 100644 index 0000000..e69de29 diff --git a/distribusi b/distribusi new file mode 160000 index 0000000..63beced --- /dev/null +++ b/distribusi @@ -0,0 +1 @@ +Subproject commit 63becedaf51d069744b4088253d883d08e883aa1 diff --git a/makefile b/makefile new file mode 100644 index 0000000..e69de29 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..27f62cd --- /dev/null +++ b/pyproject.toml @@ -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 +)/ +''' + + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..085ac44 --- /dev/null +++ b/requirements.txt @@ -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 diff --git a/verse/__init.py__ b/verse/__init.py__ new file mode 100644 index 0000000..e69de29 diff --git a/verse/app.py b/verse/app.py new file mode 100644 index 0000000..8921454 --- /dev/null +++ b/verse/app.py @@ -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 diff --git a/verse/deploydb.py b/verse/deploydb.py new file mode 100644 index 0000000..3815b02 --- /dev/null +++ b/verse/deploydb.py @@ -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() diff --git a/verse/loginform.py b/verse/loginform.py new file mode 100644 index 0000000..79da845 --- /dev/null +++ b/verse/loginform.py @@ -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") diff --git a/verse/migrations/README b/verse/migrations/README new file mode 100644 index 0000000..0e04844 --- /dev/null +++ b/verse/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/verse/migrations/alembic.ini b/verse/migrations/alembic.ini new file mode 100644 index 0000000..ec9d45c --- /dev/null +++ b/verse/migrations/alembic.ini @@ -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 diff --git a/verse/migrations/env.py b/verse/migrations/env.py new file mode 100644 index 0000000..68feded --- /dev/null +++ b/verse/migrations/env.py @@ -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() diff --git a/verse/migrations/script.py.mako b/verse/migrations/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/verse/migrations/script.py.mako @@ -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"} diff --git a/verse/registerform.py b/verse/registerform.py new file mode 100644 index 0000000..d69b5f3 --- /dev/null +++ b/verse/registerform.py @@ -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") diff --git a/verse/start.py b/verse/start.py new file mode 100644 index 0000000..858b1b2 --- /dev/null +++ b/verse/start.py @@ -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) diff --git a/verse/static/css/style.css b/verse/static/css/style.css new file mode 100644 index 0000000..28317da --- /dev/null +++ b/verse/static/css/style.css @@ -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; +} diff --git a/verse/static/icons/about.txt b/verse/static/icons/about.txt new file mode 100644 index 0000000..f6a9f45 --- /dev/null +++ b/verse/static/icons/about.txt @@ -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)) diff --git a/verse/static/icons/android-chrome-192x192.png b/verse/static/icons/android-chrome-192x192.png new file mode 100644 index 0000000..3b958ff Binary files /dev/null and b/verse/static/icons/android-chrome-192x192.png differ diff --git a/verse/static/icons/android-chrome-512x512.png b/verse/static/icons/android-chrome-512x512.png new file mode 100644 index 0000000..1d68c63 Binary files /dev/null and b/verse/static/icons/android-chrome-512x512.png differ diff --git a/verse/static/icons/apple-touch-icon.png b/verse/static/icons/apple-touch-icon.png new file mode 100644 index 0000000..97255bd Binary files /dev/null and b/verse/static/icons/apple-touch-icon.png differ diff --git a/verse/static/icons/favicon-16x16.png b/verse/static/icons/favicon-16x16.png new file mode 100644 index 0000000..05a6e48 Binary files /dev/null and b/verse/static/icons/favicon-16x16.png differ diff --git a/verse/static/icons/favicon-32x32.png b/verse/static/icons/favicon-32x32.png new file mode 100644 index 0000000..8478c1e Binary files /dev/null and b/verse/static/icons/favicon-32x32.png differ diff --git a/verse/static/icons/favicon.ico b/verse/static/icons/favicon.ico new file mode 100644 index 0000000..f3757b9 Binary files /dev/null and b/verse/static/icons/favicon.ico differ diff --git a/verse/static/icons/site.webmanifest b/verse/static/icons/site.webmanifest new file mode 100644 index 0000000..45dc8a2 --- /dev/null +++ b/verse/static/icons/site.webmanifest @@ -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"} \ No newline at end of file diff --git a/verse/static/js/script.js b/verse/static/js/script.js new file mode 100644 index 0000000..534d428 --- /dev/null +++ b/verse/static/js/script.js @@ -0,0 +1,5 @@ +console.log("everything is still smooth") + +// function(e) { +// (e.keyCode === 13 || e.keyCode === 32) && $(this).trigger("click") +// } diff --git a/verse/templates/base.html b/verse/templates/base.html new file mode 100644 index 0000000..0818d9e --- /dev/null +++ b/verse/templates/base.html @@ -0,0 +1,24 @@ + + + + + + Autonomous Practices X Distribusi-Verse + + + + + + + + + {% block main %} + {% endblock main %} + +
+ +
+ + + + diff --git a/verse/templates/index.html b/verse/templates/index.html new file mode 100644 index 0000000..43dfa5d --- /dev/null +++ b/verse/templates/index.html @@ -0,0 +1,32 @@ +{% extends "base.html" %} +{% block main %} +
+ + +
+ +{% if current_user.is_authenticated %} +

Hi {{ current_user.email }}!

+{% endif %} + + +
+

List of distribusis

+ +
+{% endblock %} diff --git a/verse/templates/login.html b/verse/templates/login.html new file mode 100644 index 0000000..d435098 --- /dev/null +++ b/verse/templates/login.html @@ -0,0 +1,26 @@ +{% extends "base.html" %} +{% block main %} +
+
+ {{ loginform.csrf_token }} +
+ {{ loginform.email.label }} + {{ loginform.email }} + {% for message in loginform.email.errors %} +
{{ message }}
+ {% endfor %} +
+
+ {{ loginform.password.label }} + {{ loginform.password }} + {% for message in loginform.password.errors %} +
{{ message }}
+ {% endfor %} +
+ +
+
+{% endblock main %} diff --git a/verse/templates/register.html b/verse/templates/register.html new file mode 100644 index 0000000..d2c4338 --- /dev/null +++ b/verse/templates/register.html @@ -0,0 +1,32 @@ +{% extends "base.html" %} +{% block main %} +
+
+ {{ registerform.csrf_token }} +
+ {{ registerform.email.label }} + {{ registerform.email }} + {% for message in registerform.email.errors %} +
{{ message }}
+ {% endfor %} +
+
+ {{ registerform.password.label }} + {{ registerform.password }} + {% for message in registerform.password.errors %} +
{{ message }}
+ {% endfor %} +
+
+ {{ registerform.confirmpassword.label }} + {{ registerform.confirmpassword }} + {% for message in registerform.confirmpassword.errors %} +
{{ message }}
+ {% endfor %} +
+ +
+
+{% endblock main %} diff --git a/verse/usermodel.py b/verse/usermodel.py new file mode 100644 index 0000000..78645e9 --- /dev/null +++ b/verse/usermodel.py @@ -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 "" % self.username