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