diff --git a/.gitignore b/.gitignore index d4ac028..32bd3b0 100644 --- a/.gitignore +++ b/.gitignore @@ -191,5 +191,5 @@ flycheck_*.el # network security /network-security.data - - +.flaskenv +app/app.db diff --git a/app/file.py b/app/file.py new file mode 100644 index 0000000..db4c899 --- /dev/null +++ b/app/file.py @@ -0,0 +1,15 @@ +import mimetypes +from os.path import join, basename, dirname +from werkzeug.utils import secure_filename +from slugify import slugify +mimetypes.init() + +def get_url_for(file, username, collection_slug, piece_slug): + name = f'{username}-{collection_slug}-{piece_slug}-{secure_filename(basename(file.filename))}' + path = join(dirname(__file__), "static", name) + file.save(path) + + return f"/static/{name}" + +def get_type_for(file): + return mimetypes.guess_type(file.filename)[0] diff --git a/app/forms.py b/app/forms.py index 23ec097..beca0aa 100644 --- a/app/forms.py +++ b/app/forms.py @@ -1,6 +1,8 @@ from flask_wtf import FlaskForm +from flask_wtf.file import FileField, FileRequired from wtforms import StringField, PasswordField, BooleanField, SubmitField from wtforms.validators import DataRequired, ValidationError, URL +from werkzeug.utils import secure_filename from .models import CopyrightHolder class LoginForm(FlaskForm): @@ -9,6 +11,17 @@ class LoginForm(FlaskForm): remember = BooleanField('🍪') submit = SubmitField('login') +class CollectionForm(FlaskForm): + name = StringField('name', validators=[DataRequired()]) + description = StringField('description', validators=[DataRequired()]) + submit = SubmitField('make') + +class PieceForm(FlaskForm): + name = StringField('name', validators=[DataRequired()]) + description = StringField('description', validators=[DataRequired()]) + file = FileField('file', validators=[FileRequired()]) + submit = SubmitField('save') + class RegistrationForm(FlaskForm): name = StringField('username', validators=[DataRequired()]) password = PasswordField('password', validators=[DataRequired()]) diff --git a/app/models.py b/app/models.py index ff61364..332c6a4 100644 --- a/app/models.py +++ b/app/models.py @@ -29,20 +29,20 @@ def is_admin(self): class Collection(db.Model): id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(128), index=True, unique=True) + name = db.Column(db.String(128), index=True) description = db.Column(db.String(256)) slug = db.Column(db.String(128), index=True) copyright_holder_id = db.Column(db.Integer, db.ForeignKey('copyright_holder.id')) - password_hash = db.Column(db.String(128)) pieces = db.relationship('Piece', backref='collection', lazy='dynamic') def __repr__(self): return f'' class Piece(db.Model): id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(128), index=True, unique=True) + name = db.Column(db.String(128), index=True) + slug = db.Column(db.String(128), index=True) description = db.Column(db.String(256)) - url = db.Column(db.String(128)) + url = db.Column(db.String(256)) timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow) type = db.Column(db.String(10), index=True) collection_id = db.Column(db.Integer, db.ForeignKey('collection.id')) diff --git a/app/routes.py b/app/routes.py index 1054909..85ec906 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,22 +1,26 @@ from flask import render_template, flash, redirect, url_for -from flask_login import current_user, login_user +from flask_login import current_user, login_user, login_required from . import app from . import db -from .forms import LoginForm, RegistrationForm +from .forms import LoginForm, RegistrationForm, CollectionForm, PieceForm from .models import CopyrightHolder, Collection, Piece - +from .file import get_url_for, get_type_for +from slugify import slugify from os import scandir from os.path import join, dirname + @app.route('/') def index(): return render_template("index.html", collections=Collection.query.all()) +@app.route('/logout/') @app.route('/logout') def logout(): return render_template("base.html") +@app.route('/register/', methods=['GET', 'POST']) @app.route('/register', methods=['GET', 'POST']) def register(): form = RegistrationForm() @@ -32,6 +36,7 @@ def register(): return redirect(url_for('index')) return render_template('register.html', title="make user", form=form) +@app.route('/login/', methods=['GET', 'POST']) @app.route('/login', methods=['GET', 'POST']) def login(): if current_user.is_authenticated: @@ -47,26 +52,117 @@ def login(): return render_template('login.html', title="login", form=form) @app.route('/') +@app.route('//') def copyright_holder(username): ch = CopyrightHolder.query.filter_by(username=username).first() if ch is None: - return "404" + flash("404") + return redirect(url_for('index')) return render_template("copyright_holder.html", copyright_holder=ch) -@app.route('//new') + +@app.route('//new', methods=['GET', 'POST']) +@app.route('//new/', methods=['GET', 'POST']) +@login_required def new_collection(username): - return render_template("base.html") + ch = CopyrightHolder.query.filter_by(username=username).first() + if ch is None: + flash("404") + return redirect(url_for('index')) + if current_user.id != ch.id: + flash("hey, that's not yours") + return redirect(url_for('index')) + form = CollectionForm() + if form.validate_on_submit(): + slug = slugify(form.name.data) + existing = Collection.query.filter_by(slug=slug).first() + if existing is not None: + flash(f"that name would need the path {username}/{slug}, which is taken") + return redirect(url_for('new_collection', + username=username)) + collection = Collection(name=form.name.data, + description=form.description.data, + slug=slug, + copyright_holder_id=current_user.id) + + db.session.add(collection) + db.session.commit() + + flash(f"{collection.name} created") + return redirect(url_for('collection', username=username, slug=slug)) + return render_template('new_collection.html', + title="create new collection", + form=form) @app.route('//') +@app.route('///') def collection(username, slug): - collection = Collection.query.filter_by(slug=slug).first() + ch = CopyrightHolder.query.filter_by(username=username).first() + collection = ch.collections.filter_by(slug=slug).first() if collection is None: return "404" return render_template("collection.html", - copyright_holder=collection) + copyright_holder=ch, + collection=collection) + +@app.route('///') +@app.route('////') +def piece(username, collection_slug, piece_slug): + ch = CopyrightHolder.query.filter_by(username=username).first() + collection = ch.collections.filter_by(slug=collection_slug).first() + piece = collection.pieces.filter_by(slug=piece_slug).first() + return render_template("_piece.html", + copyright_holder=ch, + collection=collection, + piece=piece) + -@app.route('///new') +@login_required +@app.route('///new', methods=['GET', 'POST']) +@app.route('///new/', methods=['GET', 'POST']) def new_piece(username, slug): - return render_template("base.html") + ch = CopyrightHolder.query.filter_by(username=username).first() + if ch is None: + flash("404") + return redirect(url_for('index')) + if current_user.id != ch.id: + flash("hey, that's not yours") + return redirect(url_for('index')) + collection = Collection.query.filter_by(slug=slug).first() + if collection is None: + flash("404") + return redirect(url_for('copyright_holder', username=username)) + form = PieceForm() + if form.validate_on_submit(): + piece_slug = slugify(form.name.data) + existing = Piece.query.filter_by(slug=slug).first() + if existing is not None: + flash(f"that name would need the path {username}/{slug}/{piece_slug}, which is taken") + return redirect(url_for('new_piece', + username=username, + slug=slug)) + file = form.file.data + url = get_url_for(file, + username=username, + collection_slug=slug, + piece_slug=piece_slug) + type = get_type_for(file) + piece = Piece(name=form.name.data, + description=form.description.data, + slug=piece_slug, + collection_id=collection.id, + type=type, + url=url) + + db.session.add(piece) + db.session.commit() + + flash(f"{piece.name} created") + return redirect(url_for('collection', + username=username, + slug=slug)) + return render_template('new_piece.html', + title=f"add to '{collection.name}'", + form=form) diff --git a/app/static/chee-parts-of-songs-easy-clap-some-trop.flac b/app/static/chee-parts-of-songs-easy-clap-some-trop.flac new file mode 100644 index 0000000..98cb48e Binary files /dev/null and b/app/static/chee-parts-of-songs-easy-clap-some-trop.flac differ diff --git a/app/static/hoohoo b/app/static/hoohoo new file mode 100644 index 0000000..e69de29 diff --git a/app/templates/_piece.html b/app/templates/_piece.html new file mode 100644 index 0000000..f10ec8a --- /dev/null +++ b/app/templates/_piece.html @@ -0,0 +1,49 @@ +

+ {{piece.name}} +

+{{piece.description}} +download +{% autoescape false %} + {{piece.html}} +{% endautoescape %} + +{% if piece.type.startswith("audio/") %} + +{% endif %} + +{% if piece.type.startswith("image/") %} + +{% endif %} + +{% if piece.type.startswith("video/") %} + +{% endif %} + +

+ + CC0 + +
+ To the extent possible under law, + + + {{collection.copyright_holder.legal_name}} + + + has waived all copyright and related or neighboring rights to + + + {{piece.name}} + . + This work is published from: + + {{collection.country}} + . diff --git a/app/templates/collection.html b/app/templates/collection.html index 8ae3e1d..1cf1064 100644 --- a/app/templates/collection.html +++ b/app/templates/collection.html @@ -1,45 +1,14 @@ {% extends "base.html" %} -{% if current_user.is_authenticated and current_user.id == collection.copyright_holder_id %} - - new piece - -{% endif %} +{% block nav %} + {% if current_user.is_authenticated and current_user.id == copyright_holder.id %} + + new piece + + {% endif %} +{% endblock %} {% block content %} -

pieces

- {% for item in collection.pieces %} -

- {{item.description}} - {% autoescape false %} - {{item.html}} - {% endautoescape %} - -

- - CC0 - -
- To the extent possible under law, - - - {{collection.copyright_holder}} - - - has waived all copyright and related or neighboring rights to - - - {{item.name}} - . - This work is published from: - - {{collection.country}} - . - {% endfor %} - {% endblock %} +

{{collection.description}}

+ {% for piece in collection.pieces %} + {% include '_piece.html' %} + {% endfor %} +{% endblock %} diff --git a/app/templates/copyright_holder.html b/app/templates/copyright_holder.html index d6b4f3f..3c3ba93 100644 --- a/app/templates/copyright_holder.html +++ b/app/templates/copyright_holder.html @@ -1,21 +1,22 @@ {% extends "base.html" %} {% block nav %} -{% if current_user.is_authenticated and current_user.id == copyright_holder.id %} - - new collection - -{% endif %} + {% if current_user.is_authenticated and current_user.id == copyright_holder.id %} + + new collection + + {% endif %} {% endblock %} {% block content %}

collections

- {% for collection in copyright_holder.collections %} -
    + - {% endfor %} + {% endfor %} +
{% endblock %} diff --git a/app/templates/index.html b/app/templates/index.html index 4f60db0..fded9b1 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -4,7 +4,15 @@

collections

diff --git a/app/templates/new_collection.html b/app/templates/new_collection.html new file mode 100644 index 0000000..6b86578 --- /dev/null +++ b/app/templates/new_collection.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} +{% block content %} +
+ {{form.hidden_tag()}} +

+ {{form.name.label}}
+ {{form.name(size=32)}} +

+

+ {{form.description.label}}
+ {{form.description(size=32)}} +

+ +

+ {{form.submit()}} +

+
+{% endblock %} diff --git a/app/templates/new_piece.html b/app/templates/new_piece.html new file mode 100644 index 0000000..972f5fc --- /dev/null +++ b/app/templates/new_piece.html @@ -0,0 +1,21 @@ +{% extends "base.html" %} {% block content %} +
+ {{form.hidden_tag()}} +

+ {{form.name.label}}
+ {{form.name(size=32)}} +

+ +

+ {{form.description.label}}
+ {{form.description(size=32)}} +

+ +

+ {{form.file.label}}
+ {{form.file}} +

+ +

{{form.submit()}}

+
+{% endblock %} diff --git a/app/templates/register.html b/app/templates/register.html index 06d356d..8fd185d 100644 --- a/app/templates/register.html +++ b/app/templates/register.html @@ -1,6 +1,6 @@ {% extends "base.html" %} {% block content %} - {% if current_user.is_admin() %} + {% if current_user.is_authenticated and current_user.is_admin() %}
{{form.hidden_tag()}}

diff --git a/migrations/versions/8bf3428c3612_descriptions.py b/migrations/versions/8bf3428c3612_descriptions.py deleted file mode 100644 index 0d41dd8..0000000 --- a/migrations/versions/8bf3428c3612_descriptions.py +++ /dev/null @@ -1,30 +0,0 @@ -"""descriptions - -Revision ID: 8bf3428c3612 -Revises: 15f913cb40db -Create Date: 2020-11-16 15:10:49.779287 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '8bf3428c3612' -down_revision = '15f913cb40db' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('collection', sa.Column('description', sa.String(length=256), nullable=True)) - op.add_column('piece', sa.Column('description', sa.String(length=256), nullable=True)) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('piece', 'description') - op.drop_column('collection', 'description') - # ### end Alembic commands ### diff --git a/migrations/versions/15f913cb40db_initial.py b/migrations/versions/b75d14548ae9_init.py similarity index 86% rename from migrations/versions/15f913cb40db_initial.py rename to migrations/versions/b75d14548ae9_init.py index d569e53..316cf05 100644 --- a/migrations/versions/15f913cb40db_initial.py +++ b/migrations/versions/b75d14548ae9_init.py @@ -1,8 +1,8 @@ -"""initial +"""init -Revision ID: 15f913cb40db +Revision ID: b75d14548ae9 Revises: -Create Date: 2020-11-16 14:47:20.578784 +Create Date: 2020-11-16 19:05:12.885853 """ from alembic import op @@ -10,7 +10,7 @@ # revision identifiers, used by Alembic. -revision = '15f913cb40db' +revision = 'b75d14548ae9' down_revision = None branch_labels = None depends_on = None @@ -34,25 +34,28 @@ def upgrade(): op.create_table('collection', sa.Column('id', sa.Integer(), nullable=False), sa.Column('name', sa.String(length=128), nullable=True), + sa.Column('description', sa.String(length=256), nullable=True), sa.Column('slug', sa.String(length=128), nullable=True), sa.Column('copyright_holder_id', sa.Integer(), nullable=True), - sa.Column('password_hash', sa.String(length=128), nullable=True), sa.ForeignKeyConstraint(['copyright_holder_id'], ['copyright_holder.id'], ), sa.PrimaryKeyConstraint('id') ) - op.create_index(op.f('ix_collection_name'), 'collection', ['name'], unique=True) + op.create_index(op.f('ix_collection_name'), 'collection', ['name'], unique=False) op.create_index(op.f('ix_collection_slug'), 'collection', ['slug'], unique=False) op.create_table('piece', sa.Column('id', sa.Integer(), nullable=False), sa.Column('name', sa.String(length=128), nullable=True), - sa.Column('url', sa.String(length=128), nullable=True), + sa.Column('slug', sa.String(length=128), nullable=True), + sa.Column('description', sa.String(length=256), nullable=True), + sa.Column('url', sa.String(length=256), nullable=True), sa.Column('timestamp', sa.DateTime(), nullable=True), sa.Column('type', sa.String(length=10), nullable=True), sa.Column('collection_id', sa.Integer(), nullable=True), sa.ForeignKeyConstraint(['collection_id'], ['collection.id'], ), sa.PrimaryKeyConstraint('id') ) - op.create_index(op.f('ix_piece_name'), 'piece', ['name'], unique=True) + op.create_index(op.f('ix_piece_name'), 'piece', ['name'], unique=False) + op.create_index(op.f('ix_piece_slug'), 'piece', ['slug'], unique=False) op.create_index(op.f('ix_piece_timestamp'), 'piece', ['timestamp'], unique=False) op.create_index(op.f('ix_piece_type'), 'piece', ['type'], unique=False) # ### end Alembic commands ### @@ -62,6 +65,7 @@ def downgrade(): # ### commands auto generated by Alembic - please adjust! ### op.drop_index(op.f('ix_piece_type'), table_name='piece') op.drop_index(op.f('ix_piece_timestamp'), table_name='piece') + op.drop_index(op.f('ix_piece_slug'), table_name='piece') op.drop_index(op.f('ix_piece_name'), table_name='piece') op.drop_table('piece') op.drop_index(op.f('ix_collection_slug'), table_name='collection')