initial commit
This commit is contained in:
commit
8837b1e04f
2
autoapp.py
Normal file
2
autoapp.py
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
from sourcing import create_app
|
||||||
|
app = create_app('config.default')
|
12
create_db.py
Executable file
12
create_db.py
Executable file
|
@ -0,0 +1,12 @@
|
||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
from sourcing.model import Base
|
||||||
|
from sourcing.database import init_db, session
|
||||||
|
from sourcing import create_app
|
||||||
|
|
||||||
|
app = create_app('config.default')
|
||||||
|
db_url = app.config['DB_URL']
|
||||||
|
init_db(db_url)
|
||||||
|
engine = session.get_bind()
|
||||||
|
# Base.metadata.drop_all(engine)
|
||||||
|
Base.metadata.create_all(engine)
|
7
run.py
Executable file
7
run.py
Executable file
|
@ -0,0 +1,7 @@
|
||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
from sourcing import create_app
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = create_app('config.default')
|
||||||
|
app.run('0.0.0.0', debug=True)
|
12
sourcing/__init__.py
Normal file
12
sourcing/__init__.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
from flask import Flask
|
||||||
|
from . import database
|
||||||
|
from . import view
|
||||||
|
from .utils import display_datetime
|
||||||
|
|
||||||
|
def create_app(config):
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config.from_object(config)
|
||||||
|
database.init_app(app)
|
||||||
|
view.init_app(app)
|
||||||
|
app.jinja_env.filters['datetime'] = display_datetime
|
||||||
|
return app
|
18
sourcing/database.py
Normal file
18
sourcing/database.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import scoped_session, sessionmaker
|
||||||
|
|
||||||
|
session = scoped_session(sessionmaker())
|
||||||
|
|
||||||
|
def init_db(db_url):
|
||||||
|
session.configure(bind=get_engine(db_url))
|
||||||
|
|
||||||
|
def get_engine(db_url):
|
||||||
|
return create_engine(db_url, pool_recycle=3600)
|
||||||
|
|
||||||
|
def init_app(app):
|
||||||
|
db_url = app.config['DB_URL']
|
||||||
|
session.configure(bind=get_engine(db_url))
|
||||||
|
|
||||||
|
@app.teardown_appcontext
|
||||||
|
def shutdown_session(exception=None):
|
||||||
|
session.remove()
|
75
sourcing/forms.py
Normal file
75
sourcing/forms.py
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
from flask_wtf import Form
|
||||||
|
from wtforms.fields import StringField, PasswordField, BooleanField, HiddenField, TextAreaField, RadioField, FileField, IntegerField
|
||||||
|
from wtforms.validators import InputRequired, Email, Length, ValidationError, Regexp, NoneOf, Optional
|
||||||
|
from .model import User, LoginError, re_username, reserved_name, user_exists
|
||||||
|
|
||||||
|
PASSWORD_LEN = 64
|
||||||
|
EMAIL_LEN = 64
|
||||||
|
|
||||||
|
class SignupForm(Form):
|
||||||
|
username = StringField('username',
|
||||||
|
[InputRequired(),
|
||||||
|
Regexp(re_username),
|
||||||
|
NoneOf(reserved_name, message='Not available.'),
|
||||||
|
Length(min=3, max=64)],
|
||||||
|
[lambda name: name and name.replace(' ', '_')])
|
||||||
|
email = StringField('e-mail address',
|
||||||
|
[InputRequired(), Email(),
|
||||||
|
Length(min=5, max=EMAIL_LEN)],
|
||||||
|
description="we never share your e-mail address")
|
||||||
|
password = PasswordField('password',
|
||||||
|
[InputRequired(), Length(min=4, max=PASSWORD_LEN)])
|
||||||
|
|
||||||
|
def validate_username(form, field):
|
||||||
|
if user_exists(User.username, field.data):
|
||||||
|
raise ValidationError('Not available')
|
||||||
|
|
||||||
|
def validate_email(form, field):
|
||||||
|
if user_exists(User.email, field.data):
|
||||||
|
raise ValidationError('In use by another account')
|
||||||
|
|
||||||
|
class LoginForm(Form):
|
||||||
|
user_or_email = StringField('username or e-mail address',
|
||||||
|
[InputRequired(), Length(min=3, max=EMAIL_LEN)],
|
||||||
|
[lambda name: name and name.replace(' ', '_')])
|
||||||
|
password = PasswordField('password',
|
||||||
|
[InputRequired(), Length(max=PASSWORD_LEN)])
|
||||||
|
remember = BooleanField('stay logged in')
|
||||||
|
next = HiddenField('next')
|
||||||
|
|
||||||
|
def validate(self):
|
||||||
|
rv = Form.validate(self)
|
||||||
|
if not rv:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.user = User.attempt_login(self.user_or_email.data,
|
||||||
|
self.password.data)
|
||||||
|
return True
|
||||||
|
except LoginError as e:
|
||||||
|
self.user_or_email.errors.append(e.msg)
|
||||||
|
return False
|
||||||
|
|
||||||
|
class ForgotPasswordForm(Form):
|
||||||
|
username_or_email = StringField('username or e-mail address',
|
||||||
|
[InputRequired(), Length(max=EMAIL_LEN)])
|
||||||
|
|
||||||
|
class AccountSettingsForm(Form):
|
||||||
|
full_name = StringField('full name', [Length(max=64)])
|
||||||
|
|
||||||
|
class ChangePasswordForm(Form):
|
||||||
|
old_password = PasswordField('current password',
|
||||||
|
[InputRequired(), Length(max=PASSWORD_LEN)])
|
||||||
|
new_password = PasswordField('new password',
|
||||||
|
[InputRequired(), Length(max=PASSWORD_LEN)])
|
||||||
|
|
||||||
|
class SourceDocForm(Form):
|
||||||
|
text = TextAreaField('text', [InputRequired()])
|
||||||
|
db_price_per_character = IntegerField('price per character', [Optional()])
|
||||||
|
db_document_price = IntegerField('document price', [Optional()])
|
||||||
|
|
||||||
|
class ItemForm(Form):
|
||||||
|
text = TextAreaField('text', [InputRequired()])
|
||||||
|
|
||||||
|
class UploadSourceDocForm(Form):
|
||||||
|
sourcedoc_file = FileField('SourceDoc', [Regexp(r'^[^/\\]+\.txt$')])
|
234
sourcing/model.py
Normal file
234
sourcing/model.py
Normal file
|
@ -0,0 +1,234 @@
|
||||||
|
from flask import url_for
|
||||||
|
from .database import session
|
||||||
|
from .parse import parse_link, parse_sourcedoc_facet, parse_span
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy import Column, ForeignKey
|
||||||
|
from sqlalchemy.types import String, Unicode, Integer, DateTime, Boolean, UnicodeText, Enum
|
||||||
|
from sqlalchemy import func
|
||||||
|
from sqlalchemy.orm import relationship, validates, synonym
|
||||||
|
from sqlalchemy.sql import exists
|
||||||
|
from flask_login import UserMixin
|
||||||
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
|
from sqlalchemy.ext.hybrid import hybrid_property
|
||||||
|
import re
|
||||||
|
|
||||||
|
from hashids import Hashids
|
||||||
|
|
||||||
|
doc_hashids = Hashids(min_length=8)
|
||||||
|
|
||||||
|
Base = declarative_base()
|
||||||
|
Base.query = session.query_property()
|
||||||
|
|
||||||
|
# list of disallowed usernames - maybe this should be in the database
|
||||||
|
reserved_name = ['root', 'admin', 'administrator', 'support', 'info',
|
||||||
|
'test', 'tech', 'online', 'old', 'new', 'jobs', 'login', 'job', 'ipad'
|
||||||
|
'iphone', 'javascript', 'script', 'host', 'mail', 'image', 'faq',
|
||||||
|
'file', 'ftp', 'error', 'warning', 'the', 'assistance', 'maintenance',
|
||||||
|
'controller', 'head', 'chief', 'anon']
|
||||||
|
|
||||||
|
re_username = re.compile('^\w+$', re.U)
|
||||||
|
re_full_name = re.compile('^([-.\'" ]|[^\W\d_])+$', re.U)
|
||||||
|
|
||||||
|
def user_exists(field, value):
|
||||||
|
return session.query(exists().where(field == value)).scalar()
|
||||||
|
|
||||||
|
class TimeStampedModel(Base):
|
||||||
|
__abstract__ = True
|
||||||
|
created = Column(DateTime, default=func.now())
|
||||||
|
modified = Column(DateTime, default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
|
class LoginError(Exception):
|
||||||
|
def __init__(self, msg):
|
||||||
|
self.msg = msg
|
||||||
|
|
||||||
|
class User(TimeStampedModel, UserMixin):
|
||||||
|
__tablename__ = 'user'
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
username = Column(Unicode(32), unique=True, nullable=False)
|
||||||
|
pw_hash = Column(String(160), nullable=False)
|
||||||
|
email = Column(Unicode(64), unique=True, nullable=False)
|
||||||
|
email_verified = Column(Boolean(), nullable=False, default=False)
|
||||||
|
disabled = Column(Boolean(), nullable=False, default=False)
|
||||||
|
deleted = Column(Boolean(), nullable=False, default=False)
|
||||||
|
is_super = Column(Boolean, nullable=False, default=False)
|
||||||
|
last_login = Column(DateTime)
|
||||||
|
full_name = Column(Unicode(64))
|
||||||
|
balance = Column(Integer, nullable=False, default=0)
|
||||||
|
|
||||||
|
user_id = synonym('id')
|
||||||
|
name = synonym('full_name')
|
||||||
|
user_name = synonym('username')
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
pw_hash = generate_password_hash(kwargs.pop('password'))
|
||||||
|
return super(User, self).__init__(pw_hash=pw_hash, **kwargs)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<User: {!r}>'.format(self.username)
|
||||||
|
|
||||||
|
def set_password(self, password):
|
||||||
|
self.pw_hash = generate_password_hash(password)
|
||||||
|
|
||||||
|
def check_password(self, password):
|
||||||
|
return check_password_hash(self.pw_hash, password)
|
||||||
|
|
||||||
|
def get_id(self):
|
||||||
|
return self.id
|
||||||
|
|
||||||
|
@validates('email')
|
||||||
|
def validate_email(self, key, value):
|
||||||
|
assert '@' in value
|
||||||
|
return value
|
||||||
|
|
||||||
|
@validates('username')
|
||||||
|
def validate_usernane(self, key, value):
|
||||||
|
assert re_username.match(value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
@validates('full_name')
|
||||||
|
def validate_full_name(self, key, value):
|
||||||
|
if value:
|
||||||
|
assert re_full_name.match(value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
@hybrid_property
|
||||||
|
def is_live(self):
|
||||||
|
return self.email_verified & ~self.disabled & ~self.deleted
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def lookup_user_or_email(cls, user_or_email):
|
||||||
|
field = cls.email if '@' in user_or_email else cls.username
|
||||||
|
return cls.query.filter(field == user_or_email).one_or_none()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mail_to_name(self):
|
||||||
|
'''Name to use on e-mails sent to the user.'''
|
||||||
|
return self.full_name or self.username
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def attempt_login(cls, user_or_email, password):
|
||||||
|
user = cls.lookup_user_or_email(user_or_email)
|
||||||
|
if not user:
|
||||||
|
raise LoginError('user not found')
|
||||||
|
if user.disabled:
|
||||||
|
raise LoginError('user account disabled')
|
||||||
|
if not user.check_password(password):
|
||||||
|
raise LoginError('incorrect password')
|
||||||
|
return user
|
||||||
|
|
||||||
|
class Item(TimeStampedModel):
|
||||||
|
__tablename__ = 'item'
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
user_id = Column(Integer, ForeignKey('user.id'))
|
||||||
|
published = Column(DateTime)
|
||||||
|
type = Column(Enum('sourcedoc', 'xanadoc', 'xanalink', name='item_type'), nullable=False)
|
||||||
|
filename = Column(Unicode)
|
||||||
|
text = Column(UnicodeText)
|
||||||
|
|
||||||
|
user = relationship('User', backref='items')
|
||||||
|
|
||||||
|
__mapper_args__ = {
|
||||||
|
'polymorphic_identity': 'item',
|
||||||
|
'polymorphic_on': type,
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hashid(self):
|
||||||
|
return doc_hashids.encode(self.id)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_by_hashid(cls, hashid):
|
||||||
|
return cls.query.get(doc_hashids.decode(hashid))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url(self):
|
||||||
|
return url_for('.view_item', username=self.user.username, hashid=self.hashid)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def external_url(self):
|
||||||
|
return url_for('.view_item',
|
||||||
|
username=self.user.username,
|
||||||
|
hashid=self.hashid,
|
||||||
|
_external=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def edit_url(self):
|
||||||
|
return url_for('.edit_item', username=self.user.username, hashid=self.hashid)
|
||||||
|
|
||||||
|
def title(self, titles=None):
|
||||||
|
if not titles:
|
||||||
|
titles = XanaLink.get_all_titles()
|
||||||
|
return self.type + ": " + titles.get(self, self.hashid)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_external(cls, url):
|
||||||
|
home = url_for('.home', _external=True)
|
||||||
|
if not url.startswith(home):
|
||||||
|
return
|
||||||
|
username, _, hashid = url[len(home):].partition('/')
|
||||||
|
q = cls.query.filter(User.username == username,
|
||||||
|
cls.id == doc_hashids.decode(hashid)[0])
|
||||||
|
return q.one_or_none()
|
||||||
|
|
||||||
|
class XanaDoc(Item):
|
||||||
|
__tablename__ = 'xanadoc'
|
||||||
|
id = Column(Integer, ForeignKey('item.id'), primary_key=True)
|
||||||
|
|
||||||
|
__mapper_args__ = {'polymorphic_identity': 'xanadoc'}
|
||||||
|
|
||||||
|
class XanaLink(Item):
|
||||||
|
__tablename__ = 'xanalink'
|
||||||
|
id = Column(Integer, ForeignKey('item.id'), primary_key=True)
|
||||||
|
|
||||||
|
__mapper_args__ = {'polymorphic_identity': 'xanalink'}
|
||||||
|
|
||||||
|
def parse(self):
|
||||||
|
return parse_link(self.text)
|
||||||
|
|
||||||
|
def title(self, titles=None):
|
||||||
|
if titles is None:
|
||||||
|
titles = XanaLink.get_all_titles()
|
||||||
|
if self in titles:
|
||||||
|
return self.type + ": " + titles[self]
|
||||||
|
|
||||||
|
parsed = self.parse()
|
||||||
|
|
||||||
|
if parsed['type'] == 'title':
|
||||||
|
ident = parsed['facets'][0][0].partition(': ')[2]
|
||||||
|
item = Item.from_external(ident)
|
||||||
|
if item in titles:
|
||||||
|
return parsed['type'] + " link for " + item.title(titles=titles)
|
||||||
|
|
||||||
|
return parsed['type'] + " link: " + self.hashid
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_all_titles(cls):
|
||||||
|
titles = {}
|
||||||
|
for link in (obj.parse() for obj in cls.query):
|
||||||
|
if link['type'] != 'title':
|
||||||
|
continue
|
||||||
|
facet1, facet2 = link['facets']
|
||||||
|
link_type, _, ident = facet1[0].partition(': ')
|
||||||
|
item = Item.from_external(ident)
|
||||||
|
print(ident, item)
|
||||||
|
|
||||||
|
ident2, start, length = parse_span(facet2[0])
|
||||||
|
title = SourceDoc.from_external(ident2).text[start:length + start]
|
||||||
|
titles[item] = title
|
||||||
|
return titles
|
||||||
|
|
||||||
|
class SourceDoc(Item):
|
||||||
|
__tablename__ = 'sourcedoc'
|
||||||
|
id = Column(Integer, ForeignKey('item.id'), primary_key=True)
|
||||||
|
db_price_per_character = Column(Integer)
|
||||||
|
db_document_price = Column(Integer)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def document_price(self):
|
||||||
|
return self.db_document_price or self.db_price_per_character * len(self.text)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def price_per_character(self):
|
||||||
|
return self.db_price_per_character or self.db_document_price / len(self.text)
|
||||||
|
|
||||||
|
__mapper_args__ = {'polymorphic_identity': 'sourcedoc'}
|
273
sourcing/parse.py
Normal file
273
sourcing/parse.py
Normal file
|
@ -0,0 +1,273 @@
|
||||||
|
import re
|
||||||
|
import requests
|
||||||
|
import os.path
|
||||||
|
import random
|
||||||
|
from html import escape
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
re_comment = re.compile(r'#.*')
|
||||||
|
re_span_pointer = re.compile(r'span: (.*),start=(\d+),length=(\d+)')
|
||||||
|
re_xanalink = re.compile('xanalink: +([^ ]+) *$')
|
||||||
|
re_facet = re.compile('^facet\d* *=\s*(.*)\s*$')
|
||||||
|
|
||||||
|
re_colon_slash = re.compile('[/:]+')
|
||||||
|
|
||||||
|
project_dir = os.path.dirname(os.path.dirname(__file__))
|
||||||
|
cache_location = os.path.join(project_dir, 'cache')
|
||||||
|
|
||||||
|
max_sourcedoc_size = 600000
|
||||||
|
|
||||||
|
xnb_per_char = 150000
|
||||||
|
|
||||||
|
censor_urls = {'http://hyperland.com/xuCambDemo/J.Ineffable.txt',
|
||||||
|
'http://royalty.pub/Ineffable/17/32/Trovato'}
|
||||||
|
|
||||||
|
def censor_text(text):
|
||||||
|
return ''.join(chr(random.randint(9728, 9983)) if c.isalnum() else c
|
||||||
|
for c in text)
|
||||||
|
|
||||||
|
def get_url(url):
|
||||||
|
filename = os.path.join(cache_location, url_filename(url))
|
||||||
|
|
||||||
|
if os.path.exists(filename):
|
||||||
|
content = open(filename, 'rb').read()
|
||||||
|
else:
|
||||||
|
content = requests.get(url).content
|
||||||
|
open(filename, 'wb').write(content)
|
||||||
|
|
||||||
|
return content.decode(errors='replace')
|
||||||
|
|
||||||
|
def get_text(url):
|
||||||
|
# assume UTF-8
|
||||||
|
|
||||||
|
text = get_url(url)
|
||||||
|
if url in censor_urls:
|
||||||
|
text = censor_text(text)
|
||||||
|
|
||||||
|
heading = url.rsplit('/', 1)[-1]
|
||||||
|
return {
|
||||||
|
'url': url,
|
||||||
|
'text': text,
|
||||||
|
'heading': heading,
|
||||||
|
'length': len(text),
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse_span(line):
|
||||||
|
m = re_span_pointer.match(line)
|
||||||
|
if not m:
|
||||||
|
return None
|
||||||
|
return (m.group(1), int(m.group(2)), int(m.group(3)))
|
||||||
|
|
||||||
|
def parse_edl(edl_text):
|
||||||
|
edl = {
|
||||||
|
'spans': [],
|
||||||
|
'links': [],
|
||||||
|
}
|
||||||
|
for line in edl_text.splitlines():
|
||||||
|
line = re_comment.sub('', line).strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
span_pointer = parse_span(line)
|
||||||
|
if span_pointer:
|
||||||
|
edl['spans'].append(span_pointer)
|
||||||
|
continue
|
||||||
|
m = re_xanalink.match(line)
|
||||||
|
if m:
|
||||||
|
link_url = m.group(1)
|
||||||
|
edl['links'].append({
|
||||||
|
'url': link_url,
|
||||||
|
'text': get_url(link_url),
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
|
||||||
|
return edl
|
||||||
|
|
||||||
|
def get_span(text, url, start, length):
|
||||||
|
return {
|
||||||
|
'url': url,
|
||||||
|
'start': start,
|
||||||
|
'length': length,
|
||||||
|
'text': text[url][start:start + length]
|
||||||
|
}
|
||||||
|
|
||||||
|
def fulfil_edl(edl):
|
||||||
|
text = {}
|
||||||
|
for url, start, length in parse_edl(edl):
|
||||||
|
if url not in text:
|
||||||
|
text[url] = get_text(url)
|
||||||
|
|
||||||
|
yield get_span(text, url, start, length)
|
||||||
|
|
||||||
|
def get_urls(spans):
|
||||||
|
return {i[0] for i in spans}
|
||||||
|
|
||||||
|
def url_filename(url):
|
||||||
|
return re_colon_slash.sub('_', url)
|
||||||
|
|
||||||
|
def find_min_max(spans, source):
|
||||||
|
text_min, text_max = {}, {}
|
||||||
|
for url, start, length in spans:
|
||||||
|
if url in text_min:
|
||||||
|
text_min[url] = min(text_min[url], start)
|
||||||
|
else:
|
||||||
|
text_min[url] = start
|
||||||
|
|
||||||
|
if url in text_max:
|
||||||
|
text_max[url] = max(text_max[url], start + length)
|
||||||
|
else:
|
||||||
|
text_max[url] = start + length
|
||||||
|
|
||||||
|
for s in source:
|
||||||
|
url = s['url']
|
||||||
|
s['min'] = text_min[url]
|
||||||
|
s['max'] = text_max[url]
|
||||||
|
|
||||||
|
def span_html(span_type, num):
|
||||||
|
return '<span class="{span_type} sourcedoc{span_type}" id="{span_type}{num}">'.format(num=num, span_type=span_type)
|
||||||
|
|
||||||
|
def xanadoc_span_html(num, text, url, start, length, highlight=True, censor=False):
|
||||||
|
cls = []
|
||||||
|
if highlight:
|
||||||
|
cls = ['xanadoctransclusion', 'transclusion']
|
||||||
|
html_class = ' class="{}"'.format(' '.join(cls)) if cls else ''
|
||||||
|
|
||||||
|
html = '<span id="span{}"{} data-url="{}" data-start="{}" data-length="{}">{}</span>'.format(num, html_class, escape(url), start, length, text)
|
||||||
|
|
||||||
|
if censor:
|
||||||
|
return '<span class="censor">' + html + '</span>'
|
||||||
|
else:
|
||||||
|
return html
|
||||||
|
|
||||||
|
def parse_sourcedoc_facet(facet):
|
||||||
|
leg = facet[0]
|
||||||
|
prefix = 'sourcedoc: '
|
||||||
|
assert leg.startswith(prefix)
|
||||||
|
return leg[len(prefix):]
|
||||||
|
|
||||||
|
def parse_link(link_text):
|
||||||
|
link_type = None
|
||||||
|
expect = 'link_type'
|
||||||
|
facets = []
|
||||||
|
for line in link_text.splitlines():
|
||||||
|
line = re_comment.sub('', line).strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
if expect == 'link_type':
|
||||||
|
if line.startswith('type='):
|
||||||
|
link_type = line[5:]
|
||||||
|
expect = 'facets'
|
||||||
|
continue
|
||||||
|
if expect != 'facets':
|
||||||
|
# print("unrecognized:", line)
|
||||||
|
continue
|
||||||
|
|
||||||
|
m = re_facet.match(line)
|
||||||
|
if m:
|
||||||
|
legs = []
|
||||||
|
facets.append(legs)
|
||||||
|
if m.group(1):
|
||||||
|
line = m.group(1)
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
if legs and legs[-1] == 'span:' and line.startswith('http'):
|
||||||
|
legs[-1] += ' ' + line
|
||||||
|
else:
|
||||||
|
legs.append(line.strip())
|
||||||
|
return {'type': link_type, 'facets': facets}
|
||||||
|
|
||||||
|
def fulfil_edl_with_sources(edl_text):
|
||||||
|
edl = parse_edl(edl_text)
|
||||||
|
spans = edl['spans']
|
||||||
|
|
||||||
|
hide_transclusions = set()
|
||||||
|
|
||||||
|
two_facet_links = []
|
||||||
|
|
||||||
|
link_num = 0
|
||||||
|
for link in edl['links']:
|
||||||
|
link_detail = parse_link(link['text'])
|
||||||
|
if link_detail['type'] == 'HideTransclusions':
|
||||||
|
hide_transclusions.add(parse_sourcedoc_facet(link_detail['facets'][0]))
|
||||||
|
elif len(link_detail['facets']) == 2:
|
||||||
|
two_facet_links.append((link_num, [parse_span(span[0]) for span in link_detail['facets']]))
|
||||||
|
link_num += 1
|
||||||
|
|
||||||
|
source = [get_text(url) for url in get_urls(spans)]
|
||||||
|
|
||||||
|
source_text = {s['url']: s['text'] for s in source}
|
||||||
|
|
||||||
|
source_doc_links = defaultdict(list)
|
||||||
|
|
||||||
|
for link_num, facets in two_facet_links:
|
||||||
|
for span in facets:
|
||||||
|
url, start, length = span
|
||||||
|
source_doc_links[url].append((start, length, link_num, 'link'))
|
||||||
|
if url in source_text:
|
||||||
|
continue
|
||||||
|
|
||||||
|
s = get_text(url)
|
||||||
|
source.append(s)
|
||||||
|
source_text[s['url']] = s['text']
|
||||||
|
|
||||||
|
for s in source_doc_links.values():
|
||||||
|
s.sort()
|
||||||
|
|
||||||
|
spans = list(enumerate(spans))
|
||||||
|
|
||||||
|
doc_spans = []
|
||||||
|
for num, (url, start, length) in spans:
|
||||||
|
highlight = url not in hide_transclusions
|
||||||
|
span_text = source_text[url] # [start:start + length]
|
||||||
|
censor = url in censor_urls
|
||||||
|
new_text = ''
|
||||||
|
pos = start
|
||||||
|
for link_start, link_len, link_num, span_type in source_doc_links[url]:
|
||||||
|
link_end = link_start + link_len
|
||||||
|
if link_start >= start + length:
|
||||||
|
break
|
||||||
|
if link_end < start:
|
||||||
|
continue
|
||||||
|
open_tag = '<span class="xanadoclink link" id="xanalink{}">'.format(link_num)
|
||||||
|
link_span = (open_tag +
|
||||||
|
escape(span_text[link_start:link_end]) +
|
||||||
|
'</span>')
|
||||||
|
new_text += escape(span_text[pos:link_start]) + link_span
|
||||||
|
pos = link_end
|
||||||
|
new_text += escape(span_text[pos:start + length])
|
||||||
|
cur = xanadoc_span_html(num, new_text, url, start, length, highlight=highlight, censor=censor)
|
||||||
|
doc_spans.append(cur)
|
||||||
|
|
||||||
|
doc = ''.join(doc_spans)
|
||||||
|
|
||||||
|
for s in source:
|
||||||
|
text = s.pop('text')
|
||||||
|
if s['length'] > max_sourcedoc_size:
|
||||||
|
# print('{} > {}'.format(s['length'], max_sourcedoc_size))
|
||||||
|
continue
|
||||||
|
if s['url'] in hide_transclusions:
|
||||||
|
continue
|
||||||
|
source_spans = [(start, length, num, 'transclusion') for num, (url, start, length) in spans if url == s['url']]
|
||||||
|
source_spans += source_doc_links[s['url']]
|
||||||
|
source_spans.sort()
|
||||||
|
|
||||||
|
new_text = ''
|
||||||
|
pos = 0
|
||||||
|
|
||||||
|
for start, length, num, span_type in source_spans:
|
||||||
|
end = start + length
|
||||||
|
new_text += (escape(text[pos:start]) +
|
||||||
|
span_html(span_type, num) +
|
||||||
|
escape(text[start:end]) +
|
||||||
|
'</span>')
|
||||||
|
pos = end
|
||||||
|
new_text += escape(text[pos:])
|
||||||
|
new_text = new_text.replace('\n', '<br/>\n')
|
||||||
|
|
||||||
|
s['text'] = new_text
|
||||||
|
|
||||||
|
return {
|
||||||
|
'source': source,
|
||||||
|
'doc': doc.replace('\n', '<br/>\n'),
|
||||||
|
'span_count': len(spans),
|
||||||
|
'link_count': len(two_facet_links),
|
||||||
|
}
|
1
sourcing/static/bootstrap
Symbolic link
1
sourcing/static/bootstrap
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/javascript/bootstrap
|
1
sourcing/static/handlebars
Symbolic link
1
sourcing/static/handlebars
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/javascript/handlebars
|
1
sourcing/static/jquery
Symbolic link
1
sourcing/static/jquery
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/javascript/jquery
|
1
sourcing/static/jquery-ui
Symbolic link
1
sourcing/static/jquery-ui
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/javascript/jquery-ui
|
15
sourcing/static/js/doc.js
Normal file
15
sourcing/static/js/doc.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
$(function() {
|
||||||
|
$("div#right").hide();
|
||||||
|
/* $("button#add-new-span").click(function() {
|
||||||
|
$("div#right").show();
|
||||||
|
}); */
|
||||||
|
$("a.source-doc-link").click(function(e) {
|
||||||
|
var link = $(e.target);
|
||||||
|
var title = link.text();
|
||||||
|
console.log("source doc link " + title);
|
||||||
|
$("div#source-doc-heading").text("Add span from " + title);
|
||||||
|
$("#newSpanModal").modal('hide');
|
||||||
|
$("div#right").show();
|
||||||
|
$("div#source-doc-body").load(link.data("url"));
|
||||||
|
});
|
||||||
|
});
|
49
sourcing/static/js/sourcedoc.js
Normal file
49
sourcing/static/js/sourcedoc.js
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
function add_message() {
|
||||||
|
$("div#messages").text("whatever");
|
||||||
|
}
|
||||||
|
|
||||||
|
function show_selection(event) {
|
||||||
|
var range = window.getSelection().getRangeAt(0);
|
||||||
|
var start_element = range.startContainer.parentElement;
|
||||||
|
var end_element = range.endContainer.parentElement;
|
||||||
|
|
||||||
|
if ($(start_element).closest("div#right").prop("tagName") == 'DIV') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!start_element.hasAttribute('data-start') ||
|
||||||
|
!end_element.hasAttribute('data-start')) {
|
||||||
|
$("div#right").hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var start = parseInt(start_element.getAttribute('data-start')) + range.startOffset;
|
||||||
|
var end = parseInt(end_element.getAttribute('data-start')) + range.endOffset;
|
||||||
|
var length = end - start;
|
||||||
|
if(length === 0) {
|
||||||
|
$("div#right").hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#span").text(doc_url + ",start=" + start + ",length=" + length);
|
||||||
|
|
||||||
|
/*
|
||||||
|
$("div#right").show();
|
||||||
|
$("span#length").text(length);
|
||||||
|
$("input[name='offset']").val(start);
|
||||||
|
$("input[name='length']").val(length);
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$(function() {
|
||||||
|
$("button#go").click(add_message);
|
||||||
|
$("div#right").hide();
|
||||||
|
|
||||||
|
$("body").mouseup(show_selection);
|
||||||
|
|
||||||
|
$("#show-span-selector").click(function(e) {
|
||||||
|
$("#span-selector").removeClass("hidden");
|
||||||
|
$(this).hide();
|
||||||
|
});
|
||||||
|
});
|
17
sourcing/templates/edit.html
Normal file
17
sourcing/templates/edit.html
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{% from "form/controls.html" import render_field %}
|
||||||
|
|
||||||
|
{% set title="edit " + doc.title %}
|
||||||
|
|
||||||
|
{% set action=doc.edit_url %}
|
||||||
|
{% set label="save" %}
|
||||||
|
|
||||||
|
{% set fields %}
|
||||||
|
{#
|
||||||
|
{{ render_field(form.filename) }}
|
||||||
|
{{ render_field(form.db_price_per_character) }}
|
||||||
|
{{ render_field(form.db_document_price) }}
|
||||||
|
#}
|
||||||
|
{{ render_field(form.text, rows=20) }}
|
||||||
|
{% endset %}
|
||||||
|
|
||||||
|
{% include "form/simple.html" %}
|
10
sourcing/templates/flash_msg.html
Normal file
10
sourcing/templates/flash_msg.html
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{% with messages = get_flashed_messages() %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-success alert-dismissible" role="alert">
|
||||||
|
<button type="button" class="close" data-dismiss="alert"><span aria-hidden="true">×</span><span class="sr-only">Close</span></button>
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
11
sourcing/templates/foot.html
Normal file
11
sourcing/templates/foot.html
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<hr/>
|
||||||
|
<footer>© 2017 Project Xanadu</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="{{ url_for('static', filename='jquery/jquery.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='bootstrap/js/bootstrap.js') }}"></script>
|
||||||
|
|
||||||
|
{{ scripts | safe }}
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
33
sourcing/templates/form/controls.html
Normal file
33
sourcing/templates/form/controls.html
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
{% macro render_field(field) %}
|
||||||
|
<div class="form-group">
|
||||||
|
{{ field.label() }}
|
||||||
|
{{ field(class="form-control", **kwargs) }}
|
||||||
|
{% if field.description %}
|
||||||
|
<span class="help">{{ field.description|safe }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro checkbox(field) %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="checkbox-inline">
|
||||||
|
{{ field() }}
|
||||||
|
{{ field.label.text }}
|
||||||
|
</label>
|
||||||
|
{% if field.description %}
|
||||||
|
<span class="help">{{ field.description|safe }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro radio(field) %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="radio-inline">
|
||||||
|
{{ field() }}
|
||||||
|
{{ field.label.text }}
|
||||||
|
</label>
|
||||||
|
{% if field.description %}
|
||||||
|
<span class="help">{{ field.description|safe }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
9
sourcing/templates/form/errors.html
Normal file
9
sourcing/templates/form/errors.html
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{% if form.errors %}
|
||||||
|
<ul class="errors">
|
||||||
|
{% for field in form %}
|
||||||
|
{% for error in form.errors[field.name] %}
|
||||||
|
<li>{{ field.label }}: {{ error }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
11
sourcing/templates/form/main.html
Normal file
11
sourcing/templates/form/main.html
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<div class="well">
|
||||||
|
{% include "form/errors.html" %}
|
||||||
|
<form action="{{action}}" method="{{method|default('post')}}" role="form">
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
{{ fields | safe }}
|
||||||
|
<div class="form-group">
|
||||||
|
<button type="submit" class="btn btn-default">{{ label }}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
10
sourcing/templates/form/simple.html
Normal file
10
sourcing/templates/form/simple.html
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{% include "head.html" %}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<h1>{{ title }}</h1>
|
||||||
|
{% include "form/main.html" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% include "foot.html" %}
|
16
sourcing/templates/head.html
Normal file
16
sourcing/templates/head.html
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>{{ title | default("Xanadu") }}</title>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='bootstrap/css/bootstrap.css') }}">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
{% include "navbar.html" %}
|
||||||
|
{{ dialogs | safe }}
|
||||||
|
<div class="container">
|
||||||
|
{% include "flash_msg.html" %}
|
23
sourcing/templates/home.html
Normal file
23
sourcing/templates/home.html
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
{% include "head.html" %}
|
||||||
|
<h1>perma.pub</h1>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{% for doc in docs %}
|
||||||
|
<li>
|
||||||
|
<a href="{{ doc.url }}">{{ doc.title() }}</a>
|
||||||
|
by {{ doc.user.username }}
|
||||||
|
— created {{ doc.created | datetime }}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="{{ url_for('.new_sourcedoc') }}" class="btn btn-default">new source document</a>
|
||||||
|
<a href="{{ url_for('.new_xanalink') }}" class="btn btn-default">new xanalink</a>
|
||||||
|
<a href="{{ url_for('.new_xanadoc') }}" class="btn btn-default">new xanadoc</a>
|
||||||
|
{#
|
||||||
|
<a href="#" class="btn btn-default">upload a source document</a>
|
||||||
|
#}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% include "foot.html" %}
|
13
sourcing/templates/login.html
Normal file
13
sourcing/templates/login.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{% from "form/controls.html" import render_field, checkbox, submit %}
|
||||||
|
|
||||||
|
{% set title="Login" %}
|
||||||
|
{% set action=url_for('.login') %}
|
||||||
|
{% set label="Sign in" %}
|
||||||
|
|
||||||
|
{% set fields %}
|
||||||
|
{{ render_field(form.user_or_email) }}
|
||||||
|
{{ render_field(form.password) }}
|
||||||
|
{{ checkbox(form.remember) }}
|
||||||
|
{% endset %}
|
||||||
|
|
||||||
|
{% include "form/simple.html" %}
|
1
sourcing/templates/mail/signup.txt
Normal file
1
sourcing/templates/mail/signup.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
You signed up for xanadu
|
34
sourcing/templates/navbar.html
Normal file
34
sourcing/templates/navbar.html
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
<nav class="navbar navbar-default">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="navbar-header">
|
||||||
|
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
|
||||||
|
<span class="sr-only">Toggle navigation</span>
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
</button>
|
||||||
|
<a class="navbar-brand" href="{{ url_for('.home') }}">{{ config.SITE_NAME }}</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="navbar" class="navbar-collapse collapse">
|
||||||
|
<ul class="nav navbar-nav">
|
||||||
|
{% for page in ('home', 'about', 'contact') %}
|
||||||
|
<li{% if current_page == page %} class="active"{% endif %}><a href="{{ url_for('.' + page) }}">{{ page | title }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
<ul class="nav navbar-nav navbar-right">
|
||||||
|
{% if current_user.is_authenticated %}
|
||||||
|
{% if current_user.admin %}
|
||||||
|
<li><a href="{{ url_for('.admin') }}">admin</a></li>
|
||||||
|
{% endif %}
|
||||||
|
<li><a href="{{ url_for('.account_settings') }}">{{ current_user.username }}</a></li>
|
||||||
|
<li><a href="{{ url_for('.logout') }}">logout</a></li>
|
||||||
|
{% else %}
|
||||||
|
<li><a href="{{ url_for('.signup') }}">signup</a></li>
|
||||||
|
<li><a href="{{ url_for('.login') }}">login</a></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
15
sourcing/templates/new.html
Normal file
15
sourcing/templates/new.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{% from "form/controls.html" import render_field %}
|
||||||
|
|
||||||
|
{% set action=url_for(request.endpoint) %}
|
||||||
|
{% set label="save" %}
|
||||||
|
|
||||||
|
{% set fields %}
|
||||||
|
{#
|
||||||
|
{{ render_field(form.filename) }}
|
||||||
|
{{ render_field(form.db_price_per_character) }}
|
||||||
|
{{ render_field(form.db_document_price) }}
|
||||||
|
#}
|
||||||
|
{{ render_field(form.text, rows=20) }}
|
||||||
|
{% endset %}
|
||||||
|
|
||||||
|
{% include "form/simple.html" %}
|
13
sourcing/templates/signup.html
Normal file
13
sourcing/templates/signup.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{% from "form/controls.html" import render_field %}
|
||||||
|
|
||||||
|
{% set title="Sign up" %}
|
||||||
|
{% set action=url_for('.signup') %}
|
||||||
|
{% set label="Sign up" %}
|
||||||
|
|
||||||
|
{% set fields %}
|
||||||
|
{{ render_field(form.username) }}
|
||||||
|
{{ render_field(form.password) }}
|
||||||
|
{{ render_field(form.email) }}
|
||||||
|
{% endset %}
|
||||||
|
|
||||||
|
{% include "form/simple.html" %}
|
33
sourcing/templates/source_doc.html
Normal file
33
sourcing/templates/source_doc.html
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
{% set title = doc.filename %}
|
||||||
|
{% set price = doc.document_price %}
|
||||||
|
{% include "head.html" %}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div#text { font-family: Courier; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-12">
|
||||||
|
<h1>{{ title }}
|
||||||
|
{% if doc.user == current_user %}
|
||||||
|
<a href="{{ url_for('.edit_source_document',
|
||||||
|
filename=doc.filename) }}" class="btn btn-default">edit</a>
|
||||||
|
{% endif %}
|
||||||
|
</h1>
|
||||||
|
<p><a href="{{ url_for('.home') }}">back to index</a></p>
|
||||||
|
<ul>
|
||||||
|
<li>character count: {{ '{:,d}'.format(doc.text | length) }}</li>
|
||||||
|
{% if price %}
|
||||||
|
<li>document price: {{ '{:,.2f}'.format(price) }} nanobucks</li>
|
||||||
|
<li>price per character: {{ '{:,.2f}'.format(doc.price_per_character) }} nanobucks</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
<div class="well" id="text">
|
||||||
|
{%- for start, line in iter_lines(doc.get_text()) if line -%}
|
||||||
|
<p data-start="{{ start }}">{{ line }}</p>
|
||||||
|
{%- endfor -%}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% include "foot.html" %}
|
12
sourcing/templates/user/account.html
Normal file
12
sourcing/templates/user/account.html
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{% from "form/controls.html" import render_field %}
|
||||||
|
|
||||||
|
{% set title="account settings" %}
|
||||||
|
|
||||||
|
{% set action=url_for(".account_settings") %}
|
||||||
|
{% set label="save" %}
|
||||||
|
|
||||||
|
{% set fields %}
|
||||||
|
{{ render_field(form.full_name) }}
|
||||||
|
{% endset %}
|
||||||
|
|
||||||
|
{% include "form/simple.html" %}
|
34
sourcing/templates/view.html
Normal file
34
sourcing/templates/view.html
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
{% set title = doc.title() %}
|
||||||
|
{% include "head.html" %}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div#text { font-family: Courier; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-12">
|
||||||
|
<h1>{{ title }}
|
||||||
|
{% if doc.user == current_user %}
|
||||||
|
<a href="{{ doc.edit_url }}" class="btn btn-default">edit</a>
|
||||||
|
{% endif %}
|
||||||
|
</h1>
|
||||||
|
<p><a href="{{ url_for('.home') }}">back to index</a></p>
|
||||||
|
<div class="well" id="text">
|
||||||
|
{%- for start, line in iter_lines(doc.text) if line -%}
|
||||||
|
<p data-start="{{ start }}">{{ line }}</p>
|
||||||
|
{%- endfor -%}
|
||||||
|
</div>
|
||||||
|
{% if doc.type == 'sourcedoc' %}
|
||||||
|
<a href="#" id="show-span-selector" class="btn btn-default">show span selector</a>
|
||||||
|
<p id="span-selector" class="hidden">span: <span id="span"></span></p>
|
||||||
|
{% set scripts %}
|
||||||
|
<script>
|
||||||
|
var doc_url = '{{ doc.external_url }}';
|
||||||
|
</script>
|
||||||
|
<script src="{{ url_for('static', filename='js/sourcedoc.js') }}"></script>
|
||||||
|
{% endset %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% include "foot.html" %}
|
21
sourcing/text.py
Normal file
21
sourcing/text.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import re
|
||||||
|
import random
|
||||||
|
|
||||||
|
re_newline = re.compile('\r?\n')
|
||||||
|
|
||||||
|
def find_newlines(text):
|
||||||
|
return (m.end(0) for m in re_newline.finditer(text))
|
||||||
|
|
||||||
|
def iter_lines(text):
|
||||||
|
start = 0
|
||||||
|
for m in re_newline.finditer(text):
|
||||||
|
end = m.end(0)
|
||||||
|
yield (start, text[start:end])
|
||||||
|
start = m.end(0)
|
||||||
|
if start < len(text) - 1:
|
||||||
|
yield (start, text[start:])
|
||||||
|
|
||||||
|
def censor_text(text):
|
||||||
|
def random_chr():
|
||||||
|
return chr(random.randint(9728, 9983))
|
||||||
|
return ''.join(random_chr() if c.isalnum() else c for c in text)
|
13
sourcing/utils.py
Normal file
13
sourcing/utils.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import humanize
|
||||||
|
from datetime import date, timedelta
|
||||||
|
|
||||||
|
def display_datetime(dt):
|
||||||
|
if dt is None:
|
||||||
|
return 'n/a'
|
||||||
|
today = date.today()
|
||||||
|
if today - dt.date() < timedelta(days=1):
|
||||||
|
return humanize.naturaltime(dt)
|
||||||
|
else:
|
||||||
|
return dt.strftime('%a, %d %b %Y')
|
||||||
|
|
||||||
|
|
232
sourcing/view.py
Normal file
232
sourcing/view.py
Normal file
|
@ -0,0 +1,232 @@
|
||||||
|
from flask import (Blueprint, render_template, request, redirect, flash,
|
||||||
|
url_for, abort, jsonify)
|
||||||
|
from flask_login import (login_user, current_user, logout_user,
|
||||||
|
login_required, LoginManager)
|
||||||
|
from .forms import (LoginForm, SignupForm, AccountSettingsForm,
|
||||||
|
UploadSourceDocForm, SourceDocForm, ItemForm)
|
||||||
|
from .model import User, SourceDoc, Item, XanaDoc, XanaLink
|
||||||
|
from .database import session
|
||||||
|
from .text import iter_lines
|
||||||
|
from werkzeug.debug.tbtools import get_current_traceback
|
||||||
|
from jinja2 import evalcontextfilter, Markup
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
login_manager = LoginManager()
|
||||||
|
login_manager.login_view = 'login'
|
||||||
|
re_paragraph = re.compile(r'(?:\r\n|\r|\n){2,}')
|
||||||
|
bp = Blueprint('view', __name__)
|
||||||
|
|
||||||
|
def init_app(app):
|
||||||
|
login_manager.init_app(app)
|
||||||
|
app.register_blueprint(bp)
|
||||||
|
|
||||||
|
@app.template_filter()
|
||||||
|
@evalcontextfilter
|
||||||
|
def newline_html(eval_ctx, value):
|
||||||
|
return u'\n\n'.join(Markup(u'<p>') + p.replace('\n', Markup('<br>')) + Markup(u'</p>')
|
||||||
|
for p in re_paragraph.split(value))
|
||||||
|
|
||||||
|
@login_manager.user_loader
|
||||||
|
def load_user(user_id):
|
||||||
|
return User.query.get(user_id)
|
||||||
|
|
||||||
|
|
||||||
|
# where do we redirect after signup is complete
|
||||||
|
view_after_signup = '.home'
|
||||||
|
|
||||||
|
@bp.context_processor
|
||||||
|
def inject_user():
|
||||||
|
return dict(current_user=current_user)
|
||||||
|
|
||||||
|
def show_errors(f):
|
||||||
|
@wraps(f)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
except Exception:
|
||||||
|
traceback = get_current_traceback(skip=1, show_hidden_frames=False,
|
||||||
|
ignore_system_exceptions=True)
|
||||||
|
return traceback.render_full().encode('utf-8', 'replace')
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
@bp.route('/')
|
||||||
|
def home():
|
||||||
|
titles = XanaLink.get_all_titles()
|
||||||
|
docs = Item.query.order_by(Item.created)
|
||||||
|
return render_template('home.html', docs=docs)
|
||||||
|
|
||||||
|
@bp.route('/source_doc_upload', methods=["POST"])
|
||||||
|
@show_errors
|
||||||
|
def source_doc_upload():
|
||||||
|
f = request.files['sourcedoc_file']
|
||||||
|
text = f.read()
|
||||||
|
doc = SourceDoc(text=text, user=current_user, filename=f.filename)
|
||||||
|
session.add(doc)
|
||||||
|
session.commit()
|
||||||
|
flash('new source document uploaded')
|
||||||
|
return redirect(doc.url)
|
||||||
|
|
||||||
|
@bp.route('/about')
|
||||||
|
def about():
|
||||||
|
return render_template('about.html')
|
||||||
|
|
||||||
|
@bp.route('/contact')
|
||||||
|
def contact():
|
||||||
|
return render_template('contact.html')
|
||||||
|
|
||||||
|
def redirect_to_home():
|
||||||
|
return redirect(url_for('.home'))
|
||||||
|
|
||||||
|
@bp.route('/login', methods=['GET', 'POST'])
|
||||||
|
def login():
|
||||||
|
form = LoginForm(next=request.args.get('next'))
|
||||||
|
if form.validate_on_submit():
|
||||||
|
login_user(form.user, remember=form.remember.data)
|
||||||
|
flash('Logged in successfully.')
|
||||||
|
return redirect(request.form.get('next') or url_for('.home'))
|
||||||
|
return render_template('login.html', form=form)
|
||||||
|
|
||||||
|
@bp.route('/logout')
|
||||||
|
def logout():
|
||||||
|
logout_user()
|
||||||
|
flash('You have been logged out.')
|
||||||
|
return redirect_to_home()
|
||||||
|
|
||||||
|
@bp.route('/signup', methods=['GET', 'POST'])
|
||||||
|
def signup():
|
||||||
|
form = SignupForm()
|
||||||
|
if not form.validate_on_submit():
|
||||||
|
return render_template('signup.html', form=form)
|
||||||
|
|
||||||
|
user = User(**form.data)
|
||||||
|
session.add(user)
|
||||||
|
session.commit()
|
||||||
|
flash('New account created.')
|
||||||
|
login_user(user)
|
||||||
|
return redirect(url_for(view_after_signup))
|
||||||
|
|
||||||
|
def redirect_to_doc(doc):
|
||||||
|
return redirect(url_for('.view_document', hashid=doc.hashid))
|
||||||
|
|
||||||
|
def get_source_doc(username, hashid):
|
||||||
|
doc = Item.get_by_hashid(hashid)
|
||||||
|
if doc and doc.user.username != username:
|
||||||
|
doc = None
|
||||||
|
return doc if doc else abort(404)
|
||||||
|
|
||||||
|
def get_item(username, hashid):
|
||||||
|
doc = Item.get_by_hashid(hashid)
|
||||||
|
if doc and doc.user.username != username:
|
||||||
|
doc = None
|
||||||
|
return doc if doc else abort(404)
|
||||||
|
|
||||||
|
@bp.route('/<username>/<hashid>')
|
||||||
|
def view_item(username, hashid):
|
||||||
|
return render_template('view.html',
|
||||||
|
doc=get_item(username, hashid),
|
||||||
|
iter_lines=iter_lines)
|
||||||
|
|
||||||
|
@bp.route('/<username>/<hashid>/edit', methods=['GET', 'POST'])
|
||||||
|
def edit_item(username, hashid):
|
||||||
|
obj = get_item(current_user.username, hashid)
|
||||||
|
form = SourceDocForm(obj=obj)
|
||||||
|
if form.validate_on_submit():
|
||||||
|
form.populate_obj(obj)
|
||||||
|
session.add(obj)
|
||||||
|
session.commit()
|
||||||
|
flash('Changes to {} saved.'.format(obj.type))
|
||||||
|
return redirect(obj.url)
|
||||||
|
return render_template('edit.html', form=form, doc=obj)
|
||||||
|
|
||||||
|
@bp.route('/source_doc_text/<int:source_doc_id>')
|
||||||
|
def source_doc_text(source_doc_id):
|
||||||
|
doc = SourceDoc.query.get(source_doc_id)
|
||||||
|
|
||||||
|
return render_template('source_doc_text.html', doc=doc, iter_lines=iter_lines)
|
||||||
|
|
||||||
|
@bp.route('/settings/account', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def account_settings():
|
||||||
|
form = AccountSettingsForm(obj=current_user)
|
||||||
|
if form.validate_on_submit():
|
||||||
|
form.populate_obj(current_user)
|
||||||
|
session.add(current_user)
|
||||||
|
session.commit()
|
||||||
|
flash('Account details updated.')
|
||||||
|
return redirect(url_for(request.endpoint))
|
||||||
|
return render_template('user/account.html', form=form)
|
||||||
|
|
||||||
|
@bp.route('/new/sourcedoc', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def new_sourcedoc():
|
||||||
|
form = SourceDocForm()
|
||||||
|
if form.validate_on_submit():
|
||||||
|
doc = SourceDoc(user=current_user)
|
||||||
|
form.populate_obj(doc)
|
||||||
|
session.add(doc)
|
||||||
|
session.commit()
|
||||||
|
flash('New document saved.')
|
||||||
|
return redirect(doc.url)
|
||||||
|
return render_template('new.html', form=form, title='source document')
|
||||||
|
|
||||||
|
@bp.route('/new/xanalink', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def new_xanalink():
|
||||||
|
form = ItemForm()
|
||||||
|
if form.validate_on_submit():
|
||||||
|
obj = XanaLink(user=current_user)
|
||||||
|
form.populate_obj(obj)
|
||||||
|
session.add(obj)
|
||||||
|
session.commit()
|
||||||
|
flash('New xanalink saved.')
|
||||||
|
return redirect(obj.url)
|
||||||
|
return render_template('new.html', form=form, title='xanalink')
|
||||||
|
|
||||||
|
@bp.route('/new/xanadoc', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def new_xanadoc():
|
||||||
|
form = ItemForm()
|
||||||
|
if form.validate_on_submit():
|
||||||
|
obj = XanaLink(user=current_user)
|
||||||
|
form.populate_obj(obj)
|
||||||
|
session.add(obj)
|
||||||
|
session.commit()
|
||||||
|
flash('New xanalink saved.')
|
||||||
|
return redirect(obj.url)
|
||||||
|
return render_template('new.html', form=form, title='xanadoc')
|
||||||
|
|
||||||
|
@bp.route('/edit/<filename>', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def edit_source_document(filename):
|
||||||
|
doc = get_source_doc(current_user.username, filename)
|
||||||
|
form = SourceDocForm(obj=doc)
|
||||||
|
if form.validate_on_submit():
|
||||||
|
form.populate_obj(doc)
|
||||||
|
session.add(doc)
|
||||||
|
session.commit()
|
||||||
|
flash('Changes to document saved.')
|
||||||
|
return redirect(doc.url)
|
||||||
|
return render_template('edit.html', form=form, doc=doc)
|
||||||
|
|
||||||
|
@bp.route('/api/1/get/<username>/<filename>')
|
||||||
|
def api_get_document(username, filename):
|
||||||
|
doc = get_source_doc(username, filename)
|
||||||
|
if not doc:
|
||||||
|
return abort(404)
|
||||||
|
ret = {
|
||||||
|
'username': username,
|
||||||
|
'filename': filename,
|
||||||
|
'character_count': len(doc.text),
|
||||||
|
'document_price': str(doc.document_price),
|
||||||
|
'price_per_character': str(doc.price_per_character),
|
||||||
|
}
|
||||||
|
return jsonify(ret)
|
||||||
|
|
||||||
|
@bp.route('/all_titles')
|
||||||
|
def get_all_titles():
|
||||||
|
titles = XanaLink.get_all_titles()
|
||||||
|
for k, v in titles.items():
|
||||||
|
print(from_external(k), v)
|
||||||
|
return ''
|
Loading…
Reference in a new issue