summaryrefslogtreecommitdiff
path: root/animism-align
diff options
context:
space:
mode:
Diffstat (limited to 'animism-align')
-rw-r--r--animism-align/README.md2
-rw-r--r--animism-align/cli/app/controllers/crud_controller.py6
-rw-r--r--animism-align/cli/app/controllers/upload_controller.py5
-rw-r--r--animism-align/cli/app/controllers/user_controller.py38
-rw-r--r--animism-align/cli/app/server/web.py4
-rw-r--r--animism-align/cli/app/settings/app_cfg.py14
-rw-r--r--animism-align/cli/app/sql/models/user.py2
-rw-r--r--animism-align/cli/app/sql/versions/202103051807_make_username_unique.py41
-rw-r--r--animism-align/cli/app/utils/auth_utils.py31
-rw-r--r--animism-align/cli/commands/admin/createuser.py28
-rw-r--r--animism-align/cli/commands/admin/migrate_to_mysql.py (renamed from animism-align/cli/commands/site/migrate_to_mysql.py)0
-rw-r--r--animism-align/cli/commands/admin/secret.py7
-rw-r--r--animism-align/cli/commands/site/export.py105
-rw-r--r--animism-align/frontend/app/views/viewer/nav/viewer.router.js5
-rw-r--r--animism-align/frontend/app/views/viewer/viewer.actions.js1
-rw-r--r--animism-align/frontend/site/app.js4
-rw-r--r--animism-align/frontend/site/site/site.actions.js8
17 files changed, 272 insertions, 29 deletions
diff --git a/animism-align/README.md b/animism-align/README.md
index ae3fd39..ac00d94 100644
--- a/animism-align/README.md
+++ b/animism-align/README.md
@@ -59,7 +59,7 @@ Generate a new migration if you've modified the database:
For production, export a static version of the episode. This will make an exported folder in `data_store/exports/` which you can zip and upload somewhere.
```
-./cli.py site export -o animism
+./cli.py site export -o animism --sync
./cli.py viewer run
```
diff --git a/animism-align/cli/app/controllers/crud_controller.py b/animism-align/cli/app/controllers/crud_controller.py
index 595825d..4fcb77d 100644
--- a/animism-align/cli/app/controllers/crud_controller.py
+++ b/animism-align/cli/app/controllers/crud_controller.py
@@ -1,6 +1,7 @@
from flask import request, jsonify
from flask_classful import FlaskView, route
from werkzeug.datastructures import MultiDict
+from flask_jwt import jwt_required
from app.sql.common import db, Session
from app.server.helpers import parse_search_args, parse_sort_args
@@ -28,6 +29,7 @@ class CrudView(FlaskView):
def on_destroy(self, session, item):
pass
+ @jwt_required()
def index(self):
"""
List all {model}s
@@ -53,6 +55,7 @@ class CrudView(FlaskView):
session.close()
return jsonify(res)
+ @jwt_required()
def get(self, id: int):
"""
Fetch a single {model}.
@@ -72,6 +75,7 @@ class CrudView(FlaskView):
session.close()
return jsonify(result)
+ @jwt_required()
def post(self):
"""
Create a new {model}.
@@ -99,6 +103,7 @@ class CrudView(FlaskView):
session.close()
return jsonify(res)
+ @jwt_required()
def put(self, id: int):
"""
Update a {model}.
@@ -133,6 +138,7 @@ class CrudView(FlaskView):
session.close()
return jsonify(res)
+ @jwt_required()
def delete(self, id: int):
"""
Delete a {model}.
diff --git a/animism-align/cli/app/controllers/upload_controller.py b/animism-align/cli/app/controllers/upload_controller.py
index 3b6e661..f363b0d 100644
--- a/animism-align/cli/app/controllers/upload_controller.py
+++ b/animism-align/cli/app/controllers/upload_controller.py
@@ -1,5 +1,6 @@
from flask import request, jsonify
from flask_classful import FlaskView, route
+from flask_jwt import jwt_required
from werkzeug.datastructures import MultiDict
from werkzeug.utils import secure_filename
import os
@@ -13,6 +14,7 @@ from app.utils.file_utils import sha256_stream, sha256_tree, VALID_IMAGE_EXTS
from app.server.decorators import APIError
class UploadView(FlaskView):
+ @jwt_required()
def index(self):
"""
List all uploaded files.
@@ -28,6 +30,7 @@ class UploadView(FlaskView):
session.close()
return jsonify(response)
+ @jwt_required()
def get(self, id):
"""
Fetch a single upload.
@@ -41,6 +44,7 @@ class UploadView(FlaskView):
session.close()
return jsonify(response)
+ @jwt_required()
def post(self):
"""
Upload a new file.
@@ -119,6 +123,7 @@ class UploadView(FlaskView):
session.close()
return jsonify(response)
+ @jwt_required()
def delete(self, id):
"""
Delete an uploaded file.
diff --git a/animism-align/cli/app/controllers/user_controller.py b/animism-align/cli/app/controllers/user_controller.py
new file mode 100644
index 0000000..8d14b98
--- /dev/null
+++ b/animism-align/cli/app/controllers/user_controller.py
@@ -0,0 +1,38 @@
+from flask import request, jsonify, redirect
+from flask_classful import route
+from werkzeug.datastructures import MultiDict
+
+from app.sql.common import db, Session
+from app.sql.models.user import User, UserForm
+from app.controllers.crud_controller import CrudView
+
+from flask_jwt import current_identity
+
+class UserView(CrudView):
+ model = User
+ form = UserForm
+
+ def on_create(self, session, form, item):
+ if not current_identity.is_admin:
+ raise ValueError("Unauthorized")
+ if 'password' in form:
+ item.password = encrypt_password(form['password'])
+ if 'settings' in form:
+ item.settings = form['settings']
+
+ def on_update(self, session, form, item):
+ if not current_identity.is_admin:
+ if item.id != current_identity.id:
+ raise ValueError("Unauthorized")
+ if current_identity.is_admin != item.is_admin:
+ raise ValueError("Unauthorized")
+ if 'password' in form:
+ item.password = encrypt_password(form['password'])
+ if 'settings' in form:
+ item.settings = form['settings']
+
+ def on_destroy(self, session, form, item):
+ if not current_identity.is_admin:
+ raise ValueError("Unauthorized")
+ if item.id == current_identity.id:
+ raise ValueError("Unauthorized")
diff --git a/animism-align/cli/app/server/web.py b/animism-align/cli/app/server/web.py
index 890bd35..58754a2 100644
--- a/animism-align/cli/app/server/web.py
+++ b/animism-align/cli/app/server/web.py
@@ -14,6 +14,7 @@ logging.getLogger().addHandler(logging.StreamHandler())
from flask import Flask, Blueprint, send_from_directory, request
from app.sql.common import db, connection_url
+from app.utils.auth_utils import setup_jwt
from app.settings import app_cfg
from app.controllers.annotation_controller import AnnotationView
@@ -33,9 +34,12 @@ def create_app(script_info=None):
app.config['SQLALCHEMY_DATABASE_URI'] = connection_url
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SERVER_NAME'] = app_cfg.SERVER_NAME
+ app.config['SECRET_KEY'] = app_cfg.TOKEN_SECRET
+ app.config['JWT_AUTH_URL_RULE'] = '/api/v1/auth/login'
app.url_map.strict_slashes = False
db.init_app(app)
+ setup_jwt(app)
AnnotationView.register(app, route_prefix='/api/v1/')
ParagraphView.register(app, route_prefix='/api/v1/')
diff --git a/animism-align/cli/app/settings/app_cfg.py b/animism-align/cli/app/settings/app_cfg.py
index dfd0154..ceb9e43 100644
--- a/animism-align/cli/app/settings/app_cfg.py
+++ b/animism-align/cli/app/settings/app_cfg.py
@@ -33,6 +33,7 @@ load_dotenv(dotenv_path=fp_env)
CLICK_GROUPS = {
'peaks': 'commands/peaks',
'site': 'commands/site',
+ 'admin': 'commands/admin',
'db': '',
'flask': '',
'viewer': '',
@@ -84,6 +85,19 @@ except Exception as e:
pass
# -----------------------------------------------------------------------------
+# Le crypto
+# -----------------------------------------------------------------------------
+
+TOKEN_SECRET = os.getenv("TOKEN_SECRET") or None
+if TOKEN_SECRET is None:
+ import random
+ raise ValueError("Please set a token secret, e.g. " + hex(random.getrandbits(64 * 8)).replace('0x',''))
+
+TOKEN_SECRET_BYTES = bytearray()
+TOKEN_SECRET_BYTES.extend(map(ord, TOKEN_SECRET))
+
+
+# -----------------------------------------------------------------------------
# Exports
# -----------------------------------------------------------------------------
diff --git a/animism-align/cli/app/sql/models/user.py b/animism-align/cli/app/sql/models/user.py
index bbc6eef..85549da 100644
--- a/animism-align/cli/app/sql/models/user.py
+++ b/animism-align/cli/app/sql/models/user.py
@@ -12,7 +12,7 @@ class User(Base):
"""Table for storing the user list"""
__tablename__ = 'user'
id = Column(Integer, primary_key=True)
- username = Column(String(256, convert_unicode=True), nullable=False)
+ username = Column(String(256, convert_unicode=True), nullable=False, unique=True)
password = Column(String(256, convert_unicode=True), nullable=False)
is_admin = Column(Boolean, default=False)
settings = Column(JSON, default={}, nullable=True)
diff --git a/animism-align/cli/app/sql/versions/202103051807_make_username_unique.py b/animism-align/cli/app/sql/versions/202103051807_make_username_unique.py
new file mode 100644
index 0000000..e8f4985
--- /dev/null
+++ b/animism-align/cli/app/sql/versions/202103051807_make_username_unique.py
@@ -0,0 +1,41 @@
+"""make username unique
+
+Revision ID: 135ba3ff136a
+Revises: 5de5fdfbe69a
+Create Date: 2021-03-05 18:07:36.955364
+
+"""
+from alembic import op
+import sqlalchemy as sa
+import sqlalchemy_utc
+from sqlalchemy.dialects import mysql
+
+# revision identifiers, used by Alembic.
+revision = '135ba3ff136a'
+down_revision = '5de5fdfbe69a'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.alter_column('episode', 'episode_number',
+ existing_type=mysql.INTEGER(),
+ nullable=True)
+ op.alter_column('episode', 'release_date',
+ existing_type=mysql.VARCHAR(collation='utf8mb4_unicode_ci', length=256),
+ nullable=True)
+ op.create_unique_constraint(None, 'user', ['username'])
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_constraint(None, 'user', type_='unique')
+ op.alter_column('episode', 'release_date',
+ existing_type=mysql.VARCHAR(collation='utf8mb4_unicode_ci', length=256),
+ nullable=False)
+ op.alter_column('episode', 'episode_number',
+ existing_type=mysql.INTEGER(),
+ nullable=False)
+ # ### end Alembic commands ###
diff --git a/animism-align/cli/app/utils/auth_utils.py b/animism-align/cli/app/utils/auth_utils.py
new file mode 100644
index 0000000..71974e3
--- /dev/null
+++ b/animism-align/cli/app/utils/auth_utils.py
@@ -0,0 +1,31 @@
+from flask_jwt import JWT
+
+import hmac
+import hashlib
+from app.settings import app_cfg
+
+from app.sql.common import db, Session, User
+
+def encrypt_password(cleartext):
+ clearbytes = bytearray()
+ clearbytes.extend(map(ord, cleartext))
+ return hmac.new(app_cfg.TOKEN_SECRET_BYTES, clearbytes, hashlib.sha256).hexdigest()
+
+def authenticate(username, password):
+ session = Session()
+ password = encrypt_password(password)
+ user = session.query(User).filter(User.username == username).first()
+ session.close()
+ if user and hmac.compare_digest(user.password.encode('utf-8'), password.encode('utf-8')):
+ return user
+ return None
+
+def identity(payload):
+ session = Session()
+ user_id = payload['identity']
+ user = session.query(User).get(user_id)
+ session.close()
+ return user
+
+def setup_jwt(app):
+ return JWT(app, authenticate, identity)
diff --git a/animism-align/cli/commands/admin/createuser.py b/animism-align/cli/commands/admin/createuser.py
new file mode 100644
index 0000000..ef90f1e
--- /dev/null
+++ b/animism-align/cli/commands/admin/createuser.py
@@ -0,0 +1,28 @@
+import click
+
+@click.command('createuser')
+@click.pass_context
+def cli(ctx):
+ from getpass import getpass
+ from app.utils.auth_utils import encrypt_password
+ from app.sql.common import db, Session, User
+
+ username = input("Username: ")
+ session = Session()
+ user_exists = session.query(User).filter(User.username == username).first()
+ if user_exists:
+ session.close()
+ raise ValueError("User already exists")
+
+ password = encrypt_password(getpass())
+ is_admin = input("Is admin? (y/n): ") == 'y'
+
+ user = User(
+ username=username,
+ password=password,
+ is_admin=is_admin,
+ settings={}
+ )
+ session.add(user)
+ session.commit()
+ session.close()
diff --git a/animism-align/cli/commands/site/migrate_to_mysql.py b/animism-align/cli/commands/admin/migrate_to_mysql.py
index e69dd13..e69dd13 100644
--- a/animism-align/cli/commands/site/migrate_to_mysql.py
+++ b/animism-align/cli/commands/admin/migrate_to_mysql.py
diff --git a/animism-align/cli/commands/admin/secret.py b/animism-align/cli/commands/admin/secret.py
new file mode 100644
index 0000000..c8a212d
--- /dev/null
+++ b/animism-align/cli/commands/admin/secret.py
@@ -0,0 +1,7 @@
+import click
+
+@click.command('secret')
+@click.pass_context
+def cli(ctx):
+ import random
+ print(hex(random.getrandbits(64 * 8)).replace('0x',''))
diff --git a/animism-align/cli/commands/site/export.py b/animism-align/cli/commands/site/export.py
index f3f5d3c..d706a55 100644
--- a/animism-align/cli/commands/site/export.py
+++ b/animism-align/cli/commands/site/export.py
@@ -7,13 +7,23 @@ from functools import reduce
from shutil import copyfile
import os
+MEDIA_ANNOTATION_TYPES = [
+ 'image', 'carousel', 'grid', 'gallery',
+ 'video',
+ 'vitrine',
+]
+
@click.command('info')
# @click.option('-g', '--graph', 'opt_graph_path', required=True,
# help='Graph name')
@click.option('-o', '--output', 'opt_output_dir', required=False, default="animism",
help='Output directory')
+@click.option('-s', '--sync/--no-sync', 'opt_sync', required=False, default=True,
+ help='Whether to sync over FTP')
+@click.option('-j', '--js/--no-js', 'opt_js', required=False, default=False,
+ help='Whether to rebuild the Javascript bundle')
@click.pass_context
-def cli(ctx, opt_output_dir):
+def cli(ctx, opt_output_dir, opt_sync, opt_js):
"""Export a graph"""
# ------------------------------------------------
@@ -28,9 +38,10 @@ def cli(ctx, opt_output_dir):
page_title = "Animism: Episode 1"
page_name = "episode1"
- page_desc = "A Report on Migrating Souls in Museums and Moving Pictures. Animism on e-flux.com is the ninth iteration of the exhibition, which will be released in four episodes starting November 2020."
- page_image = "https://animism.e-flux.com/episode1/media/8ca4adc754093cc8578a3033de8f96af96180fbce838c480636ffd5d128d1937.jpg"
- site_url = "https://animism.e-flux.com/" + page_name
+ page_desc = "A Report on Migrating Souls in Museums and Moving Pictures. Animism on e-flux.com is the ninth iteration of the exhibition, presented digitally here."
+ base_href = "https://animism.e-flux.com"
+ page_image = base_href + "/episode1/media/8ca4adc754093cc8578a3033de8f96af96180fbce838c480636ffd5d128d1937.jpg"
+ site_url = base_href + "/" + page_name
page_url = "/" + page_name
media_url = "/" + page_name + "/media"
@@ -58,14 +69,7 @@ def cli(ctx, opt_output_dir):
# ------------------------------------------------
# build the json for the e-flux search
- search_json = [{
- "text": transcript_to_text(db),
- "url": site_url,
- "type": "text",
- "previewtitle": '<i>Animism</i>, episode 1',
- "previewtext": page_desc,
- "previewimage": page_image,
- }]
+ search_json = build_search_json(db, base_href, site_url, page_desc, page_image)
# ------------------------------------------------
# build the index.html
@@ -105,27 +109,80 @@ def cli(ctx, opt_output_dir):
copy_tree(join(app_cfg.DIR_STATIC, 'img'), join(site_fp_static, 'img'))
copyfile(join(app_cfg.DIR_STATIC, 'favicon.ico'), join(site_fp_root, 'favicon.ico'))
+ print("Site export complete!")
+ print(f"Site exported to: {site_fp_out}")
+ return
+
# ------------------------------------------------
# build javascript
- print("Building javascript...")
- print(f'NODE_ENV=production node ./node_modules/webpack-cli/bin/cli.js --config ./webpack.config.site.js -o {site_fp_out}/bundle.js')
- os.chdir(app_cfg.DIR_PROJECT_ROOT)
- os.system(f'NODE_ENV=production node ./node_modules/webpack-cli/bin/cli.js --config ./webpack.config.site.js -o {site_fp_out}/bundle.js')
+ if opt_js:
+ print("Building javascript...")
+ print(f'NODE_ENV=production node ./node_modules/webpack-cli/bin/cli.js --config ./webpack.config.site.js -o {site_fp_out}/bundle.js')
+ os.chdir(app_cfg.DIR_PROJECT_ROOT)
+ os.system(f'NODE_ENV=production node ./node_modules/webpack-cli/bin/cli.js --config ./webpack.config.site.js -o {site_fp_out}/bundle.js')
+ else:
+ print("Skipping JS build.")
- print("Deploying site...")
- os.system("""
- lftp -c "set ssl:verify-certificate false; set ftp:list-options -a;
- open ftp://animism:Agkp8#48@93.114.86.205;
- lcd ./data_store/exports/animism;
- cd /;
- mirror --reverse --use-cache --verbose --no-umask --ignore-time --parallel=2"
- """)
+ if opt_sync:
+ print("Deploying site...")
+ os.system("""
+ lftp -c "set ssl:verify-certificate false; set ftp:list-options -a;
+ open ftp://animism:Agkp8#48@93.114.86.205;
+ lcd ./data_store/exports/animism;
+ cd /;
+ mirror --reverse --use-cache --verbose --no-umask --ignore-time --parallel=2"
+ """)
+ else:
+ print("Skipping sync.")
print("Site export complete!")
print(f"Site exported to: {site_fp_out}")
######################################################################
+# Search JSON
+######################################################################
+
+def build_search_json(db, base_href, site_url, page_desc, page_image):
+ search_items = []
+ search_items.append({
+ "text": transcript_to_text(db),
+ "url": site_url,
+ "type": "text",
+ "previewtitle": '<i>Animism</i>, episode 1',
+ "previewtext": page_desc,
+ "previewimage": page_image,
+ })
+ seen_media = {}
+ for annotation in IterateTable(db['annotation']):
+ if annotation['type'] not in MEDIA_ANNOTATION_TYPES:
+ continue
+ if 'media_id' not in annotation['settings']:
+ continue
+ media_id = annotation['settings']['media_id']
+ if media_id in seen_media:
+ continue
+ seen_media[media_id] = True
+ if media_id not in db['media']['lookup']:
+ continue
+ media = db['media']['lookup'][media_id]
+ if 'hide_in_bibliography' in media['settings'] and media['settings']['hide_in_bibliography']:
+ continue
+ if 'thumbnail' not in media['settings']:
+ continue
+ start_ts = annotation['start_ts']
+ search_items.append({
+ "text": media['settings']['bibliography'],
+ "url": site_url + "/#ts=" + str(start_ts),
+ "type": annotation['type'],
+ "previewtitle": media['title'],
+ "previewtext": page_desc,
+ "previewimage": base_href + media['settings']['thumbnail']['url'],
+ })
+
+ return search_items
+
+######################################################################
# Database Functions
######################################################################
diff --git a/animism-align/frontend/app/views/viewer/nav/viewer.router.js b/animism-align/frontend/app/views/viewer/nav/viewer.router.js
index 6c61223..27ff7a1 100644
--- a/animism-align/frontend/app/views/viewer/nav/viewer.router.js
+++ b/animism-align/frontend/app/views/viewer/nav/viewer.router.js
@@ -22,6 +22,11 @@ class ViewerRouter extends Component {
}
return
}
+ if (component.match('timestamp_')) {
+ const timestampParts = component.split('_')
+ actions.viewer.seekToTimestamp(timestampToSeconds(timestampParts[1]))
+ return
+ }
switch (this.props.match.params.component) {
case 'transcript':
actions.viewer.showComponent('transcript')
diff --git a/animism-align/frontend/app/views/viewer/viewer.actions.js b/animism-align/frontend/app/views/viewer/viewer.actions.js
index 39aa9f8..ef98533 100644
--- a/animism-align/frontend/app/views/viewer/viewer.actions.js
+++ b/animism-align/frontend/app/views/viewer/viewer.actions.js
@@ -113,7 +113,6 @@ export const loadSections = () => dispatch => {
}
// build timeline of "cue" instructions, which tell elements to change
- // TODO: modify this to append these instructions to a list based on media_id, so we can grab it for the gallery
if (CUE_UTILITY_ANNOTATION_TYPES.has(annotation.type)) {
if (annotation.type === 'gallery_advance') {
annotation.settings.frame_index = parseInt(annotation.settings.frame_index)
diff --git a/animism-align/frontend/site/app.js b/animism-align/frontend/site/app.js
index 8509954..ac232ce 100644
--- a/animism-align/frontend/site/app.js
+++ b/animism-align/frontend/site/app.js
@@ -1,6 +1,6 @@
import React, { Component } from 'react'
-import { ConnectedRouter } from 'connected-react-router'
-import { Route } from 'react-router'
+// import { ConnectedRouter } from 'connected-react-router'
+// import { Route } from 'react-router'
// import ViewerContainer from './viewer/viewer.container'
import Viewer from 'app/views/viewer/viewer.container'
diff --git a/animism-align/frontend/site/site/site.actions.js b/animism-align/frontend/site/site/site.actions.js
index ff25484..4767619 100644
--- a/animism-align/frontend/site/site/site.actions.js
+++ b/animism-align/frontend/site/site/site.actions.js
@@ -15,6 +15,14 @@ export const loadProject = () => dispatch => {
loadFonts(),
]).then(() => {
actions.viewer.loadSections()
+ if (window.location.hash.length) {
+ const hash = window.location.hash.substr(1)
+ const hashParts = hash.split('=')
+ if (hashParts.length !== 2) return
+ if (hashParts[0] === 'ts') {
+ actions.viewer.seekToTimestamp(parseFloat(hashParts[1]))
+ }
+ }
}).catch(err => {
console.error(err)
})