Browse Source

first commit of distribusi-verse

master
crunk 6 months ago
commit
876a44633f
  1. 10
      .gitignore
  2. 3
      .gitmodules
  3. 30
      README.md
  4. 0
      __init.py__
  5. 1
      distribusi
  6. 0
      makefile
  7. 25
      pyproject.toml
  8. 39
      requirements.txt
  9. 0
      verse/__init.py__
  10. 38
      verse/app.py
  11. 17
      verse/deploydb.py
  12. 23
      verse/loginform.py
  13. 1
      verse/migrations/README
  14. 50
      verse/migrations/alembic.ini
  15. 91
      verse/migrations/env.py
  16. 24
      verse/migrations/script.py.mako
  17. 32
      verse/registerform.py
  18. 139
      verse/start.py
  19. 54
      verse/static/css/style.css
  20. 6
      verse/static/icons/about.txt
  21. BIN
      verse/static/icons/android-chrome-192x192.png
  22. BIN
      verse/static/icons/android-chrome-512x512.png
  23. BIN
      verse/static/icons/apple-touch-icon.png
  24. BIN
      verse/static/icons/favicon-16x16.png
  25. BIN
      verse/static/icons/favicon-32x32.png
  26. BIN
      verse/static/icons/favicon.ico
  27. 1
      verse/static/icons/site.webmanifest
  28. 5
      verse/static/js/script.js
  29. 24
      verse/templates/base.html
  30. 32
      verse/templates/index.html
  31. 26
      verse/templates/login.html
  32. 32
      verse/templates/register.html
  33. 16
      verse/usermodel.py

10
.gitignore

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

3
.gitmodules

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

30
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.

0
__init.py__

1
distribusi

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

0
makefile

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

39
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

0
verse/__init.py__

38
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

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

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

1
verse/migrations/README

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

50
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

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

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

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

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

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

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

BIN
verse/static/icons/android-chrome-192x192.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
verse/static/icons/android-chrome-512x512.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
verse/static/icons/apple-touch-icon.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
verse/static/icons/favicon-16x16.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 B

BIN
verse/static/icons/favicon-32x32.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 666 B

BIN
verse/static/icons/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

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

5
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")
// }

24
verse/templates/base.html

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

32
verse/templates/index.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 %}

26
verse/templates/login.html

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

32
verse/templates/register.html

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

@ -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…
Cancel
Save