Compare commits

..

No commits in common. "main" and "trashpandas" have entirely different histories.

42 changed files with 1008 additions and 1036 deletions

View File

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
""" A webframework for aspiring media terrorists. """ """ A webframework for aspiring media terrorists. """
import os import os
@ -440,32 +442,20 @@ class Poobrain(flask.Flask):
peewee.DoesNotExist: 404 peewee.DoesNotExist: 404
} }
setup_funcs = None
cronjobs = None cronjobs = None
_setup_done = False
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
if not 'root_path' in kwargs: if not 'root_path' in kwargs:
kwargs['root_path'] = str(pathlib.Path('.').absolute()) #TODO: pathlib probably isn't really needed here kwargs['root_path'] = str(pathlib.Path('.').absolute()) #TODO: pathlib probably isn't really needed here
if 'DEBUG' in dir(config) and config.DEBUG:
# There's some shitty overriding going on based on FLASK_ENV.
# Set the env var to override the override and enforce what
# the config says.
os.environ['FLASK_ENV'] = 'development'
else:
os.environ['FLASK_ENV'] = 'production'
super(Poobrain, self).__init__(*args, **kwargs) super(Poobrain, self).__init__(*args, **kwargs)
self.setup_funcs = []
self.cronjobs = [] self.cronjobs = []
@click.group(cls=flask.cli.FlaskGroup, create_app=lambda x=None: self) @click.group(cls=flask.cli.FlaskGroup, create_app=lambda x: self)
@click.option('--database', default=f"sqlite:///{project_name}.db") @click.option('--database', default="sqlite:///%s.db" % project_name)
def cli(database): def cli(database):
self.db = db_url.connect(database) self.db = db_url.connect(database)
self.cli = cli self.cli = cli
@ -494,7 +484,7 @@ class Poobrain(flask.Flask):
user = os.getlogin() user = os.getlogin()
group = grp.getgrgid(os.getgid()).gr_name group = grp.getgrgid(os.getgid()).gr_name
sys.exit(f"Somethings' fucky with the log file: {e}. Current user/group is {user}/{group}.") sys.exit("Somethings' fucky with the log file: %s. Current user/group is %s/%s." % (e,user,group))
if self.debug: if self.debug:
# show SQL queries # show SQL queries
@ -508,7 +498,7 @@ class Poobrain(flask.Flask):
import pudb import pudb
if hasattr(signal, 'SIGINFO'): if hasattr(signal, 'SIGINFO'):
pudb.set_interrupt_handler(signal.SIGINFO) pudb.set_interrupt_handler(signal.SIGINFO)
print(f"{self.name}: a graphical debugger can be invoked with SIGINFO (^T)") print("%s: a graphical debugger can be invoked with SIGINFO (^T)" % (self.name.upper()))
self.debugger = pudb self.debugger = pudb
@ -531,10 +521,10 @@ class Poobrain(flask.Flask):
else: else:
import optparse # Pretty fucking ugly, but at least its in the stdlib. TODO: Can we *somehow* make this work with prompt in cli/__init__.py install command? import optparse # Pretty fucking ugly, but at least its in the stdlib. TODO: Can we *somehow* make this work with prompt in cli/__init__.py install command?
parser = optparse.OptionParser() parser = optparse.OptionParser()
parser.add_option('--database', default=f"sqlite:///{project_name}.db", dest='database') # NOTE: If you change this, you'll also have to change the --database default in cli/__init__.py or else install will fuck up parser.add_option('--database', default="sqlite:///%s.db" % project_name, dest='database') # NOTE: If you change this, you'll also have to change the --database default in cli/__init__.py or else install will fuck up
(options, _) = parser.parse_args() (options, _) = parser.parse_args()
self.logger.warning(f"No DATABASE in config, using generated default or --database parameter '{options.database}'. This should only happen before the install command is executed.") self.logger.warning("No DATABASE in config, using generated default or --database parameter '%s'. This should only happen before the install command is executed." % options.database)
self.db = db_url.connect(options.database) self.db = db_url.connect(options.database)
self.add_url_rule('/theme/<path:resource>', 'serve_theme_resources', self.serve_theme_resources) self.add_url_rule('/theme/<path:resource>', 'serve_theme_resources', self.serve_theme_resources)
@ -548,15 +538,7 @@ class Poobrain(flask.Flask):
self.site = Pooprint('site', 'site') self.site = Pooprint('site', 'site')
self.admin = Pooprint('admin', 'admin') self.admin = Pooprint('admin', 'admin')
def full_dispatch_request(self):
if not self._setup_done:
self.run_setup()
return super().full_dispatch_request()
def main(self): def main(self):
#self.cli(obj={}) #self.cli(obj={})
self.cli() self.cli()
@ -566,28 +548,18 @@ class Poobrain(flask.Flask):
return True # autoescape everything return True # autoescape everything
def setup(self, f): def try_trigger_before_first_request_functions(self):
self.setup_funcs.append(f) if not self.setup in self.before_first_request_funcs:
self.before_first_request_funcs.append(self.setup)
super(Poobrain, self).try_trigger_before_first_request_functions()
return f
def setup(self):
def run_setup(self):
"""
Global runtime setup. Calls all @app.setup decorated functions
and registers site and admin blueprints.
Called by request_setup on first request.
"""
for f in self.setup_funcs:
f()
self.register_blueprint(self.site) self.register_blueprint(self.site)
self.register_blueprint(self.admin, url_prefix='/admin/') self.register_blueprint(self.admin, url_prefix='/admin/')
self._setup_done = True
@locked_cached_property @locked_cached_property
def theme_paths(self): def theme_paths(self):
@ -603,7 +575,7 @@ class Poobrain(flask.Flask):
return paths return paths
def serve_theme_resources(self, resource): def serve_theme_resources(self, resource):
r = False r = False
@ -642,6 +614,7 @@ class Poobrain(flask.Flask):
except jinja2.exceptions.TemplateNotFound: except jinja2.exceptions.TemplateNotFound:
abort(404) abort(404)
else: else:
paths = [os.path.join(path, resource) for path in app.theme_paths] paths = [os.path.join(path, resource) for path in app.theme_paths]
@ -658,12 +631,12 @@ class Poobrain(flask.Flask):
r.cache_control.public = True r.cache_control.public = True
r.cache_control.max_age = app.config['CACHE_LONG'] r.cache_control.max_age = app.config['CACHE_LONG']
return r return r
abort(404) abort(404)
def request_setup(self): def request_setup(self):
flask.g.boxes = {} flask.g.boxes = {}
flask.g.forms = {} flask.g.forms = {}
#self.db.close() # fails first request and thus always on sqlite #self.db.close() # fails first request and thus always on sqlite
@ -689,7 +662,7 @@ class Poobrain(flask.Flask):
flask.g.user = cert_info.user flask.g.user = cert_info.user
except auth.ClientCert.DoesNotExist: except auth.ClientCert.DoesNotExist:
self.logger.error(f"httpd verified client certificate successfully, but it's not known at this site. CN: {cert.get_subject()}, digest: {cert.digest('sha512')}") self.logger.error("httpd verified client certificate successfully, but it's not known at this site. CN: %s, digest: %s" % (cert.get_subject().CN, cert.digest('sha512')))
if flask.g.user == None: if flask.g.user == None:
try: try:
@ -729,12 +702,12 @@ class Poobrain(flask.Flask):
if not extra_modes is None: if not extra_modes is None:
for extra_mode in extra_modes: for extra_mode in extra_modes:
self.site.add_view(cls, os.path.join(rule, f"<handle>/{extra_mode}"), mode=extra_mode, force_secure=force_secure) self.site.add_view(cls, os.path.join(rule, '<handle>/%s' % extra_mode), mode=extra_mode, force_secure=force_secure)
for related_model, related_fields in cls._meta.model_backrefs.items(): # Add Models that are associated by ForeignKeyField, like /user/foo/userpermissions for related_model, related_fields in cls._meta.model_backrefs.items(): # Add Models that are associated by ForeignKeyField, like /user/foo/userpermissions
if len(related_fields) > 1: if len(related_fields) > 1:
self.logger.debug("!!! Apparent multi-field relation for {related_model.__name__}: {related_fields} !!!") self.logger.debug("!!! Apparent multi-field relation for %s: %s !!!" % (related_model.__name__, related_fields))
if issubclass(related_model, auth.Administerable): if issubclass(related_model, auth.Administerable):
self.site.add_related_view(cls, related_fields[0], os.path.join(rule, '<handle>/')) self.site.add_related_view(cls, related_fields[0], os.path.join(rule, '<handle>/'))
@ -794,18 +767,21 @@ class Poobrain(flask.Flask):
except LookupError: except LookupError:
pass pass
raise LookupError(f"Failed generating URL for {cls.__name__}[{url_params.get('handle', None)}]-{mode}. No matching route found.") raise LookupError("Failed generating URL for %s[%s]-%s. No matching route found." % (cls.__name__, url_params.get('handle', None), mode))
def get_related_view_url(self, cls, handle, related_field, add=None): def get_related_view_url(self, cls, handle, related_field, add=None):
blueprint = self.blueprints[flask.request.blueprint] blueprint = self.blueprints[flask.request.blueprint]
return blueprint.get_related_view_url(cls, handle, related_field, add=add) return blueprint.get_related_view_url(cls, handle, related_field, add=add)
def cron(self, func): def cron(self, func):
self.cronjobs.append(func) self.cronjobs.append(func)
return func return func
def cron_run(self): def cron_run(self):
self.logger.info("Starting cron run.") self.logger.info("Starting cron run.")
@ -847,9 +823,9 @@ class Pooprint(flask.Blueprint):
self.before_request(self.box_setup) self.before_request(self.box_setup)
def register(self, app, options): def register(self, app, options, first_registration=False):
super(Pooprint, self).register(app, options) super(Pooprint, self).register(app, options, first_registration=first_registration)
self.app = app self.app = app
self.db = app.db self.db = app.db
@ -893,7 +869,7 @@ class Pooprint(flask.Blueprint):
self.related_views[cls] = collections.OrderedDict() self.related_views[cls] = collections.OrderedDict()
if not related_field in self.related_views[cls]: if not related_field in self.related_views[cls]:
url_segment = f"{related_model.__name__.lower()}:{related_field.name.lower()}" url_segment = '%s:%s' % (related_model.__name__.lower(), related_field.name.lower())
rule = os.path.join(rule, url_segment, "") # empty string to get trailing slash rule = os.path.join(rule, url_segment, "") # empty string to get trailing slash
self.related_views[cls][related_field] = [] self.related_views[cls][related_field] = []
@ -914,7 +890,7 @@ class Pooprint(flask.Blueprint):
return cls.related_view_add(*args, **kwargs) return cls.related_view_add(*args, **kwargs)
offset_rule = os.path.join(rule, '+<int:offset>') offset_rule = os.path.join(rule, '+<int:offset>')
offset_endpoint = f"{endpoint}_offset" offset_endpoint = '%s_offset' % (endpoint,)
add_rule = os.path.join(rule, 'add') add_rule = os.path.join(rule, 'add')
add_endpoint = endpoint + '_add' add_endpoint = endpoint + '_add'
@ -976,7 +952,7 @@ class Pooprint(flask.Blueprint):
view_func = helpers.is_secure(view_func) # manual decoration, cause I don't know how to do this cleaner view_func = helpers.is_secure(view_func) # manual decoration, cause I don't know how to do this cleaner
offset_rule = rule+'+<int:offset>' offset_rule = rule+'+<int:offset>'
offset_endpoint = f"{endpoint}_offset" offset_endpoint = '%s_offset' % (endpoint,)
self.add_url_rule(rule, endpoint=endpoint, view_func=view_func, **options) self.add_url_rule(rule, endpoint=endpoint, view_func=view_func, **options)
self.add_url_rule(offset_rule, endpoint=offset_endpoint, view_func=view_func, **options) self.add_url_rule(offset_rule, endpoint=offset_endpoint, view_func=view_func, **options)
@ -1016,8 +992,7 @@ class Pooprint(flask.Blueprint):
if not_too_many_params and missing_all_optional: if not_too_many_params and missing_all_optional:
return endpoint return endpoint
nice_param_list = ', '.join(url_params.keys()) raise ValueError("No fitting url rule found for all params: %s", ','.join(url_params.keys()))
raise ValueError(f"No fitting url rule found for all params: {nice_param_list}")
def get_url(self, cls, mode=None, **url_params): def get_url(self, cls, mode=None, **url_params):
@ -1036,12 +1011,13 @@ class Pooprint(flask.Blueprint):
mode = 'full' mode = 'full'
if not cls in self.views: if not cls in self.views:
raise LookupError(f"No registered views for class {cls.__name__}.") raise LookupError("No registered views for class %s." % (cls.__name__,))
if not mode in self.views[cls]: if not mode in self.views[cls]:
raise LookupError(f"No registered views for class {cls.__name__} with mode {mode}.") raise LookupError("No registered views for class %s with mode %s." % (cls.__name__, mode))
endpoints = [f"{self.name}.{x}" for x in self.views[cls][mode]]
endpoints = ['%s.%s' % (self.name, x) for x in self.views[cls][mode]]
if len(endpoints) > 1: if len(endpoints) > 1:
endpoint = self.choose_endpoint(endpoints, **url_params) endpoint = self.choose_endpoint(endpoints, **url_params)
else: else:
@ -1070,12 +1046,12 @@ class Pooprint(flask.Blueprint):
offset = cls.select().where(*clauses).count() - 1 offset = cls.select().where(*clauses).count() - 1
if not cls in self.listings: if not cls in self.listings:
raise LookupError(f"No registered listings for class {cls.__name__}.") raise LookupError("No registered listings for class %s." % (cls.__name__,))
if not mode in self.listings[cls]: if not mode in self.listings[cls]:
raise LookupError(f"No registered listings for class {cls.__name__} with mode {mode}.") raise LookupError("No registered listings for class %s with mode %s." % (cls.__name__, mode))
endpoints = [f"{self.name}.{x}" for x in self.listings[cls][mode]] endpoints = ['%s.%s' % (self.name, x) for x in self.listings[cls][mode]]
endpoint = self.choose_endpoint(endpoints) endpoint = self.choose_endpoint(endpoints)
# if isinstance(offset, int) and offset > 0: # if isinstance(offset, int) and offset > 0:
@ -1084,7 +1060,7 @@ class Pooprint(flask.Blueprint):
kw = copy.copy(url_params) kw = copy.copy(url_params)
if offset > 0: if offset > 0:
kw['offset'] = offset kw['offset'] = offset
endpoint = f"{endpoint}_offset" endpoint = "%s_offset" % endpoint
return flask.url_for(endpoint, **kw) return flask.url_for(endpoint, **kw)
@ -1099,12 +1075,12 @@ class Pooprint(flask.Blueprint):
if not cls in lookup: if not cls in lookup:
raise LookupError(f"No registered related views for class {cls.__name__}.") raise LookupError("No registered related views for class %s." % (cls.__name__,))
if not related_field in lookup[cls]: if not related_field in lookup[cls]:
raise LookupError(f"No registered related views for {cls.__name__}[{handle}]<-{related_field.model.__name__}.{related_field.name}.") raise LookupError("No registered related views for %s[%s]<-%s.%s." % (cls.__name__, handle, related_field.model.__name__, related_field.name))
endpoints = [f"{self.name}.{x}" for x in lookup[cls][related_field]] endpoints = ['%s.%s' % (self.name, x) for x in lookup[cls][related_field]]
endpoint = self.choose_endpoint(endpoints, **{'handle': handle}) endpoint = self.choose_endpoint(endpoints, **{'handle': handle})
return flask.url_for(endpoint, handle=handle) return flask.url_for(endpoint, handle=handle)
@ -1112,7 +1088,7 @@ class Pooprint(flask.Blueprint):
def next_endpoint(self, cls, mode, context): # TODO: rename mode because it's not an applicable name for 'related' context def next_endpoint(self, cls, mode, context): # TODO: rename mode because it's not an applicable name for 'related' context
endpoint_base = f"{cls.__name__}_{context}_{mode}" format = '%s_%s_%s_autogen_%%d' % (cls.__name__, context, mode)
try: try:
if context == 'view': if context == 'view':
@ -1121,16 +1097,16 @@ class Pooprint(flask.Blueprint):
endpoints = self.listings[cls][mode] endpoints = self.listings[cls][mode]
elif context == 'related': elif context == 'related':
# mode is actually a foreign key field # mode is actually a foreign key field
endpoint_base = f"{cls.__name__}_{context}_{mode.model.__name__}-{mode.name}_autogen" format = '%s_%s_%s-%s_autogen_%%d' % (cls.__name__, context, mode.model.__name__, mode.name)
endpoints = self.related_views[cls][mode] endpoints = self.related_views[cls][mode]
except KeyError: # means no view/listing has been registered yet except KeyError: # means no view/listing has been registered yet
endpoints = [] endpoints = []
i = 1 i = 1
endpoint = f"{endpoint_base}_{i}" endpoint = format % (i,)
while endpoint in endpoints: while endpoint in endpoints:
endpoint = f"{endpoint_base}_{i}" endpoint = format % (i,)
i += 1 i += 1
return endpoint return endpoint
@ -1188,7 +1164,7 @@ class ErrorPage(rendering.Renderable):
self.code = code self.code = code
break break
self.title = f"Ermahgerd, {self.code}!" self.title = "Ermahgerd, %s!" % self.code
if isinstance(self.error, errors.ExposedError): if isinstance(self.error, errors.ExposedError):
self.message = str(error) self.message = str(error)
@ -1211,7 +1187,7 @@ class ErrorPage(rendering.Renderable):
def errorpage(error): def errorpage(error):
tb = None tb = None
app.logger.error(f"Error {type(error).__name__} when accessing {flask.request.path}: {error}") app.logger.error('Error %s when accessing %s: %s' % (type(error).__name__, flask.request.path, str(error)))
if app.config['DEBUG']: if app.config['DEBUG']:
if hasattr(error, 'code'): if hasattr(error, 'code'):

View File

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
from poobrains import app from poobrains import app
from . import util from . import util

View File

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
import collections import collections
import io import io
import re import re
@ -50,7 +52,7 @@ def load_dataset(handle):
ds.data[name] = source['function'](**parameters) ds.data[name] = source['function'](**parameters)
else: else:
raise poobrains.errors.ExposedError(f'Unknown dynamic dataset: {name}') raise poobrains.errors.ExposedError('Unknown dynamic dataset: %s' % name)
else: else:
@ -352,7 +354,7 @@ class EphemeralDataset(poobrains.auth.Protected):
@locked_cached_property @locked_cached_property
def ref_id(self): def ref_id(self):
return f"dataset-{self.name}" return "dataset-%s" % self.name
@locked_cached_property @locked_cached_property
def empty(self): def empty(self):
@ -509,7 +511,7 @@ class EphemeralDataset(poobrains.auth.Protected):
plot_kinds = visualization.Plot.class_children_keyed() plot_kinds = visualization.Plot.class_children_keyed()
if not kind in plot_kinds: if not kind in plot_kinds:
raise ValueError(f'Unknown plot kind: {kind}') raise ValueError('Unknown plot kind: %s' % kind)
return plot_kinds[kind](dataset=self, layers=plot_info['layers'], options=plot_info.get('options')) return plot_kinds[kind](dataset=self, layers=plot_info['layers'], options=plot_info.get('options'))
@ -552,8 +554,8 @@ class EphemeralDataset(poobrains.auth.Protected):
ds.owner = owner or g.user ds.owner = owner or g.user
now = int(time.time()) now = int(time.time())
ds.name = name or poobrains.helpers.clean_string(f"{self.name}-{now}") ds.name = name or poobrains.helpers.clean_string("%s-%d" % (self.name, now))
ds.title = f'{self.title}@{str(datetime.datetime.fromtimestamp(now))}' ds.title = '%s@%s' % (self.title, str(datetime.datetime.fromtimestamp(now)))
ds.description = self.description ds.description = self.description
ds.data = self.data ds.data = self.data
ds.save(force_insert=True) ds.save(force_insert=True)
@ -576,8 +578,6 @@ class Dataset(EphemeralDataset, poobrains.commenting.Commentable):
class Meta: class Meta:
database = app.db
modes = collections.OrderedDict([ modes = collections.OrderedDict([
('add', 'create'), ('add', 'create'),
('teaser', 'read'), ('teaser', 'read'),
@ -600,7 +600,7 @@ class Dataset(EphemeralDataset, poobrains.commenting.Commentable):
@locked_cached_property @locked_cached_property
def ref_id(self): def ref_id(self):
return f"dataset-{self.name}" return "dataset-%s" % self.name
@classmethod @classmethod
def load(self, handle): def load(self, handle):

View File

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
import math import math
import re import re
@ -41,7 +43,7 @@ def datasource_choices():
def validate_handle_free(handle): def validate_handle_free(handle):
if handle in session['apps']['DataEditor']: if handle in session['apps']['DataEditor']:
if poobrains.analysis.data.Dataset.select().where(poobrains.analysis.data.Dataset.name == handle).count() == 1: if poobrains.analysis.data.Dataset.select().where(poobrains.analysis.data.Dataset.name == handle).count() == 1:
raise poobrains.errors.ValidationError(f"Dataset named '{handle}' already exists!") raise poobrains.errors.ValidationError("Dataset named '%s' already exists!" % handle)
else: else:
del(session['apps']['DataEditor'][handle]) # FIXME: should technically be in a different function, i guess del(session['apps']['DataEditor'][handle]) # FIXME: should technically be in a different function, i guess
@ -379,17 +381,15 @@ class EditorDatasetMetaEdit(poobrains.formapp.MultistateFieldset):
super().build() super().build()
self['basedata_form'] = poobrains.form.ProxyFieldset(poobrains.auth.AutoForm(self.formapp.instance, mode='edit')) self['dataset_title'] = poobrains.form.fields.Text(label='Title', default=self.formapp.dataset.title)
#self['dataset_title'] = poobrains.form.fields.Text(label='Title', default=self.formapp.dataset.title) self['dataset_description'] = poobrains.form.fields.TextArea(label='Description', default=self.formapp.dataset.description)
#self['dataset_description'] = poobrains.form.fields.TextArea(label='Description', default=self.formapp.dataset.description)
self.apply = poobrains.form.Button('submit', label='Apply') self.apply = poobrains.form.Button('submit', label='Apply')
def process(self, submit): def process(self, submit):
if submit == 'apply': if submit == 'apply':
#self.formapp.dataset.title = self['dataset_title'].value self.formapp.dataset.title = self['dataset_title'].value
#self.formapp.dataset.description = self['dataset_description'].value self.formapp.dataset.description = self['dataset_description'].value
self['basedata_form'].process('save')
flash("Updated dataset base info.") flash("Updated dataset base info.")
self.parent.tool_active = None self.parent.tool_active = None

View File

@ -77,15 +77,12 @@ class FileFieldset(poobrains.formapp.MultistateFieldset):
self['file'] = poobrains.form.fields.File() self['file'] = poobrains.form.fields.File()
self['consumer'] = poobrains.form.fields.Select(choices=( self['consumer'] = poobrains.form.fields.Select(choices=(
('csv', "CSV"), ('csv', "CSV"),
('geojson', "geojson"), ('geojson', "geojson"),
)) ))
if self.state == 'csv': elif self.state == 'csv':
self['consumer'].value = 'csv'
self['consumer'].readonly = True
csv_options = self.session['csv_options'] csv_options = self.session['csv_options']
@ -105,14 +102,13 @@ class FileFieldset(poobrains.formapp.MultistateFieldset):
flash(f"Full error message: {e}", 'error') flash(f"Full error message: {e}", 'error')
else: else:
self.session['csv_options'] = csv_options
table = poobrains.rendering.Table(title='CSV preview') table = poobrains.rendering.Table(title='CSV preview')
for idx, row in enumerate(reader): for idx, row in enumerate(reader):
if idx == 5: if idx == 5:
break break
table.append(*row) table.append(*row)
self['table'] = poobrains.form.fields.RenderableWrapper(table) self['table'] = poobrains.form.fields.RenderableWrapper(value=table)
self.approve = poobrains.form.Button('submit', label='Approve') self.approve = poobrains.form.Button('submit', label='Approve')
escaped = {} escaped = {}
@ -138,46 +134,21 @@ class FileFieldset(poobrains.formapp.MultistateFieldset):
def process(self, submit): def process(self, submit):
if self.state == 'csv': if submit == 'change_csv_options':
if submit == 'change_csv_options': unescape = lambda x: bytes(x, 'utf-8').decode('unicode_escape')
unescape = lambda x: bytes(x, 'utf-8').decode('unicode_escape') self.session['csv_options'] = {
self.session['csv_options'] = { 'delimiter': unescape(self['delimiter'].value),
'delimiter': unescape(self['delimiter'].value or ''), 'doublequote': self['doublequote'].value,
'doublequote': self['doublequote'].value, 'escapechar': unescape(self['escapechar'].value),
'escapechar': unescape(self['escapechar'].value or ''), 'lineterminator': unescape(self['lineterminator'].value),
'lineterminator': unescape(self['lineterminator'].value), 'quotechar': unescape(self['quotechar'].value),
'quotechar': unescape(self['quotechar'].value or ''), 'quoting': self['quoting'].value,
'quoting': self['quoting'].value, 'skipinitialspace': self['skipinitialspace'].value,
'skipinitialspace': self['skipinitialspace'].value, }
}
flash("Changed CSV dialect options.") flash("Changed CSV dialect options.")
else:
dialect = dialect_from_dict(self.session['csv_options'])
try: else: # means parent form wasn't submitting via cancel button i.e. the "load" button was clicked
reader = csv.reader(io.StringIO(self.session['raw']), dialect)
except csv.Error as e:
flash("Can't read file as CSV with supplied options.", 'error')
if app.debug:
flash(f"Full error message: {e}", 'error')
ds = self.owner()
ds['csv_col_fixme'] = {
'title': "CSV Column",
'description': 'FIXME: CSV has no type recognition, everything is float64',
'dtype': 'float64',
'color': None,
'observations': {}
}
for idx, row in enumerate(reader):
ds['csv_col_fixme']['observations'][idx] = util.float64.string_numpy(row[0]);
self.parent.session['other'] = ds.to_dict(whole=True)
self.parent.state = 'merge'
else: # submitted via load button with file upload
consumer = self['consumer'].value consumer = self['consumer'].value
raw = self['file'].value.read() raw = self['file'].value.read()
@ -213,7 +184,7 @@ class FileFieldset(poobrains.formapp.MultistateFieldset):
elif consumer == 'csv': elif consumer == 'csv':
self.state = 'csv' self.state = 'csv'
self.session['csv_options'] = None self.session['data_action_info']['csv_options'] = None
else: else:
flash("Unhandled", 'error') flash("Unhandled", 'error')

View File

@ -1,4 +1,3 @@
import functools
import datetime import datetime
import numpy import numpy
import shapely.geometry import shapely.geometry
@ -28,14 +27,12 @@ def pretty_si(number):
value /= 1000.0 value /= 1000.0
return f"{value:.3f}{postfix}" return "%.3f%s" % (value, postfix)
def pretty_datetime64(dt64): def pretty_datetime64(dt64):
dt = dt64.item() dt = dt64.item()
return f"{dt.day}. {dt.month}. {dt.year} {dt.hour}:{dt.minute}:{dt.second}", return f"{dt.day}. {dt.month}. {dt.year} {dt.hour}:{dt.minute}:{dt.second}",
__geo_lookup__ = { __geo_lookup__ = {
geojson.Point: shapely.geometry.Point, geojson.Point: shapely.geometry.Point,
geojson.MultiPoint: shapely.geometry.MultiPoint, geojson.MultiPoint: shapely.geometry.MultiPoint,
@ -223,7 +220,7 @@ class DTypeFilterForm(DTypeOwnedMultistateFieldset):
self['op'] = poobrains.form.fields.Select(label='Operation', choices=self.dtype.filter_operations) self['op'] = poobrains.form.fields.Select(label='Operation', choices=self.dtype.filter_operations)
if isinstance(self.dtype.form_field, functools.partial) and issubclass(self.dtype.form_field.func, poobrains.formapp.Component): # dtype.form_field is a partial with .func being the actual class object for any Fieldset because that's derived from ClassOrInstanceBound if issubclass(self.dtype.form_field.func, poobrains.formapp.Component): # dtype.form_field is a partial with .func being the actual class object
self['value'] = self.dtype.form_field(self) self['value'] = self.dtype.form_field(self)
else: else:
self['value'] = self.dtype.form_field(type=self.dtype.form_type) self['value'] = self.dtype.form_field(type=self.dtype.form_type)

View File

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
import math import math
import pyproj import pyproj
@ -92,10 +94,10 @@ class Plot(poobrains.svg.SVG):
return [k for k in self.layers.keys()].index(layer_name) return [k for k in self.layers.keys()].index(layer_name)
def layer_id(self, layer_name): def layer_id(self, layer_name):
return f"dataset-{self.dataset.name}-{poobrains.helpers.clean_string(layer_name)}" return "dataset-%s-%s" % (self.dataset.name, poobrains.helpers.clean_string(layer_name))
def datapoint_id(self, layer_name, x): def datapoint_id(self, layer_name, x):
return f"dataset-{self.dataset.name}-{poobrains.helpers.clean_string(layer_name)}-{poobrains.helpers.clean_string(str(x))}" return "dataset-%s-%s-%s" % (self.dataset.name, poobrains.helpers.clean_string(layer_name), poobrains.helpers.clean_string(str(x)))
@locked_cached_property @locked_cached_property
def palette(self): def palette(self):

View File

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
""" """
The authentication system. The authentication system.
@ -39,7 +41,7 @@ def enforce_tls():
)) ))
@app.setup @app.before_first_request
def admin_setup(): def admin_setup():
if not app._got_first_request: if not app._got_first_request:
@ -156,28 +158,28 @@ class AutoForm(BoundForm):
def __init__(self, model_or_instance, mode='add', prefix=None, name=None, title=None, method=None, action=None): def __init__(self, model_or_instance, mode='add', prefix=None, name=None, title=None, method=None, action=None):
if not name: if not name:
name = f"{self.model.__name__}-{self.instance.handle_string.replace('.', '-')}" name = '%s-%s' % (self.model.__name__, self.instance.handle_string.replace('.', '-'))
super(AutoForm, self).__init__(model_or_instance, mode=mode, prefix=prefix, name=name, title=title, method=method, action=action) super(AutoForm, self).__init__(model_or_instance, mode=mode, prefix=prefix, name=name, title=title, method=method, action=action)
if not title: if not title:
if hasattr(self.instance, 'title') and self.instance.title: if hasattr(self.instance, 'title') and self.instance.title:
self.title = f"{self.mode} {self.model.__name__} '{self.instance.title}'" self.title = "%s %s '%s'" % (self.mode, self.model.__name__, self.instance.title)
elif self.instance.name: elif self.instance.name:
self.title = f"{self.mode} {self.model.__name__} '{self.instance.name}'" self.title = "%s %s '%s'" % (self.mode, self.model.__name__, self.instance.name)
elif self.instance.id: elif self.instance.id:
self.title = f"{self.mode} {self.model.__name__} #{self.instance.id}" self.title = "%s %s #%d" % (self.mode, self.model.__name__, self.instance.id)
else: else:
try: try:
if self.instance._pk: if self.instance._pk:
self.title = f"{self.mode} {self.model.__name__} '{self.instance._pk}'" self.title = "%s %s '%s'" % (self.mode, self.model.__name__, self.instance._pk)
else: else:
self.title = f"{self.mode} {self.model.__name__}" self.title = "%s %s" % (self.mode, self.model.__name__)
except Exception as e: except Exception as e:
self.title = f"{self.mode} {self.model.__name__}" self.title = "%s %s" % (self.mode, self.model.__name__)
for name, field in self.fields.items(): for name, field in self.fields.items():
#if hasattr(self.instance, name): # fucks up with DoesNotExist on unfilled foreign keys #if hasattr(self.instance, name): # fucks up with DoesNotExist on unfilled foreign keys
@ -216,7 +218,7 @@ class AutoForm(BoundForm):
saved = self.instance.save() saved = self.instance.save()
if saved: if saved:
flash(f"Saved {self.model.__name__} {self.instance.handle_string}.") flash(u"Saved %s %s." % (self.model.__name__, self.instance.handle_string))
try: try:
self.process_all_fieldsets(submit) self.process_all_fieldsets(submit)
except Exception as e: except Exception as e:
@ -234,7 +236,7 @@ class AutoForm(BoundForm):
return self return self
else: else:
flash(f"Couldn't save {self.model.__name__}.") flash(u"Couldn't save %s." % self.model.__name__)
except poobrains.errors.ValidationError as e: except poobrains.errors.ValidationError as e:
@ -248,21 +250,21 @@ class AutoForm(BoundForm):
if exceptions: if exceptions:
raise raise
flash(f'Integrity error: {str(e)}', 'error') flash(u'Integrity error: %s' % str(e), 'error')
app.logger.error(f"Integrity error: {str(e)}") app.logger.error(u"Integrity error: %s" % str(e))
except Exception as e: except Exception as e:
if exceptions: if exceptions:
raise raise
flash(f"Couldn't save {self.model.__name__} for mysterious reasons.") flash(u"Couldn't save %s for mysterious reasons." % self.model.__name__)
app.logger.error(f"Couldn't save {self.model.__name__}. {type(e).__name__}: {str(e)}") app.logger.error(u"Couldn't save %s. %s: %s" % (self.model.__name__, type(e).__name__, str(e)))
elif submit == 'preview': elif submit == 'preview':
self.preview = self.instance.render('full') self.preview = self.instance.render('full')
else: else:
flash(f"Not handling readonly form '{self.name}'.") flash(u"Not handling readonly form '%s'." % self.name)
return self return self
@ -275,7 +277,7 @@ class DeleteForm(BoundForm):
f = super(DeleteForm, cls).__new__(cls, model_or_instance, prefix=prefix, name=None, title=title, method=method, action=action) f = super(DeleteForm, cls).__new__(cls, model_or_instance, prefix=prefix, name=None, title=title, method=method, action=action)
f.title = f"Delete {f.instance.name}" f.title = "Delete %s" % f.instance.name
f.warning = poobrains.form.fields.Message('deletion_irrevocable', value='Deletion is not revocable. Proceed?') f.warning = poobrains.form.fields.Message('deletion_irrevocable', value='Deletion is not revocable. Proceed?')
f.submit = poobrains.form.Button('submit', name='submit', value='delete', label=u'') f.submit = poobrains.form.Button('submit', name='submit', value='delete', label=u'')
@ -285,16 +287,16 @@ class DeleteForm(BoundForm):
super(DeleteForm, self).__init__(model_or_instance, mode=mode, prefix=prefix, name=self.name, title=title, method=method, action=action) super(DeleteForm, self).__init__(model_or_instance, mode=mode, prefix=prefix, name=self.name, title=title, method=method, action=action)
if not title: if not title:
if hasattr(self.instance, 'title') and self.instance.title: if hasattr(self.instance, 'title') and self.instance.title:
self.title = f"Delete {self.model.__name__} {self.instance.title}" self.title = "Delete %s %s" % (self.model.__name__, self.instance.title)
else: else:
self.title = f"Delete {self.model.__name__} {str(self.instance._pk)}" self.title = "Delete %s %s" % (self.model.__name__, str(self.instance._pk))
def process(self, submit): def process(self, submit):
if hasattr(self.instance, 'title') and self.instance.title: if hasattr(self.instance, 'title') and self.instance.title:
message = f"Deleted {self.model.__name__} '{self.instance.title}'." message = "Deleted %s '%s'." % (self.model.__name__, self.instance.title)
else: else:
message = f"Deleted {self.model.__name__} '{str(self.instance._pk)}'." message = "Deleted %s '%s'." % (self.model.__name__, str(self.instance._pk))
self.instance.delete_instance(recursive=True) self.instance.delete_instance(recursive=True)
flash(message) flash(message)
@ -327,7 +329,7 @@ class AccessField(poobrains.form.fields.Field):
if name == 'prefix': if name == 'prefix':
for field in [self.read, self.update, self.delete]: for field in [self.read, self.update, self.delete]:
field.prefix = f'{value}.{self.name}' field.prefix = '%s.%s' % (value, self.name)
elif name == 'value': elif name == 'value':
@ -393,7 +395,7 @@ class Permission(poobrains.helpers.ChildAware):
@classmethod @classmethod
def check(cls, user): def check(cls, user):
msg = f"Permission {cls.__name__} denied." msg = "Permission %s denied." % cls.__name__
granted = None granted = None
# check user-assigned permission state # check user-assigned permission state
@ -445,7 +447,7 @@ class Permission(poobrains.helpers.ChildAware):
granted = self._check_values[user] granted = self._check_values[user]
if not granted: if not granted:
raise AccessDenied(f"Permission {self.__class__.__name__} denied.") raise AccessDenied("Permission %s denied." % self.__class__.__name__)
return granted return granted
@ -502,8 +504,8 @@ class PermissionInjection(poobrains.helpers.MetaCompatibility):
#for op in ['create', 'read', 'update', 'delete']: #for op in ['create', 'read', 'update', 'delete']:
for op in set(cls._meta.modes.values()): for op in set(cls._meta.modes.values()):
perm_name = f"{cls.__name__}_{op}" perm_name = "%s_%s" % (cls.__name__, op)
perm_label = f"{op.capitalize()} {cls.__name__}" perm_label = "%s %s" % (op.capitalize(), cls.__name__)
#cls._meta.permissions[mode] = type(perm_name, (cls._meta.permission_class,), {}) #cls._meta.permissions[mode] = type(perm_name, (cls._meta.permission_class,), {})
perm_attrs = dict() perm_attrs = dict()
perm_attrs['op'] = op perm_attrs['op'] = op
@ -536,7 +538,7 @@ class PermissionParamType(poobrains.form.types.StringParamType):
try: try:
permission, access = cleaned_string.split('.') permission, access = cleaned_string.split('.')
except Exception as e: except Exception as e:
self.fail(f'Could not split value to permission and access: {cleaned_string}') self.fail('Could not split value to permission and access: %s' % cleaned_string)
return (permission, access) return (permission, access)
@ -558,25 +560,25 @@ class FormPermissionField(poobrains.form.fields.Select):
permissions = Permission.class_children_keyed() permissions = Permission.class_children_keyed()
for perm_name in sorted(permissions): for perm_name in sorted(permissions):
perm = permissions[perm_name] perm = permissions[perm_name]
self.choices.append(([(f'{perm_name}.{value}', label) for (value, label) in perm.choices], perm_name)) self.choices.append(([('%s.%s' % (perm_name, value), label) for (value, label) in perm.choices], perm_name))
def validate(self): def validate(self):
permission, access = self.value permission, access = self.value
if not permission in Permission.class_children_keyed().keys(): if not permission in Permission.class_children_keyed().keys():
raise poobrains.errors.ValidationError(f'Unknown permission: {permission}') raise poobrains.errors.ValidationError('Unknown permission: %s' % permission)
perm_class = Permission.class_children_keyed()[permission] perm_class = Permission.class_children_keyed()[permission]
choice_values = [t[0] for t in perm_class.choices] choice_values = [t[0] for t in perm_class.choices]
if not access in choice_values: if not access in choice_values:
raise poobrains.errors.ValidationError(f"Unknown access mode '{access}' for permission '{permission}'.") raise poobrains.errors.ValidationError("Unknown access mode '%s' for permission '%s'." % (access, permission))
def admin_listing_actions(cls): def admin_listing_actions(cls):
m = poobrains.rendering.Menu('actions') m = poobrains.rendering.Menu('actions')
if 'add' in cls._meta.modes: if 'add' in cls._meta.modes:
m.append(cls.url('add'), f'add new {cls.__name__}') m.append(cls.url('add'), 'add new %s' % (cls.__name__,))
return m return m
@ -595,7 +597,7 @@ def admin_menu():
for mode, endpoints in listings.items(): for mode, endpoints in listings.items():
for endpoint in endpoints: # iterates through endpoints.keys() for endpoint in endpoints: # iterates through endpoints.keys()
menu.append(url_for(f'admin.{endpoint}'), administerable.__name__) menu.append(url_for('admin.%s' % endpoint), administerable.__name__)
return menu return menu
@ -616,11 +618,11 @@ def admin_index():
for administerable, listings in app.admin.listings.items(): for administerable, listings in app.admin.listings.items():
subcontainer = poobrains.rendering.Container(css_class='administerable-actions', mode='full') subcontainer = poobrains.rendering.Container(css_class='administerable-actions', mode='full')
menu = poobrains.rendering.Menu(f'listings-{administerable.__name__}') menu = poobrains.rendering.Menu('listings-%s' % administerable.__name__)
for mode, endpoints in listings.items(): for mode, endpoints in listings.items():
for endpoint in endpoints: # iterates through endpoints.keys() for endpoint in endpoints: # iterates through endpoints.keys()
menu.append(url_for(f'admin.{endpoint}'), administerable.__name__) menu.append(url_for('admin.%s' % endpoint), administerable.__name__)
subcontainer.append(menu) subcontainer.append(menu)
if administerable.__doc__: if administerable.__doc__:
@ -662,16 +664,17 @@ def protected(func):
cls = cls_or_instance cls = cls_or_instance
if not (issubclass(cls, Protected) or isinstance(cls_or_instance, Protected)): if not (issubclass(cls, Protected) or isinstance(cls_or_instance, Protected)):
raise ValueError(f"@protected used with non-protected class '{cls.__name__}'.") raise ValueError("@protected used with non-protected class '%s'." % cls.__name__)
if not mode in cls._meta.modes: if not mode in cls._meta.modes:
raise AccessDenied(f"Unknown mode '{mode}' for accessing {cls.__name__}.") raise AccessDenied("Unknown mode '%s' for accessing %s." % (mode, cls.__name__))
op = cls._meta.modes[mode] op = cls._meta.modes[mode]
if not op in ['create', 'read', 'update', 'delete']: if not op in ['create', 'read', 'update', 'delete']:
raise AccessDenied(f"Unknown access op '{op}' for accessing {cls.__name__}.") raise AccessDenied("Unknown access op '%s' for accessing %s." (op, cls.__name__))
if not op in cls_or_instance.permissions: if not op in cls_or_instance.permissions:
raise NotImplementedError(f"Did not find permission for op '{op}' in cls_or_instance of class '{cls.__name__}'.") raise NotImplementedError("Did not find permission for op '%s' in cls_or_instance of class '%s'." % (op, cls.__name__))
cls_or_instance.permissions[op].check(user) cls_or_instance.permissions[op].check(user)
@ -740,11 +743,11 @@ class ClientCertForm(poobrains.form.Form):
if self.controls['tls_submit'].value: if self.controls['tls_submit'].value:
r = app.response_class(pkcs12.export(passphrase=passphrase)) r = app.response_class(pkcs12.export(passphrase=passphrase))
r.mimetype = 'application/pkcs-12' r.mimetype = 'application/pkcs-12'
flash(f"The passphrase for this delicious bundle of crypto is '{passphrase}'") flash(u"The passphrase for this delicious bundle of crypto is '%s'" % passphrase)
else: # means pgp mail else: # means pgp mail
text = f"Hello {token.user.name}. Here's your new set of keys to the gates of Shambala.\nYour passphrase is '{passphrase}'." text = "Hello %s. Here's your new set of keys to the gates of Shambala.\nYour passphrase is '%s'." % (token.user.name, passphrase)
mail = poobrains.mailing.Mail(token.user.pgp_fingerprint) mail = poobrains.mailing.Mail(token.user.pgp_fingerprint)
mail['Subject'] = 'Bow before entropy' mail['Subject'] = 'Bow before entropy'
@ -756,10 +759,11 @@ class ClientCertForm(poobrains.form.Form):
mail.send() mail.send()
flash(f"Your private key and client certificate have been send to '{token.user.mail}'.") flash(u"Your private key and client certificate have been send to '%s'." % token.user.mail)
r = self r = self
try: try:
cert_info.save() cert_info.save()
@ -809,7 +813,7 @@ class OwnedPermission(Permission):
return True return True
else: else:
app.logger.warning(f"Unknown access mode '{access}' for User #{user.id} with Permission {cls.__name__}") app.logger.warning("Unknown access mode '%s' for User %d with Permission %s" % (access, user.id, cls.__name__))
raise AccessDenied("YOU SHALL NOT PASS!") raise AccessDenied("YOU SHALL NOT PASS!")
else: else:
@ -968,7 +972,7 @@ class RelatedForm(poobrains.form.Form):
endpoint = request.endpoint endpoint = request.endpoint
if not endpoint.endswith('_offset'): if not endpoint.endswith('_offset'):
endpoint = f'{endpoint}_offset' endpoint = '%s_offset' % (endpoint,)
f = super(RelatedForm, cls).__new__(cls, prefix=prefix, name=name, title=title, method=method, action=action) f = super(RelatedForm, cls).__new__(cls, prefix=prefix, name=name, title=title, method=method, action=action)
@ -1007,7 +1011,7 @@ class RelatedForm(poobrains.form.Form):
self.related_model = related_model self.related_model = related_model
self.related_field = related_field self.related_field = related_field
self.title = f"{self.related_model.__name__} for {self.instance.__class__.__name__} {self.instance.handle_string}" self.title = "%s for %s %s" % (self.related_model.__name__, self.instance.__class__.__name__, self.instance.handle_string)
def process(self, submit): def process(self, submit):
@ -1017,8 +1021,8 @@ class RelatedForm(poobrains.form.Form):
try: try:
fieldset.process(submit) fieldset.process(submit)
except Exception as e: except Exception as e:
flash(f"Failed to process fieldset '{fieldset.prefix}.{fieldset.name}'.") flash(u"Failed to process fieldset '%s.%s'." % (fieldset.prefix, fieldset.name))
app.logger.error(f"Failed to process fieldset {fieldset.prefix}.{fieldset.name} - {type(e).__name__}: {str(e)}") app.logger.error("Failed to process fieldset %s.%s - %s: %s" % (fieldset.prefix, fieldset.name, type(e).__name__, str(e)))
return redirect(request.url) return redirect(request.url)
return self return self
@ -1125,7 +1129,7 @@ class Protected(poobrains.rendering.Renderable, metaclass=PermissionInjection):
except AccessDenied: except AccessDenied:
if mode == 'inline': if mode == 'inline':
return Markup(poobrains.rendering.RenderString(f"Access Denied for {self.__class__.__name__}.")) return Markup(poobrains.rendering.RenderString("Access Denied for %s." % self.__class__.__name__))
raise raise
@ -1249,7 +1253,7 @@ class Administerable(poobrains.storage.Storable, Protected, metaclass=BaseAdmini
actions.append(self.url(mode), mode) actions.append(self.url(mode), mode)
except AccessDenied: except AccessDenied:
app.logger.debug(f"Not generating {mode} link for {self.__class__.__name__} {self.handle_string} because this user is not authorized for it.") app.logger.debug("Not generating %s link for %s %s because this user is not authorized for it." % (mode, self.__class__.__name__, self.handle_string))
except LookupError: except LookupError:
pass pass
#app.logger.debug("Couldn't create %s link for %s" % (mode, self.handle_string)) #app.logger.debug("Couldn't create %s link for %s" % (mode, self.handle_string))
@ -1287,9 +1291,9 @@ class Administerable(poobrains.storage.Storable, Protected, metaclass=BaseAdmini
def form(self, mode=None): def form(self, mode=None):
n = f'form_{mode}' n = 'form_%s' % mode
if not hasattr(self, n): if not hasattr(self, n):
raise NotImplementedError(f"Form class {self.__class__.__name__}.{n} missing.") raise NotImplementedError("Form class %s.%s missing." % (self.__class__.__name__, n))
form_class = getattr(self, n) form_class = getattr(self, n)
return form_class(mode=mode)#, name=None, title=None, method=None, action=None) return form_class(mode=mode)#, name=None, title=None, method=None, action=None)
@ -1345,7 +1349,7 @@ class Administerable(poobrains.storage.Storable, Protected, metaclass=BaseAdmini
def related_view(cls, related_field=None, handle=None, offset=0): def related_view(cls, related_field=None, handle=None, offset=0):
if related_field is None: if related_field is None:
raise TypeError(f"{cls.__name__}.related_view needs Field instance for parameter 'related_field'. Got {type(field).__name__} ({str(field)}) instead.") raise TypeError("%s.related_view needs Field instance for parameter 'related_field'. Got %s (%s) instead." % (cls.__name__, type(field).__name__, str(field)))
related_model = related_field.model related_model = related_field.model
instance = cls.load(handle) instance = cls.load(handle)
@ -1474,9 +1478,9 @@ class User(ProtectedNamed):
invalid_after = datetime.datetime.now() + datetime.timedelta(seconds=app.config['CERT_MAX_LIFETIME']) # FIXME: DRY! invalid_after = datetime.datetime.now() + datetime.timedelta(seconds=app.config['CERT_MAX_LIFETIME']) # FIXME: DRY!
if not_after > invalid_after: if not_after > invalid_after:
raise poobrains.errors.ExposedError(f"not_after too far into the future, max allowed {invalid_after} but got {not_after}") raise poobrains.errors.ExposedError("not_after too far into the future, max allowed %s but got %s" % (invalid_after, not_after))
common_name = f"{self.name}:{name}@{app.config['SITE_NAME']}" common_name = '%s:%s@%s' % (self.name, name, app.config['SITE_NAME'])
fd = open(app.config['CA_KEY'], 'rb') fd = open(app.config['CA_KEY'], 'rb')
ca_key = openssl.crypto.load_privatekey(openssl.crypto.FILETYPE_PEM, fd.read()) ca_key = openssl.crypto.load_privatekey(openssl.crypto.FILETYPE_PEM, fd.read())
@ -1552,8 +1556,8 @@ class User(ProtectedNamed):
NOTE: the passed model needs to have a datetime field with the name 'date'. This is needed for ordering. NOTE: the passed model needs to have a datetime field with the name 'date'. This is needed for ordering.
""" """
assert issubclass(model, Owned) and hasattr(model, 'date'), f"Only Owned subclasses with a 'date' field can be @User.profile'd. {model.__name__} does not qualify." assert issubclass(model, Owned) and hasattr(model, 'date'), "Only Owned subclasses with a 'date' field can be @User.profile'd. %s does not qualify." % model.__name__
assert hasattr(model, 'id') and isinstance(model.id, poobrains.storage.fields.AutoField), f"@on_profile model without id: {model.__name__}" assert hasattr(model, 'id') and isinstance(model.id, poobrains.storage.fields.AutoField), "@on_profile model without id: %s" % model.__name__
cls._on_profile.append(model) cls._on_profile.append(model)
@ -1584,7 +1588,7 @@ class User(ProtectedNamed):
for model in self.models_on_profile: for model in self.models_on_profile:
try: try:
queries.append(model.list('read', g.user, ordered=False, fields=[peewee.SQL(f"'{model.__name__}'").alias('model'), model.id, model.date.alias('date')]).where(model.owner == self)) queries.append(model.list('read', g.user, ordered=False, fields=[peewee.SQL("'%s'" % model.__name__).alias('model'), model.id, model.date.alias('date')]).where(model.owner == self))
except AccessDenied: except AccessDenied:
pass # ignore models we aren't allowed to read pass # ignore models we aren't allowed to read
@ -1649,7 +1653,7 @@ class UserPermission(Administerable):
return Permission.class_children_keyed()[self.permission] return Permission.class_children_keyed()[self.permission]
except KeyError: except KeyError:
app.logger.error(f"Unknown permission '{self.permission}' associated to user #{self.user_id}.") # can't use self.user.name because dat recursion app.logger.error("Unknown permission '%s' associated to user #%d." % (self.permission, self.user_id)) # can't use self.user.name because dat recursion
#TODO: Do we want to do more, like define a permission_class that always denies access? #TODO: Do we want to do more, like define a permission_class that always denies access?
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
@ -1659,7 +1663,7 @@ class UserPermission(Administerable):
valid_permission_names.append(cls.__name__) valid_permission_names.append(cls.__name__)
if self.permission not in valid_permission_names: if self.permission not in valid_permission_names:
raise ValueError(f"Invalid permission name: {self.permission}") raise ValueError("Invalid permission name: %s" % self.permission)
return super(UserPermission, self).save(*args, **kwargs) return super(UserPermission, self).save(*args, **kwargs)
@ -1728,7 +1732,7 @@ class GroupPermission(Administerable):
return Permission.class_children_keyed()[self.permission] return Permission.class_children_keyed()[self.permission]
except KeyError: except KeyError:
app.logger.error(f"Unknown permission '{self.permission}' associated to user #{self.group_id}.") # can't use self.group.name because dat recursion app.logger.error("Unknown permission '%s' associated to user #%d." % (self.permission, self.group_id)) # can't use self.group.name because dat recursion
#TODO: Do we want to do more, like define a permission_class that always denies access? #TODO: Do we want to do more, like define a permission_class that always denies access?
def form(self, mode=None): def form(self, mode=None):
@ -1753,7 +1757,7 @@ class GroupPermission(Administerable):
except Group.DoesNotExist: except Group.DoesNotExist:
name = 'FNORD' name = 'FNORD'
return f"{name}-{self.permission}" return "%s-%s" % (name, self.permission)
class ClientCertTokenAddForm(AutoForm): class ClientCertTokenAddForm(AutoForm):
@ -1762,7 +1766,7 @@ class ClientCertTokenAddForm(AutoForm):
r = super(ClientCertTokenAddForm, self).process(submit, exceptions=exceptions) r = super(ClientCertTokenAddForm, self).process(submit, exceptions=exceptions)
self.instance.user.notify(f"A new certificate token was created in your name! \n The token is *{self.instance.token}*. \nIt will expire on {self.instance.expiry_date}. \nThe certificate will be named '{self.instance.cert_name}'") # TODO: check if token was actually saved self.instance.user.notify("A new certificate token was created in your name! \n The token is *%s*. \nIt will expire on %s. \nThe certificate will be named '%s'" % (self.instance.token, self.instance.expiry_date, self.instance.cert_name)) # TODO: check if token was actually saved
return r return r
@ -1800,14 +1804,18 @@ class ClientCertToken(Administerable, Protected):
if user_token_count >= app.config['MAX_TOKENS']: if user_token_count >= app.config['MAX_TOKENS']:
raise ValueError( # TODO: Is ValueError really the most fitting exception there is? raise ValueError( # TODO: Is ValueError really the most fitting exception there is?
f"User {self.user.name} already has {user_token_count} out of {app.config['MAX_TOKENS']} client certificate tokens." "User %s already has %d out of %d client certificate tokens." % (
self.user.name,
user_token_count,
app.config['MAX_TOKENS']
)
) )
if self.__class__.select().where(self.__class__.user == self.user, self.__class__.cert_name == self.cert_name).count(): if self.__class__.select().where(self.__class__.user == self.user, self.__class__.cert_name == self.cert_name).count():
raise ValueError(f"User {self.user.name} already has a client certificate token for a certificate named '{self.cert_name}'.") raise ValueError("User %s already has a client certificate token for a certificate named '%s'." % (self.user.name, self.cert_name))
if ClientCert.select().where(ClientCert.user == self.user, ClientCert.name == self.cert_name).count(): if ClientCert.select().where(ClientCert.user == self.user, ClientCert.name == self.cert_name).count():
raise ValueError(f"User {self.user.name} already has a client certificate named '{self.cert_name}'.") raise ValueError("User %s already has a client certificate named '%s'." % (self.user.name, self.cert_name))
return super(ClientCertToken, self).save(force_insert=force_insert, only=only) return super(ClientCertToken, self).save(force_insert=force_insert, only=only)
@ -1836,7 +1844,7 @@ class ClientCert(Administerable):
if not self.id or force_insert: if not self.id or force_insert:
if self.__class__.select().where(self.__class__.user == self.user, self.__class__.name == self.name).count(): if self.__class__.select().where(self.__class__.user == self.user, self.__class__.name == self.name).count():
raise ValueError(f"User {self.user.name} already has a client certificate named '{self.name}'.") raise ValueError("User %s already has a client certificate named '%s'." % (self.user.name, self.name))
return super(ClientCert, self).save(force_insert=force_insert, only=only) return super(ClientCert, self).save(force_insert=force_insert, only=only)
@ -1956,7 +1964,7 @@ class Page(Owned):
elif op == 'read' and 'path' in kwargs: elif op == 'read' and 'path' in kwargs:
path = f"/{kwargs['path']}" path = '/%s' % kwargs['path']
instance = cls.get(cls.path == path) instance = cls.get(cls.path == path)
else: else:
@ -1991,7 +1999,7 @@ def bury_tokens():
count = q.execute() count = q.execute()
msg = f"Deleted {count} dead client certificate tokens." msg = "Deleted %d dead client certificate tokens." % count
click.secho(msg, fg='green') click.secho(msg, fg='green')
app.logger.info(msg) app.logger.info(msg)
@ -2031,7 +2039,7 @@ You have {other_cert_count} other valid certificates on this site.
) )
cert.user.notify(message) cert.user.notify(message)
click.echo(f"Notified user '{cert.user.name}' about expired certificate '{cert.name}'") click.echo("Notified user '%s' about expired certificate '%s'" % (cert.user.name, cert.name))
affected_users.add(cert.user) affected_users.add(cert.user)
count_expired += 1 count_expired += 1
@ -2063,7 +2071,7 @@ You have {other_cert_count} other valid certificates on this site.
) )
cert.user.notify(message) cert.user.notify(message)
click.echo(f"Notified user '{cert.user.name}' about impending expiry of certificate '{cert.name}'") click.echo("Notified user '%s' about impending expiry of certificate '%s'" % (cert.user.name, cert.name))
affected_users.add(cert.user) affected_users.add(cert.user)
count_impending_doom += 1 count_impending_doom += 1
@ -2071,4 +2079,4 @@ You have {other_cert_count} other valid certificates on this site.
cert.notification += 1 cert.notification += 1
cert.save() cert.save()
click.secho(f"Notified {len(affected_users)} users about {count_impending_doom} certificates that will soon expire and {count_expired} that have expired.", fg='green') click.secho("Notified %d users about %d certificates that will soon expire and %d that have expired." % (len(affected_users), count_impending_doom, count_expired), fg='green')

View File

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
import os import os
import datetime import datetime
import functools import functools
@ -29,7 +31,7 @@ def mkconfig(template, **values):
template_dir = os.path.join(app.poobrain_path, 'cli', 'templates') template_dir = os.path.join(app.poobrain_path, 'cli', 'templates')
jinja_env = jinja2.Environment(loader=jinja2.FileSystemLoader(template_dir)) jinja_env = jinja2.Environment(loader=jinja2.FileSystemLoader(template_dir))
template = jinja_env.get_template(f"{template}.jinja") template = jinja_env.get_template('%s.jinja' % template)
return template.render(**values) return template.render(**values)
@ -70,7 +72,7 @@ def test():
@option('--mail-user', prompt="Site email account username") @option('--mail-user', prompt="Site email account username")
@option('--mail-password', prompt="Site email password") @option('--mail-password', prompt="Site email password")
@option('--admin-mail-address', prompt="Admin email address") # FIXME: needs a regexp check @option('--admin-mail-address', prompt="Admin email address") # FIXME: needs a regexp check
@option('--admin-cert-name', prompt="Admin login certificate name", default=f"{project_name}-initial") # FIXME: needs a regexp check @option('--admin-cert-name', prompt="Admin login certificate name", default="%s-initial" % project_name) # FIXME: needs a regexp check
@option('--gnupg-homedir', prompt="gnupg homedir, relative to project root (corresponds to gpgs' --homedir)", default="gnupg") @option('--gnupg-homedir', prompt="gnupg homedir, relative to project root (corresponds to gpgs' --homedir)", default="gnupg")
@option('--gnupg-binary', default=None) @option('--gnupg-binary', default=None)
@option('--gnupg-passphrase', prompt="gnupg passphrase (used to create a keypair)", default=lambda: poobrains.helpers.random_string_light(64)) @option('--gnupg-passphrase', prompt="gnupg passphrase (used to create a keypair)", default=lambda: poobrains.helpers.random_string_light(64))
@ -118,7 +120,7 @@ def install(**options):
if 'grant' in choice_values: if 'grant' in choice_values:
access = 'grant' access = 'grant'
else: else:
echo(f"Don't know what access value to use for permission '{permission.__name__}', skipping.\n") echo("Don't know what access value to use for permission '%s', skipping.\n" % permission.__name__)
break break
gp = poobrains.auth.GroupPermission() gp = poobrains.auth.GroupPermission()
@ -169,7 +171,7 @@ def install(**options):
t.cert_name = options['admin_cert_name'] t.cert_name = options['admin_cert_name']
if t.save(): if t.save():
echo(f"Admin certificate token is: {click.style(t.token, fg='cyan', bold=True)}\n") echo("Admin certificate token is: %s\n" % click.style(t.token, fg="cyan", bold=True))
echo("We'll now configure GPG for sending encrypted mail.\n") echo("We'll now configure GPG for sending encrypted mail.\n")
@ -217,11 +219,11 @@ def install(**options):
type_dir = os.path.join(upload_dir, type_name) type_dir = os.path.join(upload_dir, type_name)
if os.path.exists(type_dir) and os.path.isfile(type_dir): if os.path.exists(type_dir) and os.path.isfile(type_dir):
echo(f"There's a file where the {type_name} upload directory should be, renaming it to {type_name}_") echo("There's a file where the %s upload directory should be, renaming it to %s_" % (type_name, type_name))
os.rename(type_dir, type_dir + '_') os.rename(type_dir, type_dir + '_')
if not os.path.exists(type_dir): if not os.path.exists(type_dir):
echo(f"creating {type_name} upload directory.") echo("creating %s upload directory." % type_name)
os.mkdir(type_dir, mode=0o770) os.mkdir(type_dir, mode=0o770)
@ -230,7 +232,8 @@ def install(**options):
else: else:
prefix = 'postgres://' prefix = 'postgres://'
options['database'] = f"{prefix}{poobrains.app.db.database}" options['database'] = '%s%s' % (prefix, poobrains.app.db.database)
config = mkconfig('config', **options) config = mkconfig('config', **options)
config_fd = open(os.path.join(app.root_path, 'config.py'), 'w') config_fd = open(os.path.join(app.root_path, 'config.py'), 'w')
@ -240,18 +243,18 @@ def install(**options):
if options['deployment'] == 'uwsgi+nginx': if options['deployment'] == 'uwsgi+nginx':
uwsgi_ini = mkconfig('uwsgi', **options) uwsgi_ini = mkconfig('uwsgi', **options)
uwsgi_ini_filename = f"{options['project_name']}.ini" uwsgi_ini_filename = '%s.ini' % options['project_name']
uwsgi_ini_fd = open(os.path.join(app.root_path, uwsgi_ini_filename), 'w') uwsgi_ini_fd = open(os.path.join(app.root_path, uwsgi_ini_filename), 'w')
uwsgi_ini_fd.write(uwsgi_ini) uwsgi_ini_fd.write(uwsgi_ini)
uwsgi_ini_fd.close() uwsgi_ini_fd.close()
echo(f"UWSGI .ini file was written to {click.style(uwsgi_ini_filename, fg='green')}") echo("UWSGI .ini file was written to %s" % click.style(uwsgi_ini_filename, fg='green'))
nginx_conf = mkconfig('nginx', **options) nginx_conf = mkconfig('nginx', **options)
nginx_conf_filename = f"{options['project_name']}.nginx.conf" nginx_conf_filename = '%s.nginx.conf' % options['project_name']
nginx_conf_fd = open(os.path.join(app.root_path, nginx_conf_filename), 'w') nginx_conf_fd = open(os.path.join(app.root_path, nginx_conf_filename), 'w')
nginx_conf_fd.write(nginx_conf) nginx_conf_fd.write(nginx_conf)
nginx_conf_fd.close() nginx_conf_fd.close()
echo(f"nginx config snippet was written to {click.style(nginx_conf_filename, fg='green')}") echo("nginx config snippet was written to %s" % click.style(nginx_conf_filename, fg='green'))
os.umask(old_umask) # restore old umask os.umask(old_umask) # restore old umask
echo("Installation complete!\n") echo("Installation complete!\n")
@ -295,14 +298,14 @@ def minica(lifetime):
tls_dir = os.path.join(app.root_path, 'tls') tls_dir = os.path.join(app.root_path, 'tls')
if os.path.exists(tls_dir): if os.path.exists(tls_dir):
secho(f"Directory/file '{tls_dir}' already exists. Move or delete it and re-run.", fg='red') secho("Directory/file '%s' already exists. Move or delete it and re-run." % tls_dir, fg='red')
raise click.Abort() raise click.Abort()
echo(f"Creating directory '{tls_dir}'") echo("Creating directory '%s'." % tls_dir)
os.mkdir(os.path.join(tls_dir)) os.mkdir(os.path.join(tls_dir))
echo("Creating certificate file") echo("Creating certificate file")
cert_pem = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert) cert_pem = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert)
fd = open(os.path.join(tls_dir, 'cert.pem'), 'w') fd = open(os.path.join(tls_dir, 'cert.pem'), 'w')
fd.write(cert_pem.decode('ascii')) fd.write(cert_pem.decode('ascii'))
@ -324,20 +327,25 @@ def add(storable):
instance = storable() instance = storable()
echo(f"Addding {storable.__name__}\n") echo("Addding %s...\n" % (storable.__name__,))
for field in storable._meta.sorted_fields: for field in storable._meta.sorted_fields:
if not isinstance(field, peewee.AutoField): if not isinstance(field, peewee.AutoField):
default = None default = None
if callable(field.default): if field.default:
default = field.default()
else:
default = field.default
if field.null and default is None: if callable(field.default):
default = '' # None interpreted as no default, '' treated as no value default = field.default()
else:
default = field.default
elif field.type == types.DATETIME:
default = datetime.datetime.now()
elif field.type == types.BOOL:
default = False
value = click.prompt(field.name, type=field.type, default=default) value = click.prompt(field.name, type=field.type, default=default)
@ -353,7 +361,8 @@ def add(storable):
def list(storable): def list(storable):
for instance in storable.select(): for instance in storable.select():
echo(f"{instance.handle_string}: {instance.title} - {instance}\n")
print("%s: %s - %s" % (instance.handle_string, instance.title, instance))
@app.cli.command() @app.cli.command()
@ -361,17 +370,17 @@ def list(storable):
@fake_before_request @fake_before_request
def delete(storable): def delete(storable):
instance = click.prompt(f"{storable.__name__} handle", type=types.StorableInstanceParamType(storable)) instance = click.prompt("%s handle" % storable.__name__, type=types.StorableInstanceParamType(storable))
echo(instance) echo(instance)
if confirm(f"Really delete this {storable.__name__}?"): if confirm("Really delete this %s?" % storable.__name__):
handle = instance.handle_string handle = instance.handle_string
if instance.delete_instance(recursive=True): if instance.delete_instance(recursive=True):
echo(f"Deleted {storable.__name__} {handle}") echo("Deleted %s %s" % (storable.__name__, handle))
else: else:
echo(f"Could not delete {storable.__name__} {handle}.") echo("Could not delete %s %s." % (storable.__name__, handle))
@app.cli.command() @app.cli.command()
@ -395,7 +404,7 @@ def import_(storable, filepath, skip_pk):
for field in fields: for field in fields:
if isinstance(field, poobrains.storage.fields.ForeignKeyField): if isinstance(field, poobrains.storage.fields.ForeignKeyField):
actual_name = f"{field.name}_id" actual_name = "%s_id" % field.name
else: else:
actual_name = field.name actual_name = field.name
@ -415,7 +424,7 @@ def import_(storable, filepath, skip_pk):
echo("Invalid import file, stopping.", err=True) echo("Invalid import file, stopping.", err=True)
return # stop. return # stop.
with click.progressbar(data, label=f"Importing as {storable.__name__}", item_show_func=lambda x: next(iter(x.values())) if x else '') as data_proxy: # next(iter(x.values)) gets first value in dict with click.progressbar(data, label="Importing as %s" % storable.__name__, item_show_func=lambda x: next(iter(x.values())) if x else '') as data_proxy: # next(iter(x.values)) gets first value in dict
for record in data_proxy: for record in data_proxy:
@ -434,7 +443,7 @@ def import_(storable, filepath, skip_pk):
elif isinstance(field, poobrains.storage.fields.ForeignKeyField): # FIXME: likely broken with ForeignKeys referencing tables with composite primary keys elif isinstance(field, poobrains.storage.fields.ForeignKeyField): # FIXME: likely broken with ForeignKeys referencing tables with composite primary keys
actual_name = f"{field.name}_id" actual_name = "%s_id" % field.name
if actual_name in record: if actual_name in record:
if record[actual_name] == '': if record[actual_name] == '':

View File

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
import os import os
import random import random
import functools import functools
@ -159,9 +161,9 @@ class CommentForm(poobrains.form.Form):
self.instance = instance self.instance = instance
self.fields['reply_to'].value = reply_to self.fields['reply_to'].value = reply_to
self.action = f"/comment/{self.instance.__class__.__name__}/{self.instance.handle_string}" # FIXME: This is shit. Maybe we want to teach Pooprint.get_view_url handling extra parameters from the URL? self.action = "/comment/%s/%s" % (self.instance.__class__.__name__, self.instance.handle_string) # FIXME: This is shit. Maybe we want to teach Pooprint.get_view_url handling extra parameters from the URL?
if reply_to: if reply_to:
self.action += f"/{reply_to.id}" self.action += "/%d" % reply_to.id
def process(self, submit): def process(self, submit):
@ -252,7 +254,7 @@ class Challenge(poobrains.storage.Named):
char_size = font.getsize(char) char_size = font.getsize(char)
char_wrapped = f' {char} ' char_wrapped = ' %s ' % char
char_wrapped_size = font.getsize(char_wrapped) char_wrapped_size = font.getsize(char_wrapped)
char_layer = Image.new('RGBA', char_wrapped_size, (0,0,0,0)) char_layer = Image.new('RGBA', char_wrapped_size, (0,0,0,0))
@ -316,11 +318,11 @@ class ChallengeForm(poobrains.form.Form):
except KeyError: except KeyError:
flask.flash(u"WORNG!1!!", 'error') flask.flash(u"WORNG!1!!", 'error')
app.logger.error(f"Challenge {self.challenge.name} refers to non-existant model!") app.logger.error("Challenge %s refers to non-existant model!" % self.challenge.name)
return flask.redirect('/') return flask.redirect('/')
except peewee.DoesNotExist: except peewee.DoesNotExist:
flask.flash("The thing you wanted to comment on does not exist anymore.") flask.flash(u"The thing you wanted to comment on does not exist anymore.")
return flask.redirect(cls.url('teaser')) return flask.redirect(cls.url('teaser'))
comment = Comment() comment = Comment()
@ -334,7 +336,7 @@ class ChallengeForm(poobrains.form.Form):
flask.flash(u"Your comment has been saved.") flask.flash(u"Your comment has been saved.")
if instance.notify_owner: if instance.notify_owner:
instance.owner.notify(f"New comment on [{self.challenge.model}/{self.challenge.handle}] by {self.challenge.author}.") instance.owner.notify("New comment on [%s/%s] by %s." % (self.challenge.model, self.challenge.handle, self.challenge.author))
self.challenge.delete_instance() # commit glorious seppuku self.challenge.delete_instance() # commit glorious seppuku
return flask.redirect(instance.url('full')) return flask.redirect(instance.url('full'))
@ -351,4 +353,4 @@ def bury_orphaned_challenges():
count = q.execute() count = q.execute()
app.logger.info(f"Deleted {count} orphaned comment challenges.") app.logger.info("Deleted %d orphaned comment challenges." % count)

View File

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
import datetime import datetime
from . import md_default from . import md_default
@ -24,10 +26,8 @@ SVG_PLOT_DESCRIPTION_HEIGHT = 80
SVG_MAP_PROJECTION = 'epsg:3857' # WGS84, as used by OSM etc. SVG_MAP_PROJECTION = 'epsg:3857' # WGS84, as used by OSM etc.
SMTP_HOST = None # str, ip address or dns name SMTP_HOST = None # str, ip address or dns name
#SMTP_PORT = 587 # int SMTP_PORT = 587 # int
#SMTP_STARTTLS = True SMTP_STARTTLS = True
SMTP_PORT = 465 # int
SMTP_STARTTLS = False
SMTP_ACCOUNT = None # str SMTP_ACCOUNT = None # str
SMTP_PASSWORD = None # str SMTP_PASSWORD = None # str
SMTP_FROM = None SMTP_FROM = None

File diff suppressed because it is too large Load Diff

View File

@ -5,12 +5,14 @@
## Kill the boilerplate ## ## Kill the boilerplate ##
```python ```python
#!/usr/bin/env python3 #!/usr/bin/env python2.7
# -*- coding: utf-8 -*-
import poobrains import poobrains
app = poobrains.app
if __name__ == '__main__': if __name__ == '__main__':
poobrains.app.cli() app.cli()
``` ```
This is the minimum viable poobrains site. This is the minimum viable poobrains site.
@ -18,8 +20,7 @@ It comes with a CLI and folds open to a complete CMS when deployed.
This site won't be very interesting but it comes batteries included This site won't be very interesting but it comes batteries included
with some basics amongst which you will find users, groups, uploads, with some basics amongst which you will find users, groups, uploads,
tags, comments and search as well as the more specialized facilities tags, comments and search.
for data analysis and visualization.
## poobrains is paranoid ## ## poobrains is paranoid ##
@ -28,11 +29,11 @@ Due to the inflationary nature of this sort of statement,
let's actually look at some central points of this. let's actually look at some central points of this.
### There is no JavaScript. ### ### There is no javascript. ###
No, really. No, really.
JavaScript might make the browser much more powerful, but sadly Javascript might make the browser much more powerful, but sadly
this comes at the price of making the browser much more powerful. this comes at the price of making the browser much more powerful.
It is remote code execution on the visitors computer *by design*. It is remote code execution on the visitors computer *by design*.
@ -41,24 +42,13 @@ While this can be used to build complex applications in the browser,
it can also be used for in-depth tracking and even serve as an infection it can also be used for in-depth tracking and even serve as an infection
vector for malware. vector for malware.
Tor users have repeatedly been de-anonymized by malicious JavaScript, Tor users have repeatedly been de-anonymized by malicious javascript,
for example when the FBI jacked Silk Road. # TODO: Sauce! for example when the FBI jacked Silk Road. # TODO: Sauce!
So, JS might be nice for developers (to a degree), but a site that So, JS might be nice for developers (to a degree), but a site that
aims to protect its users privacy should really abstain from using JS, aims to protect its users privacy should really abstain from using JS,
or at least from depending on it to work. or at least from depending on it to work.
For all these reasons and more, poobrains does not contain a single line
of JavaScript, webasm or other kind of client-side scripting this is
additionally enforced by [CSP] headers that also forbid *any* third-party
requests, nipping most, if not all\* [XSS] attacks in the bud, even if
visitors do have JavaScript enabled.
> \* XSS might still be technically possible through some eldritch
> abominations in CSS or obscure font renderer exploits, I *think*? 🤔
[CSP]: https://en.wikipedia.org/wiki/Content_Security_Policy
[XSS]: https://en.wikipedia.org/wiki/Cross-site_scripting
### There are no passwords ### ### There are no passwords ###
@ -66,58 +56,43 @@ Password re-use is probably the biggest security issue on the
internet today. Most people do it, it's a sad fact of life. internet today. Most people do it, it's a sad fact of life.
poobrains instead uses TLS client certificates for more granular poobrains instead uses TLS client certificates for more granular
control and better security, making use of public-key cryptography control and better security, thankfully handing authentication
on the web and thankfully handing authentication off to the httpd off to the httpd.
while also making it impossible to log in through an unencrypted
connection.
One downside to this is that you will need to maintain a [Certificate One downside to this is that you will need to maintain a Certificate
authority][CA] (CA) but unless you need to integrate it into an existing CA, Authority, but unless you need to integrate it into an existing CA,
poobrains will handle that for you. poobrains will do that for you.
[CA]: https://en.wikipedia.org/wiki/Certificate_authority > PS: I'm actually considering writing a tool to manage CAs comfortably,
> since everything that exists sucks. Maybe in 2018…?
### Restrictive permission system ### ### Restrictive permission system ###
Permissions are restrictive. I.e. **deny by default**. Permissions are restrictive. I.e. deny by default.
If seeing additive permission systems gives you the jitters, If seeing additive permission systems gives you the jitters,
you'll feel right at home. you'll feel right at home.
poobrains' permission system supports permissions assigned It also supports permissions assigned to users as well as groups.
to users as well as groups and to add icing on the cake
it's also extensible. To add icing on the cake, it's also extensible.
### All mails GPG-encrypted ### ### All mails GPG-encrypted ###
The mail system has been written such that it doesn't even know how The mail system has been written such that it doesn't even know how to
to send unencrypted mails. Be thankful for that, wrangling GPG is a send unencrypted mails. Be thankful for that, wrangling GPG is a horrible
horrible fucking experience. fucking experience.
### Even sessions are encrypted ###
Sessions are stored on the server side this is required to
accommodate the additional data needed for complex form-based
applications that are built and evaluated completely on the server.
But in order to give visitors more control over their data, they
are also symmetrically encrypted using a key that is only stored
on the client in a cookie.
This means poobrains can only read the contents of a session
when it's currently processing a request from the client holding
the key.
## Usability is still a big priority ## ## Usability is still a big priority ##
Yes, the aforementioned things impose some limits you don't usually Yes, the aforementioned things impose some limits you don't usually have.
have. This is not without reason tho and a great deal of work went This is not without reason tho and a great deal of work went (and will
(and will continue to go) into making poobrains usable. continue to go) into making poobrains usable.
The default theme makes extensive use of HTML5 semantics and is The default theme makes extensive use of HTML5 semantics and is fully
fully responsive down to about 320x480. responsive down to about 320x480.
But poobrains commitment to usability does not stop with the web user; But poobrains commitment to usability does not stop with the web user;
Developer eXperience is a central part of the development ethos, too. Developer eXperience is a central part of the development ethos, too.
@ -169,17 +144,20 @@ instances.
Now that we know a bit more, let's make a more sensible tiny site, shall we? Now that we know a bit more, let's make a more sensible tiny site, shall we?
```python ```python
#!/usr/bin/env python3 #!/usr/bin/env python2.7
# -*- coding: utf-8 -*-
import poobrains import poobrains
app = poobrains.app app = poobrains.app
@app.export('/post/') @app.export('/post/')
class Post(poobrains.auth.Administerable): class Post(poobrains.auth.Administerable):
title = poobrains.storage.fields.CharField() title = poobrains.storage.fields.CharField()
text = poobrains.storage.fields.MarkdownField() text = poobrains.storage.fields.MarkdownField()
if __name__ == '__main__': if __name__ == '__main__':
app.cli() app.cli()
``` ```
@ -189,7 +167,7 @@ new content type that can be administered on-site.
The `app.expose` class decorator is a neat little shortcut that puts The `app.expose` class decorator is a neat little shortcut that puts
a paginated list of `Post` at `/post/` with single instances/rows being a paginated list of `Post` at `/post/` with single instances/rows being
available under `/post/<postname>` (ex: `/post/foo` for `Post` "foo") available under `/post/\<postname\>` (ex: `/post/foo` for `Post` "foo")
## Actual deployment ## ## Actual deployment ##
@ -203,19 +181,13 @@ actually get this thing running?
While poobrains python dependencies are taken care of by pip, there are a few While poobrains python dependencies are taken care of by pip, there are a few
external dependencies you'll need: external dependencies you'll need:
* Python-3.6+ - python-2.7
* pip - pip for python-2.7
* GnuPG - gnupg-2.0
* Last tested with 2.2.27 beware of dragons might be - The 2.0 part is important, since 2.1 has the habit of adding
replaced with sequoia/p≡p in the future. new output codes that libraries don't handle *\*shakes fist at gpg\**
* [PROJ]
* Needed for map rendering.
* [NaCl]
* Needed for session encryption.
- an httpd that supports TLS client certificate authentication - an httpd that supports TLS client certificate authentication
- When in doubt, choose nginx - When in doubt, choose nginx
@ -226,11 +198,8 @@ external dependencies you'll need:
is for it to pass TLS client certificate authentication is for it to pass TLS client certificate authentication
status and the cert to the web application for every request. status and the cert to the web application for every request.
[PROJ]: https://proj.org/
[NaCl]: https://nacl.cr.yp.to/
### Directory structure ####
### Directory structure ###
Create a new directory for the project and put the file Create a new directory for the project and put the file
with the above code there, so you have a structure like: with the above code there, so you have a structure like:
@ -252,24 +221,20 @@ Change to the created directory:
`cd site` `cd site`
Create the virtualenv: Create the virtualenv:
`python3 -m venv --system-site-packages myvenv` `virtualenv --system-site-packages .`
`--system-site-packages` is not needed, but will enable your virtualenv `--system-site-packages` is not needed, but will enable your virtualenv to use
to use python libraries installed on your system. `myvenv` is the name python libraries installed on your system.
of the directory created for the venv. You can freely choose another
name, but we're sticking to `myvenv` in this guide.
Activate the virtualenv: Activate the virtualenv:
`source myvenv/bin/activate[.yourshell]` `source bin/activate[.yourshell]`
> The optional `.yourshell` is only needed if you use a non-bash
> shell like csh or fish.
If it worked, you'll see `(myvenv)` at the beginning of your terminal prompt. If it worked, you'll see `(site)` at the beginning of your terminal prompt.
### Installing poobrains ### ### Installing poobrains ###
`pip install 'git+https://rnd.phryk.net/phryk-evil-mad-sciences-llc/poobrains/'` `pip install 'git+https://github.com/phryk/poobrains'`
This will install poobrains and all needed python dependencies. This will install poobrains and all needed python dependencies.
@ -279,7 +244,7 @@ This will install poobrains and all needed python dependencies.
The installation procedure is called through your sites CLI. The installation procedure is called through your sites CLI.
To access it, make your codefile executable (`chmod +x site.py`). To access it, make your codefile executable (`chmod +x site.py`).
(I mean you could just call it with `python site.py` directly, (I mean you could just call it with the `python` interpreter directly,
but that's way less l33t.) but that's way less l33t.)
Now, kick off the installation procedure of your site by executing: Now, kick off the installation procedure of your site by executing:
@ -300,16 +265,15 @@ For the `deployment` input, this quickstart assumes the default `uwsgi+nginx`
value. You can use `custom` if you want to create the httpd config yourself, value. You can use `custom` if you want to create the httpd config yourself,
but obviously that's more work. but obviously that's more work.
In the case of `uwsgi+nginx`, you'll see files called `site.nginx.conf` In this case, you'll see files called `site.nginx.conf` as well as `site.ini`
as well as `site.ini` have appeared next to your `site.py` after the have appeared next to your site.py after the installation procedure went through.
installation procedure went through.
You can use `site.nginx.conf` verbatim as your nginx.conf if you don't You can use `site.nginx.conf` verbatim as your nginx.conf if you don't
want to run anything besides this site, otherwise, you'll just want to want to run anything besides this site, otherwise, you'll just want to
add the `server` directive in there and ignore the caching stuff in add the `server` directive in there and ignore the caching stuff in
the `http` block for now. the `http` block for now.
*Please note that until you went through "Creating a CA", nginx will not accept this config.* *Please note that unless you went through "Creating a CA", nginx will not accept this config.*
The `site.ini` can be placed in a directory uwsgi watches for ini files The `site.ini` can be placed in a directory uwsgi watches for ini files
(`[usr/local]/etc/uwsgi.d`, probably) or just be used directly by running (`[usr/local]/etc/uwsgi.d`, probably) or just be used directly by running
@ -336,9 +300,9 @@ Now, you'll have the CA in `site/tls/` and that's exactly where
`site.nginx.conf` expects it to be. `site.nginx.conf` expects it to be.
### File permissions ### ### Permissions ###
We will need to make sure that our files have the right permissions. We will need to make sure that some things have the right permissions.
Assuming the httpd will run as group `www`, do: Assuming the httpd will run as group `www`, do:
`chgrp -R www .` `chgrp -R www .`
@ -349,8 +313,8 @@ Assuming the httpd will run as group `www`, do:
### Starting up ### ### Starting up ###
Everything is in place. Start up nginx and uwsgi, using `service`, Everything is in place. Start up nginx and uwsgi, using `service`, `systemctl`
`systemctl` or whatever contraption your OS uses. or whatever contraption your OS uses.
## First visit and client certificate ## ## First visit and client certificate ##
@ -440,7 +404,7 @@ to the project.
Just getting the markdown of the `text` field rendered can be done Just getting the markdown of the `text` field rendered can be done
by creating this template file under `site/themes/default/post.jinja`: by creating this template file under `site/themes/default/post.jinja`:
```jinja ```
{% extends "administerable.jinja" %} {% extends "administerable.jinja" %}
{% block content %} {% block content %}
@ -494,7 +458,7 @@ enter the Admin Area, select "UserPermission" and then
Enter "anonymous" as user (the text field does auto-completion). Enter "anonymous" as user (the text field does auto-completion).
Open the `<select>` (the dropdown thingamabob in the form), Open the `<select>` (the dropdown thingamabob in the form),
scroll down to `Post_read` and select `Grant`. scroll down to `Post_create` and select `Grant`.
Finally, click the "Save" button. Finally, click the "Save" button.
@ -510,12 +474,10 @@ working toward the first alpha version.
Amongst the features we didn't even talk about here: Amongst the features we didn't even talk about here:
* instance-specific permissions - instance-specific permissions
* hierarchic tags - hierarchic tags
* threadable comments - threadable comments
* SCSS integration - SCSS integration
* cron jobs - templatable SVG
* templatable SVG - this includes dataset plots and simple maps
* honors themes by means of the SCSS integration - honors themes by means of the SCSS integration
* data analysis
* data visualization

View File

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
from click.exceptions import BadParameter from click.exceptions import BadParameter
class SessionError(Exception): class SessionError(Exception):
@ -32,7 +34,7 @@ class CompoundError(ValidationError):
@property @property
def message(self): def message(self):
msg = f"There were {len(self.errors)} errors." msg = "There were %d errors." % len(self.errors)
for error in self.errors: for error in self.errors:
msg += "\n"+str(error) msg += "\n"+str(error)

View File

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
# external imports # external imports
import time import time
import copy import copy
@ -337,16 +339,16 @@ class BaseForm(poobrains.rendering.Renderable, metaclass=FormMeta):
name = x.__name__.lower() name = x.__name__.lower()
if issubclass(x, BaseForm): if issubclass(x, BaseForm):
tpls.append(f'form/{name}.jinja') tpls.append('form/%s.jinja' % name)
if mode: if mode:
tpls.append(f'form/{name}-{mode}.jinja') tpls.append('form/%s-%s.jinja' % (name, mode))
else: else:
tpls.append(f'{name}.jinja') tpls.append('%s.jinja' % name)
if mode: if mode:
tpls.append(f'{name}-{mode}.jinja') tpls.append('%s-%s.jinja' % (name, mode))
return tpls return tpls
@ -408,7 +410,7 @@ class Form(BaseForm):
The easiest is just subclassing, like: The easiest is just subclassing, like:
``` python ```
@app.expose('/newperson') @app.expose('/newperson')
class MyPersonForm(poobrains.form.Form): class MyPersonForm(poobrains.form.Form):
name_prefix = poobrains.form.fields.Text(label='Prefix', help_text="Name prefix, ex: 'Dr.'") name_prefix = poobrains.form.fields.Text(label='Prefix', help_text="Name prefix, ex: 'Dr.'")
@ -422,7 +424,7 @@ class Form(BaseForm):
This will give you a `render`able form. In order for it to do anything, This will give you a `render`able form. In order for it to do anything,
you'll also have to implement the `process` function. you'll also have to implement the `process` function.
To expand on the example above, this would look like: To expand on the example above, this would look like:
``` python ```
def process(self, submit): def process(self, submit):
new_person = Person() # We'll just assume there's a `Storable` named `Person` already defined. new_person = Person() # We'll just assume there's a `Storable` named `Person` already defined.
@ -440,13 +442,13 @@ class Form(BaseForm):
Another way is dynamically building the form. Another way is dynamically building the form.
This can be done either from outside: This can be done either from outside:
``` python ```
form = poobrains.form.Form() form = poobrains.form.Form()
form.foo = poobrains.form.Button('submit', label='PUSH ME') form.foo = poobrains.form.Button('submit', label='PUSH ME')
``` ```
or from inside by subclassing again: or from inside by subclassing again:
``` python ```
class MyForm(poobrains.form.Form): class MyForm(poobrains.form.Form):
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
@ -526,7 +528,7 @@ class Form(BaseForm):
if not request.form['submit'].startswith(self.ref_id): # means the right form was submitted, should be implied by the path tho… if not request.form['submit'].startswith(self.ref_id): # means the right form was submitted, should be implied by the path tho…
app.logger.error(f"Form {self.name}: submit button of another form used: {request.form['submit']}") app.logger.error("Form %s: submit button of another form used: %s" % (self.name, request.form['submit']))
flask.flash("The form you just used might be broken. Bug someone if this problem persists.", 'error') flask.flash("The form you just used might be broken. Bug someone if this problem persists.", 'error')
return self return self
@ -565,7 +567,7 @@ class Fieldset(BaseForm):
def __setattr__(self, name, value): def __setattr__(self, name, value):
if name == 'name' and isinstance(value, str): if name == 'name' and isinstance(value, str):
assert not '.' in value, f"Form Field names *must* not contain dots: {value}" # FIXME: assert bad! raise exception assert not '.' in value, "Form Field names *must* not contain dots: %s" % value # FIXME: assert bad! raise exception
super(Fieldset, self).__setattr__(name, value) super(Fieldset, self).__setattr__(name, value)

View File

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
""" """
Form field definitions. Form field definitions.
@ -110,7 +112,7 @@ class BaseField(object, metaclass=poobrains.helpers.MetaCompatibility):
self.form = form self.form = form
if self.form.prefix: if self.form.prefix:
self.prefix = f"{self.form.prefix}.{self.form.name}" self.prefix = "%s.%s" % (self.form.prefix, self.form.name)
else: else:
self.prefix = self.form.name self.prefix = self.form.name
self.form.add_field(self, external=True) # NOTE: this has to be called *after* self.choices is filled self.form.add_field(self, external=True) # NOTE: this has to be called *after* self.choices is filled
@ -119,7 +121,7 @@ class BaseField(object, metaclass=poobrains.helpers.MetaCompatibility):
def __setattr__(self, name, value): def __setattr__(self, name, value):
if name == 'name' and isinstance(value, str): if name == 'name' and isinstance(value, str):
assert not '.' in value, f"Form Field names *must* not contain dots: {value}" assert not '.' in value, "Form Field names *must* not contain dots: %s" % value
super(BaseField, self).__setattr__(name, value) super(BaseField, self).__setattr__(name, value)
@ -134,17 +136,17 @@ class BaseField(object, metaclass=poobrains.helpers.MetaCompatibility):
if issubclass(x, Field): if issubclass(x, Field):
tpls.append(f'form/fields/{name}.jinja') tpls.append('form/fields/%s.jinja' % name)
if mode: if mode:
tpls.append(f'form/fields/{name}-{mode}.jinja') tpls.append('form/fields/%s-%s.jinja' % (name, mode))
else: else:
tpls.append(f'{name}.jinja') tpls.append('%s.jinja' % name)
if mode: if mode:
tpls.append(f'{name}-{mode}.jinja') tpls.append('%s-%s.jinja' % (name, mode))
return tpls return tpls
@ -198,7 +200,7 @@ class BaseField(object, metaclass=poobrains.helpers.MetaCompatibility):
""" HTML 'id' attribute for a datalist to be associated with this form element. """ """ HTML 'id' attribute for a datalist to be associated with this form element. """
if self.choices: if self.choices:
return f"list-{self.ref_id}" return "list-%s" % self.ref_id
return False # no choices means no datalist return False # no choices means no datalist
@ -243,7 +245,7 @@ class BaseField(object, metaclass=poobrains.helpers.MetaCompatibility):
for value in values: for value in values:
if self.choices and not value in self.valid_choices: if self.choices and not value in self.valid_choices:
compound_error.append(poobrains.errors.ValidationError(f"'{self.value}' is not an approved choice for {self.prefix}.{self.name}")) compound_error.append(poobrains.errors.ValidationError("'%s' is not an approved choice for %s.%s" % (self.value, self.prefix, self.name)))
else: # else because we don't want to clutter error output else: # else because we don't want to clutter error output
for validator in self.validators: for validator in self.validators:
@ -253,7 +255,7 @@ class BaseField(object, metaclass=poobrains.helpers.MetaCompatibility):
compound_error.append(e) compound_error.append(e)
elif self.required: elif self.required:
compound_error.append(poobrains.errors.ValidationError(f"Required field '{self.name}' was left empty.")) compound_error.append(poobrains.errors.ValidationError("Required field '%s' was left empty." % self.name))
if len(compound_error): if len(compound_error):
raise compound_error raise compound_error
@ -281,7 +283,7 @@ class BaseField(object, metaclass=poobrains.helpers.MetaCompatibility):
except poobrains.errors.BadParameter: except poobrains.errors.BadParameter:
e = poobrains.errors.ValidationError(f"Invalid input '{value}' for field {self.prefix}.{self.name}.") e = poobrains.errors.ValidationError("Invalid input '%s' for field %s.%s." % (value, self.prefix, self.name))
compound_error.append(e) compound_error.append(e)
self.errors.append(e) self.errors.append(e)
@ -292,7 +294,7 @@ class BaseField(object, metaclass=poobrains.helpers.MetaCompatibility):
except poobrains.errors.BadParameter: except poobrains.errors.BadParameter:
e = poobrains.errors.ValidationError(f"Invalid input '{value}' for field {self.prefix}.{self.name}.") e = poobrains.errors.ValidationError("Invalid input '%s' for field %s.%s." % (value, self.prefix, self.name))
compound_error.append(e) compound_error.append(e)
self.errors.append(e) self.errors.append(e)
@ -321,7 +323,7 @@ class BaseField(object, metaclass=poobrains.helpers.MetaCompatibility):
return 'true' if value == True else 'false' return 'true' if value == True else 'false'
elif isinstance(value, datetime.datetime): elif isinstance(value, datetime.datetime):
return value.strftime('%Y-%m-%dT%H:%M:%S') return value.strftime('%Y-%m-%d %H:%M:%S')
elif inspect.isclass(value): elif inspect.isclass(value):
return value.__name__ return value.__name__
@ -387,7 +389,7 @@ class Number(Field):
if not self.empty: if not self.empty:
if (self.min != None and self.value < self.min) or (self.max != None and self.value > self.max): if (self.min != None and self.value < self.min) or (self.max != None and self.value > self.max):
raise poobrains.errors.ValidationError(f"{self.name}: {self.value} is out of range. Must be in range from {self.min} to {self.max}.") raise poobrains.errors.ValidationError("%s: %d is out of range. Must be in range from %d to %d." % (self.name, self.value, self.min, self.max))
class Color(Field): class Color(Field):
@ -402,7 +404,7 @@ class Message(Field):
abstract = True abstract = True
def __repr__(self): def __repr__(self):
return f"<{self.__class__.__name__}: {self.value}>" return "<%s: %s>" % (self.__class__.__name__, self.value)
def validate(self): def validate(self):
pass pass
@ -545,8 +547,3 @@ class File(Field):
class Meta: class Meta:
abstract = True abstract = True
def bind(self, value):
# simple passthrough because this is filled with object(s) from
# request.files and shouldn't be converted to a string.
self.value = value

View File

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
""" """
Form/CLI types for coercion/validation Form/CLI types for coercion/validation
@ -28,12 +30,12 @@ class DateParamType(ParamType):
if "does not match format" in str(e): # TODO: find out what this means again and comment it if "does not match format" in str(e): # TODO: find out what this means again and comment it
app.logger.error(f"{self.__class__.__name__}.convert failed: {str(e)}") app.logger.error("%s.convert failed: %s" % (type(e).__name__, str(e)))
self.fail("We dun goof'd, this field isn't working.") self.fail("We dun goof'd, this field isn't working.")
else: else:
self.fail(f"'{value}' is not a valid date. Expected format: %Y-%m-%d") self.fail("'%s' is not a valid date. Expected format: %Y-%m-%d" % value)
DATE = DateParamType() DATE = DateParamType()
@ -49,29 +51,29 @@ class DateTimeParamType(ParamType):
return value # apparently we need this function to be idempotent. return value # apparently we need this function to be idempotent.
try: try:
return datetime.datetime.strptime(value, '%Y-%m-%dT%H:%M:%S') return datetime.datetime.strptime(value, '%Y-%m-%d %H:%M:%S')
except ValueError as e: except ValueError as e:
if "does not match format" in str(e): if "does not match format" in str(e):
app.logger.error(f"{self.__class__.__name__}.convert failed: {str(e)}") app.logger.error("%s.convert failed: %s" % (type(e).__name__, str(e)))
self.fail("We dun goof'd, this field isn't working.") self.fail("We dun goof'd, this field isn't working.")
else: else:
try: try:
return datetime.datetime.strptime(value, '%Y-%m-%dT%H:%M:%S.%f') return datetime.datetime.strptime(value, '%Y-%m-%d %H:%M:%S.%f')
except ValueError as e: except ValueError as e:
if "does not match format" in str(e): if "does not match format" in str(e):
app.logger.error(f"{self.__class__.__name__}.convert failed: {str(e)}") app.logger.error("%s.convert failed: %s" % (type(e).__name__, str(e)))
self.fail("We dun goof'd, this field isn't working.") self.fail("We dun goof'd, this field isn't working.")
else: else:
self.fail(f"'{value}' is not a valid datetime. Expected format '%Y-%m-%dT%H:%M:%S' or '%Y-%m-%dT%H:%M:%S.%f'") self.fail("'%s' is not a valid datetime. Expected format '%Y-%m-%d %H:%M:%S' or '%Y-%m-%d %H:%M:%S.%f'" % value)
DATETIME = DateTimeParamType() DATETIME = DateTimeParamType()

View File

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
""" """
Abstractions for complex, session-based form applications. Abstractions for complex, session-based form applications.
""" """

View File

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
import string import string
import random import random
import inspect import inspect
@ -263,7 +265,7 @@ def pretty_bytes(bytecount):
value /= 1024.0 value /= 1024.0
return f"{value:.2f} {unit}" return "%.2f %s" % (value, unit)
def levenshtein_distance(s1, s2): def levenshtein_distance(s1, s2):
@ -329,7 +331,6 @@ class FakeMetaOptions(object):
modes = None modes = None
permission_class = None permission_class = None
schema = None # needed by peewee.ModelBase.__new__ schema = None # needed by peewee.ModelBase.__new__
database = None # needed py peewee.ModelBase.__new__ at least from 3.15.0 on
_additional_keys = None # needed by peewee.ModelBase.__new__ _additional_keys = None # needed by peewee.ModelBase.__new__
def __init__(self): def __init__(self):
@ -436,6 +437,7 @@ class ChildAware(object, metaclass=MetaCompatibility):
""" """
Get the ancestors of this class, ordered by how far up the hierarchy they are. Get the ancestors of this class, ordered by how far up the hierarchy they are.
""" """
tiered = OrderedDict() tiered = OrderedDict()
@ -497,13 +499,13 @@ class CustomOrderedDict(dict):
def __repr__(self): def __repr__(self):
r = '{' repr = '{'
for k, v in self.items(): for k, v in self.items():
r += f"{repr(k)}: {repr(v)}" repr += '%s: %s' % (k.__repr__(), v.__repr__())
r += '}' repr += '}'
return r return repr
def __setitem__(self, key, value): def __setitem__(self, key, value):
@ -646,10 +648,13 @@ class ASVWriter(object):
def __init__(self, filepath): def __init__(self, filepath):
self.fd = codecs.open(filepath, 'a', encoding='utf-8') self.fd = codecs.open(filepath, 'a', encoding='utf-8')
def write_record(self, record): def write_record(self, record):
self.fd.write(f"{self.unit_separator.join(record)}{self.record_terminator}")
self.fd.write("%s%s" % (self.unit_separator.join(record), self.record_terminator))
def __del__(self): def __del__(self):
self.fd.close() self.fd.close()
@ -671,7 +676,13 @@ class TypedList(list):
def append(self, value): def append(self, value):
if not isinstance(value, self.type): if not isinstance(value, self.type):
raise TypeError(f"Wrong type, this TypedList only allows {self.type.__name__} but got {type(value).__name__} {repr(value)}.") raise TypeError(
"Wrong type, this TypedList only allows %s but got %s %s." % (
self.type.__name__,
type(value).__name__,
repr(value)
)
)
super(TypedList, self).append(value) super(TypedList, self).append(value)
@ -685,6 +696,12 @@ class TypedList(list):
def insert(self, index, value): def insert(self, index, value):
if not isinstance(value, self.type): if not isinstance(value, self.type):
raise TypeError(f"Wrong type, this TypedList only allows {self.type.__name__} but got {type(value).__name__} {repr(value)}.") raise TypeError(
"Wrong type, this TypedList only allows %s but got %s %s." % (
self.type.__name__,
type(value).__name__,
repr(value)
)
)
super(TypedList, self).insert(index, value) super(TypedList, self).insert(index, value)

View File

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText from email.mime.text import MIMEText
from email.mime.image import MIMEImage from email.mime.image import MIMEImage

View File

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
import collections import collections
import peewee import peewee
import jinja2 import jinja2
@ -116,7 +118,7 @@ class DisplayRenderable(markdown.inlinepatterns.Pattern):
instance.permissions['read'].check(flask.g.user) instance.permissions['read'].check(flask.g.user)
except: except:
return jinja2.Markup(md.convert(f"*You are not allowed to view an instance of {cls.__name__} that was placed here.*")) return jinja2.Markup(md.convert("*You are not allowed to view an instance of %s that was placed here.*" % cls.__name__))
if 'inline' in instance._meta.modes: if 'inline' in instance._meta.modes:
return instance.render('inline') return instance.render('inline')
elif 'teaser' in instance._meta.modes: elif 'teaser' in instance._meta.modes:
@ -128,10 +130,10 @@ class DisplayRenderable(markdown.inlinepatterns.Pattern):
except Exception as e: except Exception as e:
app.logger.debug(e) app.logger.debug(e)
return jinja2.Markup(f'<span class="markdown-error">{cls.__name__} could not be loaded.</span>') return jinja2.Markup(u'<span class="markdown-error">%s could not be loaded.</span>' % cls.__name__)
else: else:
return jinja2.Markup(f"<span class=\"markdown-error\">Don't know what {cls.__name__} is.</span>") return jinja2.Markup(u"<span class=\"markdown-error\">Don't know what %s is.</span>" % cls.__name__)
return super(DisplayRenderable, self).handleMatch(match) return super(DisplayRenderable, self).handleMatch(match)

View File

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
import markdown import markdown
class MagicDict(dict): class MagicDict(dict):
@ -66,7 +68,7 @@ class MagicDict(dict):
return (url, title) return (url, title)
except: except:
raise KeyError(f"Couldn't load '{storable}/{handle}'.") raise KeyError("Couldn't load '%s/%s'." % (storable, handle))
return super(MagicDict, self).__getitem__(key) return super(MagicDict, self).__getitem__(key)

View File

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
import collections import collections
import OpenSSL as openssl import OpenSSL as openssl
@ -30,7 +32,7 @@ class Dashbar(poobrains.rendering.Container):
try: try:
user.permissions['read'].check(g.user) user.permissions['read'].check(g.user)
menu.append(g.user.url('full'), f"{user.name}@{app.config['SITE_NAME']}") menu.append(g.user.url('full'), "%s@%s" % (user.name, app.config['SITE_NAME']))
except poobrains.auth.AccessDenied: except poobrains.auth.AccessDenied:
pass pass
@ -58,7 +60,7 @@ class Dashbar(poobrains.rendering.Container):
if notification_count == 1: if notification_count == 1:
menu.append(NotificationControl.url('full', handle=self.user.handle_string), '1 unread notification') menu.append(NotificationControl.url('full', handle=self.user.handle_string), '1 unread notification')
else: else:
menu.append(NotificationControl.url('full', handle=self.user.handle_string), f'{notification_count} unread notifications') # FIXME: pretty_si menu.append(NotificationControl.url('full', handle=self.user.handle_string), '%d unread notifications' % notification_count)
except poobrains.auth.AccessDenied: except poobrains.auth.AccessDenied:
pass pass
@ -176,7 +178,7 @@ class PGPForm(poobrains.form.Form):
super(PGPForm, self).__init__(**kwargs) super(PGPForm, self).__init__(**kwargs)
self.user = poobrains.auth.User.load(handle) self.user = poobrains.auth.User.load(handle)
self.current_key = poobrains.form.fields.Message(value=f"Your current key is: {self.user.pgp_fingerprint}") self.current_key = poobrains.form.fields.Message(value="Your current key is: %s" % self.user.pgp_fingerprint)
self.fields.order = ['current_key', 'pubkey'] self.fields.order = ['current_key', 'pubkey']
@ -198,8 +200,8 @@ class PGPForm(poobrains.form.Form):
else: else:
# Fun fact: I'm more proud of this error message than half my code. # Fun fact: I'm more proud of this error message than half my code.
flask.flash("Something went wrong when importing your new key. A pack of lazy raccoons has been dispatched to look at your plight in disinterested amusement.", 'error') flask.flash(u"Something went wrong when importing your new key. A pack of lazy raccoons has been dispatched to look at your plight in disinterested amusement.")
app.logger.error(f"GPG key import error: {result.stderr}") app.logger.error("GPG key import error: %s" % result.stderr)
return flask.redirect(flask.request.path) # reload page to show flash()es return flask.redirect(flask.request.path) # reload page to show flash()es
@ -272,12 +274,12 @@ class NotificationForm(poobrains.form.Form):
instance = poobrains.auth.Notification.load(handle) instance = poobrains.auth.Notification.load(handle)
if self.controls['mark_read'].value: if self.controls['mark_read'].value:
flask.flash(f"Marking notification {instance.id} as read.") flask.flash(u"Marking notification %d as read." % instance.id)
instance.read = True instance.read = True
instance.save() instance.save()
elif self.controls['delete'].value: elif self.controls['delete'].value:
flask.flash(f"Deleting notification {instance.id}.") flask.flash(u"Deleting notification %d." % instance.id)
instance.delete_instance() instance.delete_instance()
return self return self

View File

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
import collections import collections
import os import os
import flask import flask
@ -13,10 +15,6 @@ import poobrains.helpers
class Renderable(poobrains.helpers.ChildAware): class Renderable(poobrains.helpers.ChildAware):
"""
Base class for all renderable content.
"""
handle_string = None # string which uniquely identifies each instance of a Renderable handle_string = None # string which uniquely identifies each instance of a Renderable
name = None name = None
@ -79,9 +77,9 @@ class Renderable(poobrains.helpers.ChildAware):
name = x.__name__.lower() name = x.__name__.lower()
if mode: if mode:
tpls.append(f'{name}-{mode}.jinja') tpls.append('%s-%s.jinja' % (name, mode))
tpls.append(f'{name}.jinja') tpls.append('%s.jinja' % name)
return tpls return tpls
@ -306,7 +304,7 @@ class TableRow(object):
if key.lower() in columns_lower: if key.lower() in columns_lower:
return columns_lower.index(key.lower()) return columns_lower.index(key.lower())
raise KeyError(f"Column {key} is unknown!") raise KeyError("Column %s is unknown!" % key)
def append(self, value): def append(self, value):

View File

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
import functools import functools
import collections import collections
import math import math
@ -104,9 +106,9 @@ class Search(poobrains.auth.Protected):
clauses = [] clauses = []
if isinstance(app.db, peewee.SqliteDatabase): if isinstance(app.db, peewee.SqliteDatabase):
term = f"*{self.handle_string.lower()}*" term = '*%s*' % self.handle_string.lower()
else: # postgres else: # postgres
term = f"%{self.handle_string.lower()}%" term = '%%%s%%' % self.handle_string.lower()
if hasattr(administerable._meta, 'search_fields'): if hasattr(administerable._meta, 'search_fields'):

View File

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
# external imports # external imports
import math import math
import datetime import datetime
@ -34,11 +36,11 @@ def RegexpConstraint(field_name, regexp):
operation = app.db._operations['REGEXP'] # peewee.OP.REGEXP used to always hold the correct value, what happen? operation = app.db._operations['REGEXP'] # peewee.OP.REGEXP used to always hold the correct value, what happen?
if 'sqlite' in app.db.__class__.__name__.lower(): if 'sqlite' in app.db.__class__.__name__.lower():
regexp_compat = f'"{regexp}"' regexp_compat = '"%s"' % regexp
else: else:
regexp_compat = f"'{regexp}'" regexp_compat = "'%s'" % regexp
return peewee.Check(f'"{field_name}" {operation} {regexp_compat}') return peewee.Check('"%s" %s %s' % (field_name, operation, regexp_compat))
class OrderableMetadata(peewee.Metadata): class OrderableMetadata(peewee.Metadata):
@ -116,7 +118,7 @@ class Model(peewee.Model, poobrains.helpers.ChildAware, metaclass=ModelBase):
elif type(handle) not in (tuple, list): elif type(handle) not in (tuple, list):
handle = [handle] handle = [handle]
assert len(handle) == len(cls._meta.handle_fields), f"Handle length mismatch for {cls.__name__}, expected {len(cls._meta.handle_fields)} but got {len(handle)}!" assert len(handle) == len(cls._meta.handle_fields), "Handle length mismatch for %s, expected %d but got %d!" % (cls.__name__, len(cls._meta.handle_fields), len(handle))
for field_name in cls._meta.handle_fields: for field_name in cls._meta.handle_fields:
field = getattr(cls, field_name) field = getattr(cls, field_name)
@ -175,9 +177,9 @@ class Model(peewee.Model, poobrains.helpers.ChildAware, metaclass=ModelBase):
def __repr__(self): def __repr__(self):
try: try:
return f"<{self.__class__.__name__}[{self._pk}]>" return "<%s[%s]>" % (self.__class__.__name__, self._pk)
except Exception: except Exception:
return f"<{self.__class__.__name__}, unsaved/no primary key>" return "<%s, unsaved/no primary key>" % self.__class__.__name__
class SessionData(Model): class SessionData(Model):
@ -223,12 +225,12 @@ class Storable(Model, poobrains.rendering.Renderable):
return self.name return self.name
elif self._pk: elif self._pk:
return f"{self.__class__.__name__} {str(self._pk)}" return "%s %s" % (self.__class__.__name__, str(self._pk))
return f"New {self.__class__.__name__}" return "New %s" % self.__class__.__name__
except Exception as e: except Exception as e:
app.logger.debug(f'Failed building title for {self.__class__.__name__}: {str(e)}') app.logger.debug('Failed building title for %s: %s' % (self.__class__.__name__, str(e)))
return '' return ''
def instance_url(self, mode='full', quiet=False, **url_params): def instance_url(self, mode='full', quiet=False, **url_params):
@ -298,7 +300,7 @@ class Named(Storable):
@property @property
def ref_id(self): def ref_id(self):
return f"{self.__class__.__name__.lower()}-{self.name}" return "%s-%s" % (self.__class__.__name__.lower(), self.name)
class Listing(poobrains.rendering.Renderable): class Listing(poobrains.rendering.Renderable):
@ -345,7 +347,7 @@ class Listing(poobrains.rendering.Renderable):
endpoint = flask.request.endpoint endpoint = flask.request.endpoint
if not endpoint.endswith('_offset'): if not endpoint.endswith('_offset'):
endpoint = f'{endpoint}_offset' endpoint = '%s_offset' % (endpoint,)
self.pagination = Pagination([query], offset, endpoint, **pagination_options) self.pagination = Pagination([query], offset, endpoint, **pagination_options)
self.items = self.pagination.results self.items = self.pagination.results
@ -360,10 +362,10 @@ class Listing(poobrains.rendering.Renderable):
if mode: if mode:
if issubclass(x, Listing): if issubclass(x, Listing):
tpls.append(f'{name}-{mode}-{self.cls.__name__}.jinja') tpls.append('%s-%s-%s.jinja' % (name, mode, self.cls.__name__))
tpls.append(f'{name}-{mode}.jinja') tpls.append('%s-%s.jinja' % (name, mode))
tpls.append(f'{name}.jinja') tpls.append('%s.jinja' % name)
return tpls return tpls
@ -478,7 +480,7 @@ class StorableParamType(poobrains.form.types.ParamType):
if value.lower() in storables: if value.lower() in storables:
return storables[value.lower()] return storables[value.lower()]
self.fail(f'Not a valid storable: {value}. Try one of {storables.keys()}') self.fail(u'Not a valid storable: %s. Try one of %s' % (value, storables.keys()))
poobrains.form.types.StorableParamType = StorableParamType poobrains.form.types.StorableParamType = StorableParamType
poobrains.form.types.STORABLE = StorableParamType() poobrains.form.types.STORABLE = StorableParamType()
@ -487,4 +489,4 @@ poobrains.form.types.STORABLE = StorableParamType()
@app.cron @app.cron
def bury_sessions(): def bury_sessions():
count = SessionData.delete().where(SessionData.expires <= datetime.datetime.utcnow()).execute() count = SessionData.delete().where(SessionData.expires <= datetime.datetime.utcnow()).execute()
click.secho(f"Deleted {count} dead sessions.", fg='green') click.secho("Deleted %d dead sessions." % count, fg='green')

View File

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
# external imports # external imports
import typing import typing
import json import json
@ -65,10 +67,10 @@ class StorableInstanceParamType(poobrains.form.types.ParamType):
except self.storable.DoesNotExist: except self.storable.DoesNotExist:
self.fail(f"No such {self.storable.__name__}: {value}.") self.fail("No such %s: %s." % (self.storable.__name__, value))
if self.choices is not None and instance not in self.choices: if self.choices is not None and instance not in self.choices:
self.fail(f"'{value}' is not an approved choice.") self.fail("'%s' is not an approved choice." % value)
return instance return instance

View File

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
import os import os
import collections import collections
import scss import scss
@ -14,7 +16,7 @@ class SVG(poobrains.auth.Protected):
handle = None # needed so that app.expose registers a route with extra param, this is kinda hacky… handle = None # needed so that app.expose registers a route with extra param, this is kinda hacky…
_css_cache = None _css_cache = None
class Meta: class Meta:
modes = collections.OrderedDict([ modes = collections.OrderedDict([
@ -23,7 +25,7 @@ class SVG(poobrains.auth.Protected):
('raw', 'read'), ('raw', 'read'),
('inline', 'read') ('inline', 'read')
]) ])
style = None style = None
def __init__(self, handle=None, mode=None, **kwargs): def __init__(self, handle=None, mode=None, **kwargs):
@ -36,16 +38,16 @@ class SVG(poobrains.auth.Protected):
else: else:
self.style = Markup(app.scss_compiler.compile_string("@import 'svg';")) self.style = Markup(app.scss_compiler.compile_string("@import 'svg';"))
self.__class__._css_cache = self.style self.__class__._css_cache = self.style
def templates(self, mode=None): def templates(self, mode=None):
r = super(SVG, self).templates(mode=mode) r = super(SVG, self).templates(mode=mode)
return [f"svg/{template}" for template in r] return ["svg/%s" % template for template in r]
def instance_url(self, mode='full', quiet=False, **url_params): def instance_url(self, mode='full', quiet=False, **url_params):
url_params['handle'] = self.handle url_params['handle'] = self.handle
return super(SVG, self).instance_url(mode=mode, quiet=quiet, **url_params) return super(SVG, self).instance_url(mode=mode, quiet=quiet, **url_params)
@ -55,23 +57,23 @@ class SVG(poobrains.auth.Protected):
def view(self, mode=None, handle=None): def view(self, mode=None, handle=None):
if mode == 'raw': if mode == 'raw':
response = Response(self.render('raw')) response = Response(self.render('raw'))
response.headers['Content-Type'] = 'image/svg+xml' response.headers['Content-Type'] = u'image/svg+xml'
response.headers['Content-Disposition'] = f'filename="{self.__class__.__name__}.svg"' response.headers['Content-Disposition'] = u'filename="%s.svg"' % self.__class__.__name__
# Disable "public" mode caching downstream (nginx, varnish) in order to hopefully not leak restricted content # Disable "public" mode caching downstream (nginx, varnish) in order to hopefully not leak restricted content
response.cache_control.public = False response.cache_control.public = False
response.cache_control.private = True response.cache_control.private = True
response.cache_control.max_age = app.config['CACHE_LONG'] response.cache_control.max_age = app.config['CACHE_LONG']
return response return response
else: else:
return poobrains.helpers.ThemedPassthrough(super(SVG, self).view(mode=mode, handle=handle)) return poobrains.helpers.ThemedPassthrough(super(SVG, self).view(mode=mode, handle=handle))
@app.setup @app.before_first_request
def register_svg_raw(): def register_svg_raw():
for cls in set(SVG.class_children()): for cls in set(SVG.class_children()):
rule = os.path.join("/svg/", cls.__name__.lower(), '<handle>', 'raw') rule = os.path.join("/svg/", cls.__name__.lower(), '<handle>', 'raw')

View File

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
import string import string
import statistics # for mean import statistics # for mean
import colorsys import colorsys
@ -97,7 +99,17 @@ class Color(object):
self._update_lightness() self._update_lightness()
def __repr__(self): def __repr__(self):
return f"<{self.__class__.__name__}: red {self.red}, green {self.green}, blue {self.blue}, hue {self.hue}, saturation {self.saturation}, value {self.value}, alpha {self.alpha}>"
return '<%s: red %f, green %f, blue %f, hue %f, saturation %f, value %f, alpha %f>' % (
self.__class__.__name__,
self.red,
self.green,
self.blue,
self.hue,
self.saturation,
self.value,
self.alpha
)
def __str__(self): def __str__(self):
return f"rgba({int(self.red * 255)}, {int(self.green * 255)}, {int(self.blue * 255)}, {self.alpha})" return f"rgba({int(self.red * 255)}, {int(self.green * 255)}, {int(self.blue * 255)}, {self.alpha})"

View File

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
import scss import scss
import poobrains.helpers import poobrains.helpers

View File

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
""" The tagging system. """ """ The tagging system. """
import collections import collections
@ -57,7 +59,7 @@ class Tag(poobrains.auth.ProtectedNamed):
if current_depth > 100: if current_depth > 100:
if root: if root:
message = f"Possibly incestuous tag: '{root.name}'." message = "Possibly incestuous tag: '%s'." % root.name
else: else:
message = "Possibly incestuous tag, but don't have a root for this tree. Are you fucking with current_depth manually?" message = "Possibly incestuous tag, but don't have a root for this tree. Are you fucking with current_depth manually?"
@ -114,7 +116,7 @@ class Tag(poobrains.auth.ProtectedNamed):
try: try:
model = poobrains.storage.Storable.class_children_keyed()[model_name] model = poobrains.storage.Storable.class_children_keyed()[model_name]
except KeyError: except KeyError:
app.logger.error(f"TagBinding for unknown model: {model_name}") app.logger.error("TagBinding for unknown model: %s" % model_name)
continue continue
handles = [model.string_handle(binding.handle) for binding in bindings] handles = [model.string_handle(binding.handle) for binding in bindings]
@ -148,7 +150,7 @@ class Tag(poobrains.auth.ProtectedNamed):
def validate(self): def validate(self):
if self.looping(): if self.looping():
raise poobrains.errors.ValidationError(f"'{self.parent.title}' is a descendant of this tag and thus can't be used as parent!", field='parent') raise poobrains.errors.ValidationError("'%s' is a descendant of this tag and thus can't be used as parent!" % self.parent.title, field='parent')
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
@ -187,7 +189,7 @@ class TaggingField(poobrains.form.fields.Select):
choices.append((tag, tag.name)) choices.append((tag, tag.name))
except (peewee.OperationalError, peewee.ProgrammingError) as e: except (peewee.OperationalError, peewee.ProgrammingError) as e:
app.logger.error(f"Failed building list of tags for TaggingField: {str(e)}") app.logger.error("Failed building list of tags for TaggingField: %s" % str(e))
kwargs['choices'] = choices kwargs['choices'] = choices
kwargs['type'] = poobrains.form.types.StorableInstanceParamType(Tag) kwargs['type'] = poobrains.form.types.StorableInstanceParamType(Tag)

View File

@ -85,10 +85,10 @@ def fill_valid(instance):
if isinstance(instance, poobrains.storage.Named) and attr_name == 'name': if isinstance(instance, poobrains.storage.Named) and attr_name == 'name':
setattr(instance, attr_name, generators[fieldmap[field_class]]().lower()) setattr(instance, attr_name, generators[fieldmap[field_class]]().lower())
else: else:
raise AssertionError(f"Can't guarantee valid fill for class '{instance.__class__.__name__}' because of constraints on field '{attr_name}'!") raise AssertionError("Can't guarantee valid fill for class '%s' because of constraints on field '%s'!" % (instance.__class__.__name__, attr_name))
elif not cls_attr.__class__ in fieldmap: elif not cls_attr.__class__ in fieldmap:
raise AssertionError(f"Can't generate fill for {instance.__class__.__name__}.{attr_name} of type {field_class.__name__}") raise AssertionError("Can't generate fill for %s.%s of type %s" % (instance.__class__.__name__, attr_name, field_class.__name__))
else: else:
setattr(instance, attr_name, generators[fieldmap[field_class]]()) setattr(instance, attr_name, generators[fieldmap[field_class]]())
@ -207,7 +207,7 @@ y
if name.isupper(): if name.isupper():
poobrains.app.config[name] = getattr(config, name) poobrains.app.config[name] = getattr(config, name)
client.get('/') # first request that triggers app.run_setup to finish booting poobrains client.get('/') # first request that triggers before_first_request to finish booting poobrains
def test_cli_minica(client): def test_cli_minica(client):
@ -220,7 +220,7 @@ def test_cli_minica(client):
def test_cert_page(client): def test_cert_page(client):
rv = client.get('/cert/') rv = client.get('/cert/')
assert rv.status_code == 200, f"Expected status code 200 at /cert/, got {rv.status_code}" assert rv.status_code == 200, "Expected status code 200 at /cert/, got %d" % rv.status_code
def test_redeem_token(client): def test_redeem_token(client):
@ -239,7 +239,7 @@ def test_redeem_token(client):
try: try:
OpenSSL.crypto.load_pkcs12(rv.data, passphrase) OpenSSL.crypto.load_pkcs12(rv.data, passphrase)
except Exception: except Exception:
raise AssertionError(f"Couldn't load PKCS12 with passphrase '{passphrase}'") raise AssertionError("Couldn't load PKCS12 with passphrase '%s'" % passphrase)
# TODO: CRUD tests for ALL non-abstract Storables # TODO: CRUD tests for ALL non-abstract Storables
@pytest.mark.parametrize('cls', storables_to_test) @pytest.mark.parametrize('cls', storables_to_test)
@ -256,19 +256,19 @@ def test_crud(client, cls):
fill_valid(instance) fill_valid(instance)
assert instance.save(force_insert=True) > 0, f"Create failed for class '{cls.__name__}'!" assert instance.save(force_insert=True) > 0, "Create failed for class '%s'!" % cls.__name__
try: try:
instance = cls.load(instance.handle_string) # reloads instance from database, making sure Read works instance = cls.load(instance.handle_string) # reloads instance from database, making sure Read works
except cls.DoesNotExist: except cls.DoesNotExist:
raise AssertionError(f"Read failed for class '{cls.__name__}'!") raise AssertionError("Read failed for class '%s'!" % cls.__name__)
# make owner anon to test whether updating works properly # make owner anon to test whether updating works properly
fill_valid(instance) # put some new values into the instance fill_valid(instance) # put some new values into the instance
assert instance.save() > 0, f"Update failed for class '{cls.__name__}'!" assert instance.save() > 0, "Update failed for class '%s'!" % cls.__name__
assert instance.delete_instance() > 0, f"Delete failed for class '{cls.__name__}'!" assert instance.delete_instance() > 0, "Delete failed for class '%s'!" % cls.__name__
# TODO: use the Page permission tests as basis for auto-generated permission # TODO: use the Page permission tests as basis for auto-generated permission
@ -288,7 +288,7 @@ def test_permission_grant(client, cls, permission_holder, op_info):
pytest.skip() # this op has been explicitly disabled and isn't exposed (for which there should also be a test) pytest.skip() # this op has been explicitly disabled and isn't exposed (for which there should also be a test)
u = poobrains.auth.User() u = poobrains.auth.User()
u.name = f'test-{cls.__name__.lower()}-{permission_holder}-{op}-grant' u.name = 'test-%s-%s-%s-grant' % (cls.__name__.lower(), permission_holder, op)
u.save(force_insert=True) u.save(force_insert=True)
instance = cls() instance = cls()
@ -306,7 +306,7 @@ def test_permission_grant(client, cls, permission_holder, op_info):
else: # group else: # group
g = poobrains.auth.Group() g = poobrains.auth.Group()
g.name = f'{cls.__name__.lower()}-{op}-group-grant' g.name = '%s-%s-group-grant' % (cls.__name__.lower(), op)
g.save(force_insert=True) g.save(force_insert=True)
ug = poobrains.auth.UserGroup() ug = poobrains.auth.UserGroup()
@ -333,7 +333,7 @@ def test_permission_grant(client, cls, permission_holder, op_info):
try: try:
instance.permissions[op].check(u) instance.permissions[op].check(u)
except poobrains.auth.AccessDenied: except poobrains.auth.AccessDenied:
raise AssertionError(f"{permission_holder}-assigned Permission check on {cls.__name__} for '{op}' does not allow access!") raise AssertionError("%s-assigned Permission check on %s for '%s' does not allow access!" % (permission_holder, cls.__name__, op))
@pytest.mark.parametrize('op_info', ops, ids=lambda x: x[0]) @pytest.mark.parametrize('op_info', ops, ids=lambda x: x[0])
@ -348,7 +348,7 @@ def test_permission_deny(client, cls, permission_holder, op_info):
pytest.skip() # this op has been explicitly disabled and isn't exposed (for which there should also be a test) pytest.skip() # this op has been explicitly disabled and isn't exposed (for which there should also be a test)
u = poobrains.auth.User() u = poobrains.auth.User()
u.name = f'test-{cls.__name__.lower()}-{permission_holder}-{op}-deny' u.name = 'test-%s-%s-%s-deny' % (cls.__name__.lower(), permission_holder, op)
u.save(force_insert=True) u.save(force_insert=True)
instance = cls() instance = cls()
@ -366,7 +366,7 @@ def test_permission_deny(client, cls, permission_holder, op_info):
else: # group else: # group
g = poobrains.auth.Group() g = poobrains.auth.Group()
g.name = f'{cls.__name__.lower()}-{op}-group-deny' g.name = '%s-%s-group-deny' % (cls.__name__.lower(), op)
g.save(force_insert=True) g.save(force_insert=True)
ug = poobrains.auth.UserGroup() ug = poobrains.auth.UserGroup()
@ -406,7 +406,7 @@ def test_ownedpermission_instance(client, cls, permission_holder, op_info):
pytest.skip() # this op has been explicitly disabled and isn't exposed (for which there should also be a test) pytest.skip() # this op has been explicitly disabled and isn't exposed (for which there should also be a test)
u = poobrains.auth.User() u = poobrains.auth.User()
u.name = f'test-{cls.__name__.lower()}-{permission_holder}-{op}-instance' u.name = 'test-%s-%s-%s-instance' % (cls.__name__.lower(), permission_holder, op)
u.save(force_insert=True) u.save(force_insert=True)
instance = cls() instance = cls()
@ -427,7 +427,7 @@ def test_ownedpermission_instance(client, cls, permission_holder, op_info):
else: # group else: # group
g = poobrains.auth.Group() g = poobrains.auth.Group()
g.name = f'{cls.__name__.lower()}-{op}-group-instance' g.name = '%s-%s-group-instance' % (cls.__name__.lower(), op)
g.save(force_insert=True) g.save(force_insert=True)
ug = poobrains.auth.UserGroup() ug = poobrains.auth.UserGroup()
@ -464,7 +464,7 @@ def test_ownedpermission_instance(client, cls, permission_holder, op_info):
try: try:
instance.permissions[op].check(u) instance.permissions[op].check(u)
except poobrains.auth.AccessDenied: except poobrains.auth.AccessDenied:
raise AssertionError(f"{permission_holder}-assigned OwnedPermission check on {cls.__name__} for '{op}' with instance access '{op_abbr}' does not allow access!") raise AssertionError("%s-assigned OwnedPermission check on %s for '%s' with instance access '%s' does not allow access!" % (permission_holder, cls.__name__, op, op_abbr))
@pytest.mark.parametrize('op_info', ops, ids=lambda x: x[0]) @pytest.mark.parametrize('op_info', ops, ids=lambda x: x[0])
@ -479,7 +479,7 @@ def test_ownedpermission_own_instance(client, cls, permission_holder, op_info):
pytest.skip() # this op has been explicitly disabled and isn't exposed (for which there should also be a test) pytest.skip() # this op has been explicitly disabled and isn't exposed (for which there should also be a test)
u = poobrains.auth.User() u = poobrains.auth.User()
u.name = f'test-{cls.__name__.lower()}-{permission_holder}-{op}-own-instance' u.name = 'test-%s-%s-%s-own-instance' % (cls.__name__.lower(), permission_holder, op)
u.save(force_insert=True) u.save(force_insert=True)
u = poobrains.auth.User.load(u.name) # reload user to update own_permissions u = poobrains.auth.User.load(u.name) # reload user to update own_permissions
poobrains.g.user = u # chep login fake because Owned uses g.user as default owner poobrains.g.user = u # chep login fake because Owned uses g.user as default owner
@ -502,7 +502,7 @@ def test_ownedpermission_own_instance(client, cls, permission_holder, op_info):
else: # group else: # group
g = poobrains.auth.Group() g = poobrains.auth.Group()
g.name = f'{cls.__name__.lower()}-{op}-group-own-instance' g.name = '%s-%s-group-own-instance' % (cls.__name__.lower(), op)
g.save(force_insert=True) g.save(force_insert=True)
ug = poobrains.auth.UserGroup() ug = poobrains.auth.UserGroup()
@ -539,7 +539,7 @@ def test_ownedpermission_own_instance(client, cls, permission_holder, op_info):
try: try:
instance.permissions[op].check(u) instance.permissions[op].check(u)
except poobrains.auth.AccessDenied: except poobrains.auth.AccessDenied:
raise AssertionError(f"{permission_holder}-assigned OwnedPermission check on {cls.__name__} for '{op}' with own_instance access '{op_abbr}' does not allow access!") raise AssertionError("%s-assigned OwnedPermission check on %s for '%s' with own_instance access '%s' does not allow access!" % (permission_holder, cls.__name__, op, op_abbr))
def run_all(): def run_all():
@ -569,12 +569,12 @@ def run_all():
pass pass
try: try:
os.unlink(f'{poobrains.project_name}.ini') os.unlink('%s.ini' % poobrains.project_name)
except: except:
pass pass
try: try:
os.unlink(f'{poobrains.project_name}.nginx.conf') os.unlink('%s.nginx.conf' % poobrains.project_name)
except: except:
pass pass

View File

@ -1,8 +1,8 @@
$color_background_dark: rgba(8,8,8, 0.85); $color_background_dark: rgba(8,8,8, 0.8);
$color_background_light: rgba(255,255,255, 0.85); $color_background_light: rgba(255,255,255, 0.8);
$color_highlight: rgba(0,128,255, 0.85); $color_highlight: rgba(0,128,255, 0.8);
/*$color_highlight: transparentize(rebeccapurple, 20%);*/ /*$color_highlight: transparentize(rebeccapurple, 20%);*/
$color_danger: rgba(255, 0, 128, 0.85); $color_danger: rgba(255, 0, 128, 0.8);
$color_font_dark: opacify($color_background_dark, 10%); $color_font_dark: opacify($color_background_dark, 10%);
$color_font_light: opacify($color_background_light, 10%); $color_font_light: opacify($color_background_light, 10%);
$color_background_form: opacify($color_background_light, -0.3); $color_background_form: opacify($color_background_light, -0.3);

View File

@ -1,33 +0,0 @@
.content-type-documentation {
aside {
padding: 0.5rem;
background: transparentize($color_background_light, 75%);
text-align: left;
margin-bottom: 0.5rem;
&.doc-error {
background: transparentize($color_danger, 70%);
}
a {
display: inline-block;
}
}
ul.mro {
li {
border-left: 2px solid $color_highlight;
&:last-child {
border-left: none;
}
}
}
.doc-datum {
.doc-datum-name {
display: inline;
}
}
}

View File

@ -72,7 +72,6 @@ body.content-type-dataeditor {
flex-flow: row wrap; flex-flow: row wrap;
background: transparent; background: transparent;
backdrop-filter: none;
.display-wrapper, .display-wrapper,
.form-wrapper { .form-wrapper {
@ -87,7 +86,6 @@ body.content-type-dataeditor {
padding: 0; padding: 0;
height: 100%; height: 100%;
background: transparent; background: transparent;
backdrop-filter: none;
section.tab { section.tab {
overflow-y: auto; overflow-y: auto;
@ -138,7 +136,6 @@ body.content-type-dataeditor {
padding: 0; padding: 0;
min-height: 100%; min-height: 100%;
background: transparent; background: transparent;
backdrop-filter: none;
border: none; border: none;
& > header, & > header,
@ -163,7 +160,6 @@ body.content-type-dataeditor {
section.tab { section.tab {
background: $color_background_dark; background: $color_background_dark;
backdrop-filter: blur($backdrop_blur);
height: calc(100vh - 3rem); height: calc(100vh - 3rem);
fieldset.action-control { fieldset.action-control {
@ -174,7 +170,6 @@ body.content-type-dataeditor {
margin: 0; margin: 0;
border: 0 none; border: 0 none;
background: transparent; background: transparent;
backdrop-filter: none;
& > header { & > header {
display: none; display: none;
@ -196,7 +191,6 @@ body.content-type-dataeditor {
height: calc(100vh - 3.25rem); /* tab-nav is 3rem high, border-top on container is 0.25rem */ height: calc(100vh - 3.25rem); /* tab-nav is 3rem high, border-top on container is 0.25rem */
background: transparent; background: transparent;
backdrop-filter: none;
& > header { & > header {
display: none; display: none;
@ -268,7 +262,6 @@ body.content-type-dataeditor {
& > fieldset { & > fieldset {
background: transparent; background: transparent;
backdrop-filter: none;
} }
} }
} }

View File

@ -4,7 +4,7 @@
{% include 'head.jinja' %} {% include 'head.jinja' %}
</head> </head>
<body class="content-type-{{ content.__class__.__name__.lower() }}{% if g.boxes.dashbar %} dashbar{% endif %}"> <body class="content-type-{{ content.__class__.__name__.lower() }}">
{% if g.boxes.dashbar %} {% if g.boxes.dashbar %}
{{ g.boxes.dashbar.render() }} {{ g.boxes.dashbar.render() }}

View File

@ -104,8 +104,7 @@
@media (min-width: 56rem) { @media (min-width: 56rem) {
#logo-link { #logo-link {
display: block !important; display: flex !important;
padding: 4rem 0;
} }
body > header { body > header {
@ -114,6 +113,7 @@
.sticky { .sticky {
width: 13rem; width: 13rem;
top: 11.5rem !important; /* 7.5rem logo height + 2rem space at top and bottom */ top: 11.5rem !important; /* 7.5rem logo height + 2rem space at top and bottom */
margin-bottom: 4rem; /* needed at least in firefox, to avoid vertical cutoff. should be the same as vertical padding for logo */
} }
} }
@ -133,10 +133,10 @@ html {
width: 100%; width: 100%;
min-height: 100vh; min-height: 100vh;
/*overflow-y: scroll;*/ /* always show vertical scrollbar, viewport dependant widths stay the same */ /*overflow-y: scroll;*/ /* always show vertical scrollbar, viewport dependant widths stay the same */
/*background-image: url('/theme/bg.svg'), linear-gradient(45deg, lighten(opacify($color_background_dark, 100%), 15%), mix($color_highlight, $color_background_dark, 25%));*/ background-image: url('/theme/bg.svg'), linear-gradient(45deg, lighten(opacify($color_background_dark, 100%), 15%), mix($color_highlight, $color_background_dark, 25%));
background-image: url('/theme/bg.svg');
/*background-image: url('/theme/pooscape-probablyold.svg'), linear-gradient(to bottom, opacify($color_background_dark, 0.2), darken(opacify($color_highlight, 0.2), 40%) 100%);*/ /*background-image: url('/theme/pooscape-probablyold.svg'), linear-gradient(to bottom, opacify($color_background_dark, 0.2), darken(opacify($color_highlight, 0.2), 40%) 100%);*/
background-color: opacify($color_background_dark, 100%); /*background-image: url('/theme/test.png');*/
background-color: #333;
background-size: cover; background-size: cover;
background-position: center; background-position: center;
background-attachment: fixed; background-attachment: fixed;
@ -225,18 +225,24 @@ body {
flex-basis: 15rem; flex-basis: 15rem;
flex-shrink: 1; flex-shrink: 1;
flex-grow: 15; flex-grow: 15;
display: flex;
flex-direction: column; flex-direction: column;
align-self: stretch; align-self: stretch;
align-items: center; align-items: center;
justify-content: space-around;
max-height: 100vh; max-height: 100vh;
box-sizing: border-box; box-sizing: border-box;
padding: 1rem; padding: 1rem;
z-index: 200; z-index: 200;
background-color: $color_background_dark; background-color: $color_background_dark;
/*border-left: 0.25rem solid;
border-right: 0.25rem solid;
border-bottom: 0.25rem solid;
border-color: $color_border;*/
/*-webkit-clip-path: polygon(0 0, 0 calc(100% - 3rem), 50% 100%, 100% calc(100% - 3rem), 100% 0);*/ /*-webkit-clip-path: polygon(0 0, 0 calc(100% - 3rem), 50% 100%, 100% calc(100% - 3rem), 100% 0);*/
overflow: auto; overflow: hidden;
&:nth-child(2) { /* means dashbar is rendered */ &:nth-child(2) { /* means dashbar is rendered */
margin-top: 3rem; margin-top: 3rem;
@ -312,8 +318,6 @@ body {
} }
#logo { #logo {
display: block;
margin: 0 auto;
height: 7.5rem; height: 7.5rem;
pointer-events: none; pointer-events: none;
} }
@ -479,10 +483,7 @@ main {
flex-direction: column; flex-direction: column;
*:target { *:target {
/*scroll-margin-top: 15rem; */ /* offset :target'ed items to avoid having them under the sticky header */ scroll-margin-top: 15rem; /* offset :target'ed items to avoid having them under the sticky header */
body.dashbar & {
scroll-margin-top: 4rem;
}
display: block; display: block;
} }
@ -511,7 +512,6 @@ main {
div.content, div.content,
section.controls { section.controls {
background: transparent; background: transparent;
backdrop-filter: none;
} }
section.controls { section.controls {
@ -843,6 +843,10 @@ form {
& > div.content { & > div.content {
background: $color_background_dark; background: $color_background_dark;
h2, h3, h4, h5, h6 {
text-align: center;
}
} }
} }
@ -860,7 +864,6 @@ article {
&.mode-inline { &.mode-inline {
& > .content { & > .content {
background: transparent; background: transparent;
backdrop-filter: none;
padding: 0; padding: 0;
} }
} }
@ -945,13 +948,11 @@ article {
& > div.content { & > div.content {
background: transparent; background: transparent;
backdrop-filter: none;
padding: 0; padding: 0;
} }
& > footer { & > footer {
background: transparent !important; background: transparent !important;
backdrop-filter: none !important;
} }
} }
@ -959,7 +960,6 @@ article {
article > .content { article > .content {
/* nested containers shouldn't stack background color on top of each other */ /* nested containers shouldn't stack background color on top of each other */
background: transparent; background: transparent;
backdrop-filter: none;
} }
} }
@ -983,21 +983,18 @@ article {
form > footer { form > footer {
background: transparent; background: transparent;
backdrop-filter: none;
} }
} }
& > div.content { & > div.content {
background: transparent; background: transparent;
backdrop-filter: none;
padding: 0; padding: 0;
} }
& > footer { & > footer {
background: transparent !important; background: transparent !important;
backdrop-filter: none !important;
} }
} }
@ -1336,6 +1333,5 @@ span.debug-hint {
@import 'modal'; @import 'modal';
@import 'tabs'; @import 'tabs';
@import 'optin'; @import 'optin';
@import 'documentation';
@import 'editor'; @import 'editor';
@import 'custom'; @import 'custom';

View File

@ -3,10 +3,6 @@
$palette_size: 4; $palette_size: 4;
$palette_rotation: 180; /* overall hue rotation of the palette */ $palette_rotation: 180; /* overall hue rotation of the palette */
$palette: (); $palette: ();
$backdrop_blur: 3px;
/*@for $i from 1 through $palette_size { /*@for $i from 1 through $palette_size {
$palette: append($palette, adjust_hue($color_highlight, ($palette_rotation/$palette_size) * ($i - 1))); $palette: append($palette, adjust_hue($color_highlight, ($palette_rotation/$palette_size) * ($i - 1)));
}*/ }*/

View File

@ -69,14 +69,14 @@ svg#hexascroll {
animation: decorotion 5s linear infinite; animation: decorotion 5s linear infinite;
use { use {
animation: scroll 10s linear infinite; /* apply the animation to every hexagon instead of the g so we don't get perspective fuckups */ /*animation: scroll 10s linear infinite; /* apply the animation to every hexagon instead of the g so we don't get perspective fuckups */*/
} }
} }
} }
use { use {
fill: opacify($color_background_dark, -0.2); fill: opacify($color_background_dark, -0.2);
stroke: darken($color_highlight, 30%); stroke: $color_background_light;
stroke-width: 2; stroke-width: 2;
&.hexagon { &.hexagon {

View File

@ -18,7 +18,6 @@
cursor: pointer; cursor: pointer;
padding: 0 1rem; padding: 0 1rem;
background: $color_background_dark; background: $color_background_dark;
backdrop-filter: blur($backdrop_blur);
} }
} }
} }

View File

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
import os import os
import collections import collections
import peewee import peewee
@ -46,7 +48,7 @@ class UploadForm(poobrains.auth.AutoForm):
not extension in self.instance.extension_whitelist: not extension in self.instance.extension_whitelist:
raise poobrains.errors.CompoundError( raise poobrains.errors.CompoundError(
[poobrains.errors.ValidationError( [poobrains.errors.ValidationError(
f"Invalid file extension: {extension}. Try one of {list(self.instance.extension_whitelist)}." 'Invalid file extension: %s. Try one of %s.' % (extension, list(self.instance.extension_whitelist))
)] )]
) )
@ -57,7 +59,7 @@ class UploadForm(poobrains.auth.AutoForm):
upload_file = self.fields['upload'].value upload_file = self.fields['upload'].value
filename = self.fields['filename'].value filename = self.fields['filename'].value
if filename != '': if filename is not '':
file_path = os.path.join(self.instance.path, filename) file_path = os.path.join(self.instance.path, filename)
@ -65,19 +67,19 @@ class UploadForm(poobrains.auth.AutoForm):
if force: if force:
extension = self.fields['filename'].value.split('.')[-1] extension = self.fields['filename'].value.split('.')[-1]
filename = f"{self.fields['name'].value}.{extension}" # name field autogenerated and filled by AutoForm filename = '%s.%s' % (self.fields['name'].value, extension) # name field autogenerated and filled by AutoForm
self.fields['filename'].value = filename # needed so that super call fills in the right name self.fields['filename'].value = filename # needed so that super call fills in the right name
file_path = os.path.join(self.instance.path, filename) file_path = os.path.join(self.instance.path, filename)
else: else:
try: try:
conflict = self.model.select().where(self.model.filename == filename).get() conflict = self.model.select().where(self.model.filename == filename).get()
flask.flash(jinja2.Markup(f'A file already exists at the same place on the filesystem. See if <a href="{conflict.url("raw")}">{conflict.name}</a> is the same file.')) flask.flash(jinja2.Markup(u'A file already exists at the same place on the filesystem. See if <a href="%s">%s</a> is the same file.' % (conflict.url('raw'), conflict.name)))
except self.model.DoesNotExist: except self.model.DoesNotExist:
flask.flash("A file unknown to this site already exists at the same place on the filesystem. /dev/null has been informed.") flask.flash(u'A file unknown to this site already exists at the same place on the filesystem. /dev/null has been informed.')
app.logger.error(f"Unknown file clutter: {app.root_path}/upload/{self.instance.path}/{filename}") app.logger.error('Unknown file clutter: %s/upload/%s/%s' % (app.root_path, self.instance.path, filename))
return self return self
@ -89,13 +91,12 @@ class UploadForm(poobrains.auth.AutoForm):
try: try:
os.remove(self.instance.file_path) os.remove(self.instance.file_path)
except OSError as e: except OSError as e:
app.logger.debug(f"Could not delete old file '{self.filename}' for {self.__class__.__name__} '{self.name}': ") app.logger.debug(u"Could not delete old file '%s' for %s '%s': " % (self.filename, self.__class__.__name__, self.name))
# FIXME: Doesn't the : imply there should be more info following? e.message?
except IOError as e: except IOError as e:
flask.flash(f"Failed saving file '{filename}'.", 'error') flask.flash(u"Failed saving file '%s'." % filename, 'error')
app.logger.error(f"Failed saving file: {filename}\n{type(e).__name__}: {str(e)} / {e.strerror} / {e.filename}") app.logger.error(u"Failed saving file: %s\n%s: %s / %s / %s" % (filename, type(e).__name__, str(e), e.strerror, e.filename))
return self # stop handling, show form within same request return self # stop handling, show form within same request
elif self.mode == 'edit': elif self.mode == 'edit':
@ -106,9 +107,9 @@ class UploadForm(poobrains.auth.AutoForm):
return super(UploadForm, self).process(submit, exceptions=True) return super(UploadForm, self).process(submit, exceptions=True)
except peewee.DatabaseError as e: except peewee.DatabaseError as e:
flask.flash(f"Could not save file metadata for file '{filename}'. Deleting file, sorry if it was big. ¯\_(ツ)_/¯") # TODO: make this resumable? flask.flash(u"Could not save file metadata for file '%s'. Deleting file, sorry if it was big. ¯\_(ツ)_/¯" % filename)
flask.flash(str(e)) flask.flash(str(e))
app.logger.error(f"Failed saving file metadata: {filename}\n{type(e).__name__}: {str(e)}") app.logger.error(u"Failed saving file metadata: %s\n%s: %s" % (filename, type(e).__name__, str(e)))
os.remove(file_path) os.remove(file_path)
return self return self
@ -161,7 +162,7 @@ class File(poobrains.auth.NamedOwned):
if mode == 'raw': if mode == 'raw':
response = flask.send_from_directory(self.path, self.filename) response = flask.send_from_directory(self.path, self.filename)
response.headers['Content-Disposition'] = f'filename="{self.filename}"' response.headers['Content-Disposition'] = u'filename="%s"' % self.filename
# Disable "public" mode caching downstream (nginx, varnish) in order to hopefully not leak restricted content # Disable "public" mode caching downstream (nginx, varnish) in order to hopefully not leak restricted content
response.cache_control.public = False response.cache_control.public = False
@ -194,7 +195,7 @@ class File(poobrains.auth.NamedOwned):
try: try:
os.remove(os.path.join(self.path, self.filename)) os.remove(os.path.join(self.path, self.filename))
except OSError as e: except OSError as e:
flask.flash(f"Could not delete {self.__class__.__name__} '{self.filename}'.") flask.flash(u"Could not delete %s '%s'." % (self.__class__.__name__, self.filename))
return r return r

View File

@ -16,11 +16,9 @@ setup(
'pyScss', 'pyScss',
'pillow', # for image manipulation and generation (primarily to generate captchas) 'pillow', # for image manipulation and generation (primarily to generate captchas)
'markdown (>=3.0)', 'markdown (>=3.0)',
'pretty-bad-protocol', # formerly 'gnupg'
'numpy',
'pyproj', # map projection 'pyproj', # map projection
'pretty-bad-protocol', # formerly 'gnupg'
'geojson', 'geojson',
'Shapely',
'bson', 'bson',
], ],