Compare commits
No commits in common. "main" and "trashpandas" have entirely different histories.
main
...
trashpanda
@ -1,3 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
""" A webframework for aspiring media terrorists. """
|
||||
|
||||
import os
|
||||
@ -440,32 +442,20 @@ class Poobrain(flask.Flask):
|
||||
peewee.DoesNotExist: 404
|
||||
}
|
||||
|
||||
setup_funcs = None
|
||||
cronjobs = None
|
||||
|
||||
_setup_done = False
|
||||
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
if not 'root_path' in kwargs:
|
||||
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)
|
||||
|
||||
self.setup_funcs = []
|
||||
self.cronjobs = []
|
||||
|
||||
@click.group(cls=flask.cli.FlaskGroup, create_app=lambda x=None: self)
|
||||
@click.option('--database', default=f"sqlite:///{project_name}.db")
|
||||
@click.group(cls=flask.cli.FlaskGroup, create_app=lambda x: self)
|
||||
@click.option('--database', default="sqlite:///%s.db" % project_name)
|
||||
def cli(database):
|
||||
self.db = db_url.connect(database)
|
||||
self.cli = cli
|
||||
@ -494,7 +484,7 @@ class Poobrain(flask.Flask):
|
||||
|
||||
user = os.getlogin()
|
||||
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:
|
||||
# show SQL queries
|
||||
@ -508,7 +498,7 @@ class Poobrain(flask.Flask):
|
||||
import pudb
|
||||
if hasattr(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
|
||||
|
||||
@ -531,10 +521,10 @@ class Poobrain(flask.Flask):
|
||||
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?
|
||||
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()
|
||||
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.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.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):
|
||||
#self.cli(obj={})
|
||||
self.cli()
|
||||
@ -566,28 +548,18 @@ class Poobrain(flask.Flask):
|
||||
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 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()
|
||||
|
||||
def setup(self):
|
||||
|
||||
self.register_blueprint(self.site)
|
||||
self.register_blueprint(self.admin, url_prefix='/admin/')
|
||||
|
||||
self._setup_done = True
|
||||
|
||||
|
||||
@locked_cached_property
|
||||
def theme_paths(self):
|
||||
@ -603,7 +575,7 @@ class Poobrain(flask.Flask):
|
||||
|
||||
return paths
|
||||
|
||||
|
||||
|
||||
def serve_theme_resources(self, resource):
|
||||
|
||||
r = False
|
||||
@ -642,6 +614,7 @@ class Poobrain(flask.Flask):
|
||||
except jinja2.exceptions.TemplateNotFound:
|
||||
abort(404)
|
||||
|
||||
|
||||
else:
|
||||
|
||||
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.max_age = app.config['CACHE_LONG']
|
||||
return r
|
||||
|
||||
|
||||
abort(404)
|
||||
|
||||
|
||||
def request_setup(self):
|
||||
|
||||
|
||||
flask.g.boxes = {}
|
||||
flask.g.forms = {}
|
||||
#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
|
||||
|
||||
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:
|
||||
try:
|
||||
@ -729,12 +702,12 @@ class Poobrain(flask.Flask):
|
||||
|
||||
if not extra_modes is None:
|
||||
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
|
||||
|
||||
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):
|
||||
self.site.add_related_view(cls, related_fields[0], os.path.join(rule, '<handle>/'))
|
||||
@ -794,18 +767,21 @@ class Poobrain(flask.Flask):
|
||||
except LookupError:
|
||||
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):
|
||||
|
||||
blueprint = self.blueprints[flask.request.blueprint]
|
||||
return blueprint.get_related_view_url(cls, handle, related_field, add=add)
|
||||
|
||||
|
||||
def cron(self, func):
|
||||
|
||||
self.cronjobs.append(func)
|
||||
return func
|
||||
|
||||
|
||||
def cron_run(self):
|
||||
|
||||
self.logger.info("Starting cron run.")
|
||||
@ -847,9 +823,9 @@ class Pooprint(flask.Blueprint):
|
||||
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.db = app.db
|
||||
@ -893,7 +869,7 @@ class Pooprint(flask.Blueprint):
|
||||
self.related_views[cls] = collections.OrderedDict()
|
||||
|
||||
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
|
||||
self.related_views[cls][related_field] = []
|
||||
|
||||
@ -914,7 +890,7 @@ class Pooprint(flask.Blueprint):
|
||||
return cls.related_view_add(*args, **kwargs)
|
||||
|
||||
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_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
|
||||
|
||||
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(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:
|
||||
return endpoint
|
||||
|
||||
nice_param_list = ', '.join(url_params.keys())
|
||||
raise ValueError(f"No fitting url rule found for all params: {nice_param_list}")
|
||||
raise ValueError("No fitting url rule found for all params: %s", ','.join(url_params.keys()))
|
||||
|
||||
|
||||
def get_url(self, cls, mode=None, **url_params):
|
||||
@ -1036,12 +1011,13 @@ class Pooprint(flask.Blueprint):
|
||||
mode = 'full'
|
||||
|
||||
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]:
|
||||
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:
|
||||
endpoint = self.choose_endpoint(endpoints, **url_params)
|
||||
else:
|
||||
@ -1070,12 +1046,12 @@ class Pooprint(flask.Blueprint):
|
||||
offset = cls.select().where(*clauses).count() - 1
|
||||
|
||||
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]:
|
||||
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)
|
||||
|
||||
# if isinstance(offset, int) and offset > 0:
|
||||
@ -1084,7 +1060,7 @@ class Pooprint(flask.Blueprint):
|
||||
kw = copy.copy(url_params)
|
||||
if offset > 0:
|
||||
kw['offset'] = offset
|
||||
endpoint = f"{endpoint}_offset"
|
||||
endpoint = "%s_offset" % endpoint
|
||||
|
||||
return flask.url_for(endpoint, **kw)
|
||||
|
||||
@ -1099,12 +1075,12 @@ class Pooprint(flask.Blueprint):
|
||||
|
||||
|
||||
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]:
|
||||
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})
|
||||
|
||||
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
|
||||
|
||||
endpoint_base = f"{cls.__name__}_{context}_{mode}"
|
||||
format = '%s_%s_%s_autogen_%%d' % (cls.__name__, context, mode)
|
||||
|
||||
try:
|
||||
if context == 'view':
|
||||
@ -1121,16 +1097,16 @@ class Pooprint(flask.Blueprint):
|
||||
endpoints = self.listings[cls][mode]
|
||||
elif context == 'related':
|
||||
# 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]
|
||||
|
||||
except KeyError: # means no view/listing has been registered yet
|
||||
endpoints = []
|
||||
|
||||
i = 1
|
||||
endpoint = f"{endpoint_base}_{i}"
|
||||
endpoint = format % (i,)
|
||||
while endpoint in endpoints:
|
||||
endpoint = f"{endpoint_base}_{i}"
|
||||
endpoint = format % (i,)
|
||||
i += 1
|
||||
|
||||
return endpoint
|
||||
@ -1188,7 +1164,7 @@ class ErrorPage(rendering.Renderable):
|
||||
self.code = code
|
||||
break
|
||||
|
||||
self.title = f"Ermahgerd, {self.code}!"
|
||||
self.title = "Ermahgerd, %s!" % self.code
|
||||
|
||||
if isinstance(self.error, errors.ExposedError):
|
||||
self.message = str(error)
|
||||
@ -1211,7 +1187,7 @@ class ErrorPage(rendering.Renderable):
|
||||
def errorpage(error):
|
||||
|
||||
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 hasattr(error, 'code'):
|
||||
|
@ -1,3 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from poobrains import app
|
||||
|
||||
from . import util
|
||||
|
@ -1,3 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import collections
|
||||
import io
|
||||
import re
|
||||
@ -50,7 +52,7 @@ def load_dataset(handle):
|
||||
ds.data[name] = source['function'](**parameters)
|
||||
|
||||
else:
|
||||
raise poobrains.errors.ExposedError(f'Unknown dynamic dataset: {name}')
|
||||
raise poobrains.errors.ExposedError('Unknown dynamic dataset: %s' % name)
|
||||
|
||||
else:
|
||||
|
||||
@ -352,7 +354,7 @@ class EphemeralDataset(poobrains.auth.Protected):
|
||||
|
||||
@locked_cached_property
|
||||
def ref_id(self):
|
||||
return f"dataset-{self.name}"
|
||||
return "dataset-%s" % self.name
|
||||
|
||||
@locked_cached_property
|
||||
def empty(self):
|
||||
@ -509,7 +511,7 @@ class EphemeralDataset(poobrains.auth.Protected):
|
||||
plot_kinds = visualization.Plot.class_children_keyed()
|
||||
|
||||
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'))
|
||||
|
||||
@ -552,8 +554,8 @@ class EphemeralDataset(poobrains.auth.Protected):
|
||||
ds.owner = owner or g.user
|
||||
|
||||
now = int(time.time())
|
||||
ds.name = name or poobrains.helpers.clean_string(f"{self.name}-{now}")
|
||||
ds.title = f'{self.title}@{str(datetime.datetime.fromtimestamp(now))}'
|
||||
ds.name = name or poobrains.helpers.clean_string("%s-%d" % (self.name, now))
|
||||
ds.title = '%s@%s' % (self.title, str(datetime.datetime.fromtimestamp(now)))
|
||||
ds.description = self.description
|
||||
ds.data = self.data
|
||||
ds.save(force_insert=True)
|
||||
@ -576,8 +578,6 @@ class Dataset(EphemeralDataset, poobrains.commenting.Commentable):
|
||||
|
||||
class Meta:
|
||||
|
||||
database = app.db
|
||||
|
||||
modes = collections.OrderedDict([
|
||||
('add', 'create'),
|
||||
('teaser', 'read'),
|
||||
@ -600,7 +600,7 @@ class Dataset(EphemeralDataset, poobrains.commenting.Commentable):
|
||||
|
||||
@locked_cached_property
|
||||
def ref_id(self):
|
||||
return f"dataset-{self.name}"
|
||||
return "dataset-%s" % self.name
|
||||
|
||||
@classmethod
|
||||
def load(self, handle):
|
||||
|
@ -1,3 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import math
|
||||
import re
|
||||
|
||||
@ -41,7 +43,7 @@ def datasource_choices():
|
||||
def validate_handle_free(handle):
|
||||
if handle in session['apps']['DataEditor']:
|
||||
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:
|
||||
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()
|
||||
|
||||
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_description'] = poobrains.form.fields.TextArea(label='Description', default=self.formapp.dataset.description)
|
||||
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.apply = poobrains.form.Button('submit', label='Apply')
|
||||
|
||||
def process(self, submit):
|
||||
|
||||
if submit == 'apply':
|
||||
#self.formapp.dataset.title = self['dataset_title'].value
|
||||
#self.formapp.dataset.description = self['dataset_description'].value
|
||||
self['basedata_form'].process('save')
|
||||
self.formapp.dataset.title = self['dataset_title'].value
|
||||
self.formapp.dataset.description = self['dataset_description'].value
|
||||
|
||||
flash("Updated dataset base info.")
|
||||
self.parent.tool_active = None
|
||||
|
@ -77,15 +77,12 @@ class FileFieldset(poobrains.formapp.MultistateFieldset):
|
||||
|
||||
self['file'] = poobrains.form.fields.File()
|
||||
|
||||
self['consumer'] = poobrains.form.fields.Select(choices=(
|
||||
('csv', "CSV"),
|
||||
('geojson', "geojson"),
|
||||
))
|
||||
self['consumer'] = poobrains.form.fields.Select(choices=(
|
||||
('csv', "CSV"),
|
||||
('geojson', "geojson"),
|
||||
))
|
||||
|
||||
if self.state == 'csv':
|
||||
|
||||
self['consumer'].value = 'csv'
|
||||
self['consumer'].readonly = True
|
||||
elif self.state == 'csv':
|
||||
|
||||
csv_options = self.session['csv_options']
|
||||
|
||||
@ -105,14 +102,13 @@ class FileFieldset(poobrains.formapp.MultistateFieldset):
|
||||
flash(f"Full error message: {e}", 'error')
|
||||
else:
|
||||
|
||||
self.session['csv_options'] = csv_options
|
||||
table = poobrains.rendering.Table(title='CSV preview')
|
||||
for idx, row in enumerate(reader):
|
||||
if idx == 5:
|
||||
break
|
||||
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')
|
||||
|
||||
escaped = {}
|
||||
@ -138,46 +134,21 @@ class FileFieldset(poobrains.formapp.MultistateFieldset):
|
||||
|
||||
def process(self, submit):
|
||||
|
||||
if self.state == 'csv':
|
||||
if submit == 'change_csv_options':
|
||||
unescape = lambda x: bytes(x, 'utf-8').decode('unicode_escape')
|
||||
self.session['csv_options'] = {
|
||||
'delimiter': unescape(self['delimiter'].value or ''),
|
||||
'doublequote': self['doublequote'].value,
|
||||
'escapechar': unescape(self['escapechar'].value or ''),
|
||||
'lineterminator': unescape(self['lineterminator'].value),
|
||||
'quotechar': unescape(self['quotechar'].value or ''),
|
||||
'quoting': self['quoting'].value,
|
||||
'skipinitialspace': self['skipinitialspace'].value,
|
||||
}
|
||||
if submit == 'change_csv_options':
|
||||
unescape = lambda x: bytes(x, 'utf-8').decode('unicode_escape')
|
||||
self.session['csv_options'] = {
|
||||
'delimiter': unescape(self['delimiter'].value),
|
||||
'doublequote': self['doublequote'].value,
|
||||
'escapechar': unescape(self['escapechar'].value),
|
||||
'lineterminator': unescape(self['lineterminator'].value),
|
||||
'quotechar': unescape(self['quotechar'].value),
|
||||
'quoting': self['quoting'].value,
|
||||
'skipinitialspace': self['skipinitialspace'].value,
|
||||
}
|
||||
|
||||
flash("Changed CSV dialect options.")
|
||||
else:
|
||||
dialect = dialect_from_dict(self.session['csv_options'])
|
||||
flash("Changed CSV dialect options.")
|
||||
|
||||
try:
|
||||
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
|
||||
else: # means parent form wasn't submitting via cancel button – i.e. the "load" button was clicked
|
||||
|
||||
consumer = self['consumer'].value
|
||||
raw = self['file'].value.read()
|
||||
@ -213,7 +184,7 @@ class FileFieldset(poobrains.formapp.MultistateFieldset):
|
||||
|
||||
elif consumer == 'csv':
|
||||
self.state = 'csv'
|
||||
self.session['csv_options'] = None
|
||||
self.session['data_action_info']['csv_options'] = None
|
||||
|
||||
else:
|
||||
flash("Unhandled", 'error')
|
||||
|
@ -1,4 +1,3 @@
|
||||
import functools
|
||||
import datetime
|
||||
import numpy
|
||||
import shapely.geometry
|
||||
@ -28,14 +27,12 @@ def pretty_si(number):
|
||||
|
||||
value /= 1000.0
|
||||
|
||||
return f"{value:.3f}{postfix}"
|
||||
|
||||
return "%.3f%s" % (value, postfix)
|
||||
|
||||
def pretty_datetime64(dt64):
|
||||
dt = dt64.item()
|
||||
return f"{dt.day}. {dt.month}. {dt.year} – {dt.hour}:{dt.minute}:{dt.second}",
|
||||
|
||||
|
||||
__geo_lookup__ = {
|
||||
geojson.Point: shapely.geometry.Point,
|
||||
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)
|
||||
|
||||
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)
|
||||
else:
|
||||
self['value'] = self.dtype.form_field(type=self.dtype.form_type)
|
||||
|
@ -1,3 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import math
|
||||
import pyproj
|
||||
|
||||
@ -92,10 +94,10 @@ class Plot(poobrains.svg.SVG):
|
||||
return [k for k in self.layers.keys()].index(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):
|
||||
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
|
||||
def palette(self):
|
||||
|
@ -1,3 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The authentication system.
|
||||
|
||||
@ -39,7 +41,7 @@ def enforce_tls():
|
||||
))
|
||||
|
||||
|
||||
@app.setup
|
||||
@app.before_first_request
|
||||
def admin_setup():
|
||||
|
||||
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):
|
||||
|
||||
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)
|
||||
|
||||
if not 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:
|
||||
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:
|
||||
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:
|
||||
try:
|
||||
|
||||
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:
|
||||
self.title = f"{self.mode} {self.model.__name__}"
|
||||
self.title = "%s %s" % (self.mode, self.model.__name__)
|
||||
|
||||
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():
|
||||
#if hasattr(self.instance, name): # fucks up with DoesNotExist on unfilled foreign keys
|
||||
@ -216,7 +218,7 @@ class AutoForm(BoundForm):
|
||||
saved = self.instance.save()
|
||||
|
||||
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:
|
||||
self.process_all_fieldsets(submit)
|
||||
except Exception as e:
|
||||
@ -234,7 +236,7 @@ class AutoForm(BoundForm):
|
||||
return self
|
||||
else:
|
||||
|
||||
flash(f"Couldn't save {self.model.__name__}.")
|
||||
flash(u"Couldn't save %s." % self.model.__name__)
|
||||
|
||||
except poobrains.errors.ValidationError as e:
|
||||
|
||||
@ -248,21 +250,21 @@ class AutoForm(BoundForm):
|
||||
if exceptions:
|
||||
raise
|
||||
|
||||
flash(f'Integrity error: {str(e)}', 'error')
|
||||
app.logger.error(f"Integrity error: {str(e)}")
|
||||
flash(u'Integrity error: %s' % str(e), 'error')
|
||||
app.logger.error(u"Integrity error: %s" % str(e))
|
||||
|
||||
except Exception as e:
|
||||
if exceptions:
|
||||
raise
|
||||
flash(f"Couldn't save {self.model.__name__} for mysterious reasons.")
|
||||
app.logger.error(f"Couldn't save {self.model.__name__}. {type(e).__name__}: {str(e)}")
|
||||
flash(u"Couldn't save %s for mysterious reasons." % self.model.__name__)
|
||||
app.logger.error(u"Couldn't save %s. %s: %s" % (self.model.__name__, type(e).__name__, str(e)))
|
||||
|
||||
|
||||
elif submit == 'preview':
|
||||
self.preview = self.instance.render('full')
|
||||
|
||||
else:
|
||||
flash(f"Not handling readonly form '{self.name}'.")
|
||||
flash(u"Not handling readonly form '%s'." % self.name)
|
||||
|
||||
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.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.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)
|
||||
if not 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:
|
||||
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):
|
||||
|
||||
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:
|
||||
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)
|
||||
flash(message)
|
||||
|
||||
@ -327,7 +329,7 @@ class AccessField(poobrains.form.fields.Field):
|
||||
|
||||
if name == 'prefix':
|
||||
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':
|
||||
|
||||
@ -393,7 +395,7 @@ class Permission(poobrains.helpers.ChildAware):
|
||||
@classmethod
|
||||
def check(cls, user):
|
||||
|
||||
msg = f"Permission {cls.__name__} denied."
|
||||
msg = "Permission %s denied." % cls.__name__
|
||||
granted = None
|
||||
|
||||
# check user-assigned permission state
|
||||
@ -445,7 +447,7 @@ class Permission(poobrains.helpers.ChildAware):
|
||||
granted = self._check_values[user]
|
||||
|
||||
if not granted:
|
||||
raise AccessDenied(f"Permission {self.__class__.__name__} denied.")
|
||||
raise AccessDenied("Permission %s denied." % self.__class__.__name__)
|
||||
|
||||
return granted
|
||||
|
||||
@ -502,8 +504,8 @@ class PermissionInjection(poobrains.helpers.MetaCompatibility):
|
||||
|
||||
#for op in ['create', 'read', 'update', 'delete']:
|
||||
for op in set(cls._meta.modes.values()):
|
||||
perm_name = f"{cls.__name__}_{op}"
|
||||
perm_label = f"{op.capitalize()} {cls.__name__}"
|
||||
perm_name = "%s_%s" % (cls.__name__, op)
|
||||
perm_label = "%s %s" % (op.capitalize(), cls.__name__)
|
||||
#cls._meta.permissions[mode] = type(perm_name, (cls._meta.permission_class,), {})
|
||||
perm_attrs = dict()
|
||||
perm_attrs['op'] = op
|
||||
@ -536,7 +538,7 @@ class PermissionParamType(poobrains.form.types.StringParamType):
|
||||
try:
|
||||
permission, access = cleaned_string.split('.')
|
||||
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)
|
||||
|
||||
@ -558,25 +560,25 @@ class FormPermissionField(poobrains.form.fields.Select):
|
||||
permissions = Permission.class_children_keyed()
|
||||
for perm_name in sorted(permissions):
|
||||
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):
|
||||
permission, access = self.value
|
||||
|
||||
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]
|
||||
choice_values = [t[0] for t in perm_class.choices]
|
||||
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):
|
||||
|
||||
m = poobrains.rendering.Menu('actions')
|
||||
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
|
||||
|
||||
@ -595,7 +597,7 @@ def admin_menu():
|
||||
for mode, endpoints in listings.items():
|
||||
|
||||
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
|
||||
|
||||
@ -616,11 +618,11 @@ def admin_index():
|
||||
for administerable, listings in app.admin.listings.items():
|
||||
|
||||
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 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)
|
||||
if administerable.__doc__:
|
||||
@ -662,16 +664,17 @@ def protected(func):
|
||||
cls = cls_or_instance
|
||||
|
||||
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:
|
||||
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]
|
||||
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:
|
||||
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)
|
||||
|
||||
@ -740,11 +743,11 @@ class ClientCertForm(poobrains.form.Form):
|
||||
if self.controls['tls_submit'].value:
|
||||
r = app.response_class(pkcs12.export(passphrase=passphrase))
|
||||
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
|
||||
|
||||
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['Subject'] = 'Bow before entropy'
|
||||
@ -756,10 +759,11 @@ class ClientCertForm(poobrains.form.Form):
|
||||
|
||||
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
|
||||
|
||||
|
||||
try:
|
||||
cert_info.save()
|
||||
|
||||
@ -809,7 +813,7 @@ class OwnedPermission(Permission):
|
||||
return True
|
||||
|
||||
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!")
|
||||
|
||||
else:
|
||||
@ -968,7 +972,7 @@ class RelatedForm(poobrains.form.Form):
|
||||
|
||||
endpoint = request.endpoint
|
||||
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)
|
||||
|
||||
@ -1007,7 +1011,7 @@ class RelatedForm(poobrains.form.Form):
|
||||
self.related_model = related_model
|
||||
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):
|
||||
@ -1017,8 +1021,8 @@ class RelatedForm(poobrains.form.Form):
|
||||
try:
|
||||
fieldset.process(submit)
|
||||
except Exception as e:
|
||||
flash(f"Failed to process fieldset '{fieldset.prefix}.{fieldset.name}'.")
|
||||
app.logger.error(f"Failed to process fieldset {fieldset.prefix}.{fieldset.name} - {type(e).__name__}: {str(e)}")
|
||||
flash(u"Failed to process fieldset '%s.%s'." % (fieldset.prefix, fieldset.name))
|
||||
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 self
|
||||
@ -1125,7 +1129,7 @@ class Protected(poobrains.rendering.Renderable, metaclass=PermissionInjection):
|
||||
except AccessDenied:
|
||||
|
||||
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
|
||||
|
||||
@ -1249,7 +1253,7 @@ class Administerable(poobrains.storage.Storable, Protected, metaclass=BaseAdmini
|
||||
actions.append(self.url(mode), mode)
|
||||
|
||||
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:
|
||||
pass
|
||||
#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):
|
||||
|
||||
n = f'form_{mode}'
|
||||
n = 'form_%s' % mode
|
||||
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)
|
||||
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):
|
||||
|
||||
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
|
||||
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!
|
||||
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')
|
||||
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.
|
||||
"""
|
||||
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 hasattr(model, 'id') and isinstance(model.id, poobrains.storage.fields.AutoField), f"@on_profile model without id: {model.__name__}"
|
||||
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), "@on_profile model without id: %s" % model.__name__
|
||||
|
||||
cls._on_profile.append(model)
|
||||
|
||||
@ -1584,7 +1588,7 @@ class User(ProtectedNamed):
|
||||
for model in self.models_on_profile:
|
||||
|
||||
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:
|
||||
pass # ignore models we aren't allowed to read
|
||||
|
||||
@ -1649,7 +1653,7 @@ class UserPermission(Administerable):
|
||||
return Permission.class_children_keyed()[self.permission]
|
||||
|
||||
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?
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
@ -1659,7 +1663,7 @@ class UserPermission(Administerable):
|
||||
valid_permission_names.append(cls.__name__)
|
||||
|
||||
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)
|
||||
|
||||
@ -1728,7 +1732,7 @@ class GroupPermission(Administerable):
|
||||
return Permission.class_children_keyed()[self.permission]
|
||||
|
||||
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?
|
||||
|
||||
def form(self, mode=None):
|
||||
@ -1753,7 +1757,7 @@ class GroupPermission(Administerable):
|
||||
except Group.DoesNotExist:
|
||||
name = 'FNORD'
|
||||
|
||||
return f"{name}-{self.permission}"
|
||||
return "%s-%s" % (name, self.permission)
|
||||
|
||||
|
||||
class ClientCertTokenAddForm(AutoForm):
|
||||
@ -1762,7 +1766,7 @@ class ClientCertTokenAddForm(AutoForm):
|
||||
|
||||
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
|
||||
|
||||
@ -1800,14 +1804,18 @@ class ClientCertToken(Administerable, Protected):
|
||||
|
||||
if user_token_count >= app.config['MAX_TOKENS']:
|
||||
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():
|
||||
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():
|
||||
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)
|
||||
|
||||
@ -1836,7 +1844,7 @@ class ClientCert(Administerable):
|
||||
|
||||
if not self.id or force_insert:
|
||||
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)
|
||||
|
||||
@ -1956,7 +1964,7 @@ class Page(Owned):
|
||||
|
||||
elif op == 'read' and 'path' in kwargs:
|
||||
|
||||
path = f"/{kwargs['path']}"
|
||||
path = '/%s' % kwargs['path']
|
||||
instance = cls.get(cls.path == path)
|
||||
|
||||
else:
|
||||
@ -1991,7 +1999,7 @@ def bury_tokens():
|
||||
|
||||
count = q.execute()
|
||||
|
||||
msg = f"Deleted {count} dead client certificate tokens."
|
||||
msg = "Deleted %d dead client certificate tokens." % count
|
||||
click.secho(msg, fg='green')
|
||||
app.logger.info(msg)
|
||||
|
||||
@ -2031,7 +2039,7 @@ You have {other_cert_count} other valid certificates on this site.
|
||||
)
|
||||
|
||||
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)
|
||||
count_expired += 1
|
||||
|
||||
@ -2063,7 +2071,7 @@ You have {other_cert_count} other valid certificates on this site.
|
||||
)
|
||||
|
||||
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)
|
||||
count_impending_doom += 1
|
||||
@ -2071,4 +2079,4 @@ You have {other_cert_count} other valid certificates on this site.
|
||||
cert.notification += 1
|
||||
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')
|
||||
|
@ -1,3 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import datetime
|
||||
import functools
|
||||
@ -29,7 +31,7 @@ def mkconfig(template, **values):
|
||||
|
||||
template_dir = os.path.join(app.poobrain_path, 'cli', 'templates')
|
||||
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)
|
||||
|
||||
@ -70,7 +72,7 @@ def test():
|
||||
@option('--mail-user', prompt="Site email account username")
|
||||
@option('--mail-password', prompt="Site email password")
|
||||
@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-binary', default=None)
|
||||
@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:
|
||||
access = 'grant'
|
||||
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
|
||||
|
||||
gp = poobrains.auth.GroupPermission()
|
||||
@ -169,7 +171,7 @@ def install(**options):
|
||||
t.cert_name = options['admin_cert_name']
|
||||
|
||||
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")
|
||||
@ -217,11 +219,11 @@ def install(**options):
|
||||
type_dir = os.path.join(upload_dir, type_name)
|
||||
|
||||
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 + '_')
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@ -230,7 +232,8 @@ def install(**options):
|
||||
else:
|
||||
prefix = 'postgres://'
|
||||
|
||||
options['database'] = f"{prefix}{poobrains.app.db.database}"
|
||||
options['database'] = '%s%s' % (prefix, poobrains.app.db.database)
|
||||
|
||||
|
||||
config = mkconfig('config', **options)
|
||||
config_fd = open(os.path.join(app.root_path, 'config.py'), 'w')
|
||||
@ -240,18 +243,18 @@ def install(**options):
|
||||
if options['deployment'] == 'uwsgi+nginx':
|
||||
|
||||
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.write(uwsgi_ini)
|
||||
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_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.write(nginx_conf)
|
||||
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
|
||||
echo("Installation complete!\n")
|
||||
@ -295,14 +298,14 @@ def minica(lifetime):
|
||||
|
||||
tls_dir = os.path.join(app.root_path, 'tls')
|
||||
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()
|
||||
|
||||
|
||||
echo(f"Creating directory '{tls_dir}'…")
|
||||
echo("Creating directory '%s'." % 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)
|
||||
fd = open(os.path.join(tls_dir, 'cert.pem'), 'w')
|
||||
fd.write(cert_pem.decode('ascii'))
|
||||
@ -324,20 +327,25 @@ def add(storable):
|
||||
|
||||
instance = storable()
|
||||
|
||||
echo(f"Addding {storable.__name__}…\n")
|
||||
echo("Addding %s...\n" % (storable.__name__,))
|
||||
for field in storable._meta.sorted_fields:
|
||||
|
||||
if not isinstance(field, peewee.AutoField):
|
||||
|
||||
default = None
|
||||
|
||||
if callable(field.default):
|
||||
default = field.default()
|
||||
else:
|
||||
default = field.default
|
||||
if field.default:
|
||||
|
||||
if field.null and default is None:
|
||||
default = '' # None interpreted as no default, '' treated as no value
|
||||
if callable(field.default):
|
||||
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)
|
||||
|
||||
@ -353,7 +361,8 @@ def add(storable):
|
||||
def list(storable):
|
||||
|
||||
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()
|
||||
@ -361,17 +370,17 @@ def list(storable):
|
||||
@fake_before_request
|
||||
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)
|
||||
if confirm(f"Really delete this {storable.__name__}?"):
|
||||
if confirm("Really delete this %s?" % storable.__name__):
|
||||
|
||||
handle = instance.handle_string
|
||||
|
||||
if instance.delete_instance(recursive=True):
|
||||
echo(f"Deleted {storable.__name__} {handle}")
|
||||
echo("Deleted %s %s" % (storable.__name__, handle))
|
||||
|
||||
else:
|
||||
echo(f"Could not delete {storable.__name__} {handle}.")
|
||||
echo("Could not delete %s %s." % (storable.__name__, handle))
|
||||
|
||||
|
||||
@app.cli.command()
|
||||
@ -395,7 +404,7 @@ def import_(storable, filepath, skip_pk):
|
||||
for field in fields:
|
||||
|
||||
if isinstance(field, poobrains.storage.fields.ForeignKeyField):
|
||||
actual_name = f"{field.name}_id"
|
||||
actual_name = "%s_id" % field.name
|
||||
else:
|
||||
actual_name = field.name
|
||||
|
||||
@ -415,7 +424,7 @@ def import_(storable, filepath, skip_pk):
|
||||
echo("Invalid import file, stopping.", err=True)
|
||||
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:
|
||||
|
||||
@ -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
|
||||
|
||||
actual_name = f"{field.name}_id"
|
||||
actual_name = "%s_id" % field.name
|
||||
|
||||
if actual_name in record:
|
||||
if record[actual_name] == '':
|
||||
|
@ -1,3 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import random
|
||||
import functools
|
||||
@ -159,9 +161,9 @@ class CommentForm(poobrains.form.Form):
|
||||
self.instance = instance
|
||||
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:
|
||||
self.action += f"/{reply_to.id}"
|
||||
self.action += "/%d" % reply_to.id
|
||||
|
||||
|
||||
def process(self, submit):
|
||||
@ -252,7 +254,7 @@ class Challenge(poobrains.storage.Named):
|
||||
|
||||
char_size = font.getsize(char)
|
||||
|
||||
char_wrapped = f' {char} '
|
||||
char_wrapped = ' %s ' % char
|
||||
char_wrapped_size = font.getsize(char_wrapped)
|
||||
|
||||
char_layer = Image.new('RGBA', char_wrapped_size, (0,0,0,0))
|
||||
@ -316,11 +318,11 @@ class ChallengeForm(poobrains.form.Form):
|
||||
|
||||
except KeyError:
|
||||
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('/')
|
||||
|
||||
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'))
|
||||
|
||||
comment = Comment()
|
||||
@ -334,7 +336,7 @@ class ChallengeForm(poobrains.form.Form):
|
||||
flask.flash(u"Your comment has been saved.")
|
||||
|
||||
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
|
||||
return flask.redirect(instance.url('full'))
|
||||
@ -351,4 +353,4 @@ def bury_orphaned_challenges():
|
||||
|
||||
count = q.execute()
|
||||
|
||||
app.logger.info(f"Deleted {count} orphaned comment challenges.")
|
||||
app.logger.info("Deleted %d orphaned comment challenges." % count)
|
||||
|
@ -1,3 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import datetime
|
||||
from . import md_default
|
||||
|
||||
@ -24,10 +26,8 @@ SVG_PLOT_DESCRIPTION_HEIGHT = 80
|
||||
SVG_MAP_PROJECTION = 'epsg:3857' # WGS84, as used by OSM etc.
|
||||
|
||||
SMTP_HOST = None # str, ip address or dns name
|
||||
#SMTP_PORT = 587 # int
|
||||
#SMTP_STARTTLS = True
|
||||
SMTP_PORT = 465 # int
|
||||
SMTP_STARTTLS = False
|
||||
SMTP_PORT = 587 # int
|
||||
SMTP_STARTTLS = True
|
||||
SMTP_ACCOUNT = None # str
|
||||
SMTP_PASSWORD = None # str
|
||||
SMTP_FROM = None
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -5,12 +5,14 @@
|
||||
## Kill the boilerplate ##
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
#!/usr/bin/env python2.7
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import poobrains
|
||||
app = poobrains.app
|
||||
|
||||
if __name__ == '__main__':
|
||||
poobrains.app.cli()
|
||||
app.cli()
|
||||
```
|
||||
|
||||
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
|
||||
with some basics amongst which you will find users, groups, uploads,
|
||||
tags, comments and search as well as the more specialized facilities
|
||||
for data analysis and visualization.
|
||||
tags, comments and search.
|
||||
|
||||
|
||||
## 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.
|
||||
|
||||
|
||||
### There is no JavaScript. ###
|
||||
### There is no javascript. ###
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
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!
|
||||
|
||||
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,
|
||||
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 ###
|
||||
|
||||
@ -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.
|
||||
|
||||
poobrains instead uses TLS client certificates for more granular
|
||||
control and better security, making use of public-key cryptography
|
||||
on the web and thankfully handing authentication off to the httpd
|
||||
while also making it impossible to log in through an unencrypted
|
||||
connection.
|
||||
control and better security, thankfully handing authentication
|
||||
off to the httpd.
|
||||
|
||||
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,
|
||||
poobrains will handle that for you.
|
||||
One downside to this is that you will need to maintain a Certificate
|
||||
Authority, but unless you need to integrate it into an existing CA,
|
||||
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 ###
|
||||
|
||||
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,
|
||||
you'll feel right at home.
|
||||
|
||||
poobrains' permission system supports permissions assigned
|
||||
to users as well as groups and – to add icing on the cake –
|
||||
it's also extensible.
|
||||
It also supports permissions assigned to users as well as groups.
|
||||
|
||||
To add icing on the cake, it's also extensible.
|
||||
|
||||
|
||||
### All mails GPG-encrypted ###
|
||||
|
||||
The mail system has been written such that it doesn't even know how
|
||||
to send unencrypted mails. Be thankful for that, wrangling GPG is a
|
||||
horrible 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.
|
||||
The mail system has been written such that it doesn't even know how to
|
||||
send unencrypted mails. Be thankful for that, wrangling GPG is a horrible
|
||||
fucking experience.
|
||||
|
||||
|
||||
## Usability is still a big priority ##
|
||||
|
||||
Yes, the aforementioned things impose some limits you don't usually
|
||||
have. This is not without reason tho and a great deal of work went
|
||||
(and will continue to go) into making poobrains usable.
|
||||
Yes, the aforementioned things impose some limits you don't usually have.
|
||||
This is not without reason tho and a great deal of work went (and will
|
||||
continue to go) into making poobrains usable.
|
||||
|
||||
The default theme makes extensive use of HTML5 semantics and is
|
||||
fully responsive down to about 320x480.
|
||||
The default theme makes extensive use of HTML5 semantics and is fully
|
||||
responsive down to about 320x480.
|
||||
|
||||
But poobrains commitment to usability does not stop with the web user;
|
||||
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?
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
#!/usr/bin/env python2.7
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import poobrains
|
||||
app = poobrains.app
|
||||
|
||||
|
||||
@app.export('/post/')
|
||||
class Post(poobrains.auth.Administerable):
|
||||
|
||||
title = poobrains.storage.fields.CharField()
|
||||
text = poobrains.storage.fields.MarkdownField()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
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
|
||||
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 ##
|
||||
@ -203,19 +181,13 @@ actually get this thing running?
|
||||
While poobrains python dependencies are taken care of by pip, there are a few
|
||||
external dependencies you'll need:
|
||||
|
||||
* Python-3.6+
|
||||
- python-2.7
|
||||
|
||||
* pip
|
||||
- pip for python-2.7
|
||||
|
||||
* GnuPG
|
||||
* Last tested with 2.2.27 – beware of dragons – might be
|
||||
replaced with sequoia/p≡p in the future.
|
||||
|
||||
* [PROJ]
|
||||
* Needed for map rendering.
|
||||
|
||||
* [NaCl]
|
||||
* Needed for session encryption.
|
||||
- gnupg-2.0
|
||||
- The 2.0 part is important, since 2.1 has the habit of adding
|
||||
new output codes that libraries don't handle *\*shakes fist at gpg\**
|
||||
|
||||
- an httpd that supports TLS client certificate authentication
|
||||
- When in doubt, choose nginx
|
||||
@ -226,11 +198,8 @@ external dependencies you'll need:
|
||||
is for it to pass TLS client certificate authentication
|
||||
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
|
||||
with the above code there, so you have a structure like:
|
||||
@ -252,24 +221,20 @@ Change to the created directory:
|
||||
`cd site`
|
||||
|
||||
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
|
||||
to use python libraries installed on your system. `myvenv` is the name
|
||||
of the directory created for the venv. You can freely choose another
|
||||
name, but we're sticking to `myvenv` in this guide.
|
||||
`--system-site-packages` is not needed, but will enable your virtualenv to use
|
||||
python libraries installed on your system.
|
||||
|
||||
Activate the virtualenv:
|
||||
`source myvenv/bin/activate[.yourshell]`
|
||||
> The optional `.yourshell` is only needed if you use a non-bash
|
||||
> shell like csh or fish.
|
||||
`source bin/activate[.yourshell]`
|
||||
|
||||
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 ###
|
||||
|
||||
`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.
|
||||
|
||||
@ -279,7 +244,7 @@ This will install poobrains and all needed python dependencies.
|
||||
The installation procedure is called through your sites CLI.
|
||||
|
||||
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.)
|
||||
|
||||
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,
|
||||
but obviously that's more work.
|
||||
|
||||
In the case of `uwsgi+nginx`, you'll see files called `site.nginx.conf`
|
||||
as well as `site.ini` have appeared next to your `site.py` after the
|
||||
installation procedure went through.
|
||||
In this case, you'll see files called `site.nginx.conf` as well as `site.ini`
|
||||
have appeared next to your site.py after the installation procedure went through.
|
||||
|
||||
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
|
||||
add the `server` directive in there and ignore the caching stuff in
|
||||
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
|
||||
(`[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.
|
||||
|
||||
|
||||
### 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:
|
||||
|
||||
`chgrp -R www .`
|
||||
@ -349,8 +313,8 @@ Assuming the httpd will run as group `www`, do:
|
||||
|
||||
### Starting up ###
|
||||
|
||||
Everything is in place. Start up nginx and uwsgi, using `service`,
|
||||
`systemctl` or whatever contraption your OS uses.
|
||||
Everything is in place. Start up nginx and uwsgi, using `service`, `systemctl`
|
||||
or whatever contraption your OS uses.
|
||||
|
||||
|
||||
## First visit and client certificate ##
|
||||
@ -440,7 +404,7 @@ to the project.
|
||||
Just getting the markdown of the `text` field rendered can be done
|
||||
by creating this template file under `site/themes/default/post.jinja`:
|
||||
|
||||
```jinja
|
||||
```
|
||||
{% extends "administerable.jinja" %}
|
||||
|
||||
{% block content %}
|
||||
@ -494,7 +458,7 @@ enter the Admin Area, select "UserPermission" and then
|
||||
Enter "anonymous" as user (the text field does auto-completion).
|
||||
|
||||
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.
|
||||
|
||||
@ -510,12 +474,10 @@ working toward the first alpha version.
|
||||
|
||||
Amongst the features we didn't even talk about here:
|
||||
|
||||
* instance-specific permissions
|
||||
* hierarchic tags
|
||||
* threadable comments
|
||||
* SCSS integration
|
||||
* cron jobs
|
||||
* templatable SVG
|
||||
* honors themes by means of the SCSS integration
|
||||
* data analysis
|
||||
* data visualization
|
||||
- instance-specific permissions
|
||||
- hierarchic tags
|
||||
- threadable comments
|
||||
- SCSS integration
|
||||
- templatable SVG
|
||||
- this includes dataset plots and simple maps
|
||||
- honors themes by means of the SCSS integration
|
||||
|
@ -1,3 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from click.exceptions import BadParameter
|
||||
|
||||
class SessionError(Exception):
|
||||
@ -32,7 +34,7 @@ class CompoundError(ValidationError):
|
||||
|
||||
@property
|
||||
def message(self):
|
||||
msg = f"There were {len(self.errors)} errors."
|
||||
msg = "There were %d errors." % len(self.errors)
|
||||
|
||||
for error in self.errors:
|
||||
msg += "\n"+str(error)
|
||||
|
@ -1,3 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# external imports
|
||||
import time
|
||||
import copy
|
||||
@ -337,16 +339,16 @@ class BaseForm(poobrains.rendering.Renderable, metaclass=FormMeta):
|
||||
name = x.__name__.lower()
|
||||
|
||||
if issubclass(x, BaseForm):
|
||||
tpls.append(f'form/{name}.jinja')
|
||||
tpls.append('form/%s.jinja' % name)
|
||||
|
||||
if mode:
|
||||
tpls.append(f'form/{name}-{mode}.jinja')
|
||||
tpls.append('form/%s-%s.jinja' % (name, mode))
|
||||
|
||||
else:
|
||||
tpls.append(f'{name}.jinja')
|
||||
tpls.append('%s.jinja' % name)
|
||||
|
||||
if mode:
|
||||
tpls.append(f'{name}-{mode}.jinja')
|
||||
tpls.append('%s-%s.jinja' % (name, mode))
|
||||
|
||||
return tpls
|
||||
|
||||
@ -408,7 +410,7 @@ class Form(BaseForm):
|
||||
|
||||
The easiest is just subclassing, like:
|
||||
|
||||
``` python
|
||||
```
|
||||
@app.expose('/newperson')
|
||||
class MyPersonForm(poobrains.form.Form):
|
||||
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,
|
||||
you'll also have to implement the `process` function.
|
||||
To expand on the example above, this would look like:
|
||||
``` python
|
||||
```
|
||||
def process(self, submit):
|
||||
|
||||
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.
|
||||
|
||||
This can be done either from outside:
|
||||
``` python
|
||||
```
|
||||
form = poobrains.form.Form()
|
||||
form.foo = poobrains.form.Button('submit', label='PUSH ME')
|
||||
```
|
||||
|
||||
or from inside by subclassing again:
|
||||
``` python
|
||||
```
|
||||
class MyForm(poobrains.form.Form):
|
||||
def __init__(self, **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…
|
||||
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')
|
||||
|
||||
return self
|
||||
@ -565,7 +567,7 @@ class Fieldset(BaseForm):
|
||||
def __setattr__(self, name, value):
|
||||
|
||||
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)
|
||||
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Form field definitions.
|
||||
|
||||
@ -110,7 +112,7 @@ class BaseField(object, metaclass=poobrains.helpers.MetaCompatibility):
|
||||
|
||||
self.form = form
|
||||
if self.form.prefix:
|
||||
self.prefix = f"{self.form.prefix}.{self.form.name}"
|
||||
self.prefix = "%s.%s" % (self.form.prefix, self.form.name)
|
||||
else:
|
||||
self.prefix = self.form.name
|
||||
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):
|
||||
|
||||
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)
|
||||
|
||||
@ -134,17 +136,17 @@ class BaseField(object, metaclass=poobrains.helpers.MetaCompatibility):
|
||||
|
||||
if issubclass(x, Field):
|
||||
|
||||
tpls.append(f'form/fields/{name}.jinja')
|
||||
tpls.append('form/fields/%s.jinja' % name)
|
||||
|
||||
if mode:
|
||||
tpls.append(f'form/fields/{name}-{mode}.jinja')
|
||||
tpls.append('form/fields/%s-%s.jinja' % (name, mode))
|
||||
|
||||
else:
|
||||
|
||||
tpls.append(f'{name}.jinja')
|
||||
tpls.append('%s.jinja' % name)
|
||||
|
||||
if mode:
|
||||
tpls.append(f'{name}-{mode}.jinja')
|
||||
tpls.append('%s-%s.jinja' % (name, mode))
|
||||
|
||||
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. """
|
||||
if self.choices:
|
||||
return f"list-{self.ref_id}"
|
||||
return "list-%s" % self.ref_id
|
||||
|
||||
return False # no choices means no datalist
|
||||
|
||||
@ -243,7 +245,7 @@ class BaseField(object, metaclass=poobrains.helpers.MetaCompatibility):
|
||||
for value in values:
|
||||
|
||||
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
|
||||
for validator in self.validators:
|
||||
@ -253,7 +255,7 @@ class BaseField(object, metaclass=poobrains.helpers.MetaCompatibility):
|
||||
compound_error.append(e)
|
||||
|
||||
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):
|
||||
raise compound_error
|
||||
@ -281,7 +283,7 @@ class BaseField(object, metaclass=poobrains.helpers.MetaCompatibility):
|
||||
|
||||
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)
|
||||
self.errors.append(e)
|
||||
|
||||
@ -292,7 +294,7 @@ class BaseField(object, metaclass=poobrains.helpers.MetaCompatibility):
|
||||
|
||||
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)
|
||||
self.errors.append(e)
|
||||
|
||||
@ -321,7 +323,7 @@ class BaseField(object, metaclass=poobrains.helpers.MetaCompatibility):
|
||||
return 'true' if value == True else 'false'
|
||||
|
||||
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):
|
||||
return value.__name__
|
||||
@ -387,7 +389,7 @@ class Number(Field):
|
||||
if not self.empty:
|
||||
|
||||
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):
|
||||
@ -402,7 +404,7 @@ class Message(Field):
|
||||
abstract = True
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__}: {self.value}>"
|
||||
return "<%s: %s>" % (self.__class__.__name__, self.value)
|
||||
|
||||
def validate(self):
|
||||
pass
|
||||
@ -545,8 +547,3 @@ class File(Field):
|
||||
|
||||
class Meta:
|
||||
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
|
||||
|
@ -1,3 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
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
|
||||
|
||||
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.")
|
||||
|
||||
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()
|
||||
|
||||
@ -49,29 +51,29 @@ class DateTimeParamType(ParamType):
|
||||
return value # apparently we need this function to be idempotent.
|
||||
|
||||
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:
|
||||
|
||||
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.")
|
||||
|
||||
else:
|
||||
|
||||
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:
|
||||
|
||||
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.")
|
||||
|
||||
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()
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Abstractions for complex, session-based form applications.
|
||||
"""
|
||||
|
@ -1,3 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import string
|
||||
import random
|
||||
import inspect
|
||||
@ -263,7 +265,7 @@ def pretty_bytes(bytecount):
|
||||
|
||||
value /= 1024.0
|
||||
|
||||
return f"{value:.2f} {unit}"
|
||||
return "%.2f %s" % (value, unit)
|
||||
|
||||
|
||||
def levenshtein_distance(s1, s2):
|
||||
@ -329,7 +331,6 @@ class FakeMetaOptions(object):
|
||||
modes = None
|
||||
permission_class = None
|
||||
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__
|
||||
|
||||
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.
|
||||
|
||||
"""
|
||||
|
||||
tiered = OrderedDict()
|
||||
@ -497,13 +499,13 @@ class CustomOrderedDict(dict):
|
||||
|
||||
def __repr__(self):
|
||||
|
||||
r = '{'
|
||||
repr = '{'
|
||||
|
||||
for k, v in self.items():
|
||||
r += f"{repr(k)}: {repr(v)}"
|
||||
repr += '%s: %s' % (k.__repr__(), v.__repr__())
|
||||
|
||||
r += '}'
|
||||
return r
|
||||
repr += '}'
|
||||
return repr
|
||||
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
@ -646,10 +648,13 @@ class ASVWriter(object):
|
||||
|
||||
|
||||
def __init__(self, filepath):
|
||||
|
||||
self.fd = codecs.open(filepath, 'a', encoding='utf-8')
|
||||
|
||||
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):
|
||||
self.fd.close()
|
||||
@ -671,7 +676,13 @@ class TypedList(list):
|
||||
def append(self, value):
|
||||
|
||||
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)
|
||||
|
||||
@ -685,6 +696,12 @@ class TypedList(list):
|
||||
def insert(self, index, value):
|
||||
|
||||
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)
|
||||
|
@ -1,3 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.image import MIMEImage
|
||||
|
@ -1,3 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import collections
|
||||
import peewee
|
||||
import jinja2
|
||||
@ -116,7 +118,7 @@ class DisplayRenderable(markdown.inlinepatterns.Pattern):
|
||||
instance.permissions['read'].check(flask.g.user)
|
||||
|
||||
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:
|
||||
return instance.render('inline')
|
||||
elif 'teaser' in instance._meta.modes:
|
||||
@ -128,10 +130,10 @@ class DisplayRenderable(markdown.inlinepatterns.Pattern):
|
||||
|
||||
except Exception as 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:
|
||||
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)
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import markdown
|
||||
|
||||
class MagicDict(dict):
|
||||
@ -66,7 +68,7 @@ class MagicDict(dict):
|
||||
return (url, title)
|
||||
|
||||
except:
|
||||
raise KeyError(f"Couldn't load '{storable}/{handle}'.")
|
||||
raise KeyError("Couldn't load '%s/%s'." % (storable, handle))
|
||||
|
||||
|
||||
return super(MagicDict, self).__getitem__(key)
|
||||
|
@ -1,3 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import collections
|
||||
import OpenSSL as openssl
|
||||
|
||||
@ -30,7 +32,7 @@ class Dashbar(poobrains.rendering.Container):
|
||||
|
||||
try:
|
||||
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:
|
||||
pass
|
||||
|
||||
@ -58,7 +60,7 @@ class Dashbar(poobrains.rendering.Container):
|
||||
if notification_count == 1:
|
||||
menu.append(NotificationControl.url('full', handle=self.user.handle_string), '1 unread notification')
|
||||
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:
|
||||
pass
|
||||
|
||||
@ -176,7 +178,7 @@ class PGPForm(poobrains.form.Form):
|
||||
|
||||
super(PGPForm, self).__init__(**kwargs)
|
||||
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']
|
||||
|
||||
|
||||
@ -198,8 +200,8 @@ class PGPForm(poobrains.form.Form):
|
||||
|
||||
else:
|
||||
# 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')
|
||||
app.logger.error(f"GPG key import error: {result.stderr}")
|
||||
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("GPG key import error: %s" % result.stderr)
|
||||
|
||||
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)
|
||||
|
||||
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.save()
|
||||
|
||||
elif self.controls['delete'].value:
|
||||
flask.flash(f"Deleting notification {instance.id}.")
|
||||
flask.flash(u"Deleting notification %d." % instance.id)
|
||||
instance.delete_instance()
|
||||
|
||||
return self
|
||||
|
@ -1,3 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import collections
|
||||
import os
|
||||
import flask
|
||||
@ -13,10 +15,6 @@ import poobrains.helpers
|
||||
|
||||
class Renderable(poobrains.helpers.ChildAware):
|
||||
|
||||
"""
|
||||
Base class for all renderable content.
|
||||
"""
|
||||
|
||||
handle_string = None # string which uniquely identifies each instance of a Renderable
|
||||
|
||||
name = None
|
||||
@ -79,9 +77,9 @@ class Renderable(poobrains.helpers.ChildAware):
|
||||
name = x.__name__.lower()
|
||||
|
||||
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
|
||||
|
||||
@ -306,7 +304,7 @@ class TableRow(object):
|
||||
if key.lower() in columns_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):
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import functools
|
||||
import collections
|
||||
import math
|
||||
@ -104,9 +106,9 @@ class Search(poobrains.auth.Protected):
|
||||
clauses = []
|
||||
|
||||
if isinstance(app.db, peewee.SqliteDatabase):
|
||||
term = f"*{self.handle_string.lower()}*"
|
||||
term = '*%s*' % self.handle_string.lower()
|
||||
else: # postgres
|
||||
term = f"%{self.handle_string.lower()}%"
|
||||
term = '%%%s%%' % self.handle_string.lower()
|
||||
|
||||
if hasattr(administerable._meta, 'search_fields'):
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# external imports
|
||||
import math
|
||||
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?
|
||||
|
||||
if 'sqlite' in app.db.__class__.__name__.lower():
|
||||
regexp_compat = f'"{regexp}"'
|
||||
regexp_compat = '"%s"' % regexp
|
||||
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):
|
||||
@ -116,7 +118,7 @@ class Model(peewee.Model, poobrains.helpers.ChildAware, metaclass=ModelBase):
|
||||
elif type(handle) not in (tuple, list):
|
||||
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:
|
||||
field = getattr(cls, field_name)
|
||||
@ -175,9 +177,9 @@ class Model(peewee.Model, poobrains.helpers.ChildAware, metaclass=ModelBase):
|
||||
|
||||
def __repr__(self):
|
||||
try:
|
||||
return f"<{self.__class__.__name__}[{self._pk}]>"
|
||||
return "<%s[%s]>" % (self.__class__.__name__, self._pk)
|
||||
except Exception:
|
||||
return f"<{self.__class__.__name__}, unsaved/no primary key>"
|
||||
return "<%s, unsaved/no primary key>" % self.__class__.__name__
|
||||
|
||||
|
||||
class SessionData(Model):
|
||||
@ -223,12 +225,12 @@ class Storable(Model, poobrains.rendering.Renderable):
|
||||
return self.name
|
||||
|
||||
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:
|
||||
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 ''
|
||||
|
||||
def instance_url(self, mode='full', quiet=False, **url_params):
|
||||
@ -298,7 +300,7 @@ class Named(Storable):
|
||||
@property
|
||||
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):
|
||||
@ -345,7 +347,7 @@ class Listing(poobrains.rendering.Renderable):
|
||||
|
||||
endpoint = flask.request.endpoint
|
||||
if not endpoint.endswith('_offset'):
|
||||
endpoint = f'{endpoint}_offset'
|
||||
endpoint = '%s_offset' % (endpoint,)
|
||||
|
||||
self.pagination = Pagination([query], offset, endpoint, **pagination_options)
|
||||
self.items = self.pagination.results
|
||||
@ -360,10 +362,10 @@ class Listing(poobrains.rendering.Renderable):
|
||||
|
||||
if mode:
|
||||
if issubclass(x, Listing):
|
||||
tpls.append(f'{name}-{mode}-{self.cls.__name__}.jinja')
|
||||
tpls.append(f'{name}-{mode}.jinja')
|
||||
tpls.append('%s-%s-%s.jinja' % (name, mode, self.cls.__name__))
|
||||
tpls.append('%s-%s.jinja' % (name, mode))
|
||||
|
||||
tpls.append(f'{name}.jinja')
|
||||
tpls.append('%s.jinja' % name)
|
||||
|
||||
return tpls
|
||||
|
||||
@ -478,7 +480,7 @@ class StorableParamType(poobrains.form.types.ParamType):
|
||||
if value.lower() in storables:
|
||||
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.STORABLE = StorableParamType()
|
||||
@ -487,4 +489,4 @@ poobrains.form.types.STORABLE = StorableParamType()
|
||||
@app.cron
|
||||
def bury_sessions():
|
||||
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')
|
||||
|
@ -1,3 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# external imports
|
||||
import typing
|
||||
import json
|
||||
@ -65,10 +67,10 @@ class StorableInstanceParamType(poobrains.form.types.ParamType):
|
||||
|
||||
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:
|
||||
self.fail(f"'{value}' is not an approved choice.")
|
||||
self.fail("'%s' is not an approved choice." % value)
|
||||
|
||||
return instance
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import collections
|
||||
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…
|
||||
_css_cache = None
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
modes = collections.OrderedDict([
|
||||
@ -23,7 +25,7 @@ class SVG(poobrains.auth.Protected):
|
||||
('raw', 'read'),
|
||||
('inline', 'read')
|
||||
])
|
||||
|
||||
|
||||
style = None
|
||||
|
||||
def __init__(self, handle=None, mode=None, **kwargs):
|
||||
@ -36,16 +38,16 @@ class SVG(poobrains.auth.Protected):
|
||||
else:
|
||||
self.style = Markup(app.scss_compiler.compile_string("@import 'svg';"))
|
||||
self.__class__._css_cache = self.style
|
||||
|
||||
|
||||
|
||||
|
||||
def templates(self, mode=None):
|
||||
|
||||
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):
|
||||
|
||||
|
||||
url_params['handle'] = self.handle
|
||||
|
||||
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):
|
||||
|
||||
if mode == 'raw':
|
||||
|
||||
|
||||
response = Response(self.render('raw'))
|
||||
response.headers['Content-Type'] = 'image/svg+xml'
|
||||
response.headers['Content-Disposition'] = f'filename="{self.__class__.__name__}.svg"'
|
||||
|
||||
response.headers['Content-Type'] = u'image/svg+xml'
|
||||
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
|
||||
response.cache_control.public = False
|
||||
response.cache_control.private = True
|
||||
response.cache_control.max_age = app.config['CACHE_LONG']
|
||||
|
||||
return response
|
||||
|
||||
|
||||
else:
|
||||
return poobrains.helpers.ThemedPassthrough(super(SVG, self).view(mode=mode, handle=handle))
|
||||
|
||||
|
||||
@app.setup
|
||||
@app.before_first_request
|
||||
def register_svg_raw():
|
||||
for cls in set(SVG.class_children()):
|
||||
rule = os.path.join("/svg/", cls.__name__.lower(), '<handle>', 'raw')
|
||||
|
@ -1,3 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import string
|
||||
import statistics # for mean
|
||||
import colorsys
|
||||
@ -97,7 +99,17 @@ class Color(object):
|
||||
self._update_lightness()
|
||||
|
||||
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):
|
||||
return f"rgba({int(self.red * 255)}, {int(self.green * 255)}, {int(self.blue * 255)}, {self.alpha})"
|
||||
|
@ -1,3 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import scss
|
||||
|
||||
import poobrains.helpers
|
||||
|
@ -1,3 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
""" The tagging system. """
|
||||
|
||||
import collections
|
||||
@ -57,7 +59,7 @@ class Tag(poobrains.auth.ProtectedNamed):
|
||||
if current_depth > 100:
|
||||
|
||||
if root:
|
||||
message = f"Possibly incestuous tag: '{root.name}'."
|
||||
message = "Possibly incestuous tag: '%s'." % root.name
|
||||
else:
|
||||
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:
|
||||
model = poobrains.storage.Storable.class_children_keyed()[model_name]
|
||||
except KeyError:
|
||||
app.logger.error(f"TagBinding for unknown model: {model_name}")
|
||||
app.logger.error("TagBinding for unknown model: %s" % model_name)
|
||||
continue
|
||||
|
||||
handles = [model.string_handle(binding.handle) for binding in bindings]
|
||||
@ -148,7 +150,7 @@ class Tag(poobrains.auth.ProtectedNamed):
|
||||
def validate(self):
|
||||
|
||||
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):
|
||||
@ -187,7 +189,7 @@ class TaggingField(poobrains.form.fields.Select):
|
||||
choices.append((tag, tag.name))
|
||||
|
||||
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['type'] = poobrains.form.types.StorableInstanceParamType(Tag)
|
||||
|
@ -85,10 +85,10 @@ def fill_valid(instance):
|
||||
if isinstance(instance, poobrains.storage.Named) and attr_name == 'name':
|
||||
setattr(instance, attr_name, generators[fieldmap[field_class]]().lower())
|
||||
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:
|
||||
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:
|
||||
setattr(instance, attr_name, generators[fieldmap[field_class]]())
|
||||
|
||||
@ -207,7 +207,7 @@ y
|
||||
if name.isupper():
|
||||
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):
|
||||
@ -220,7 +220,7 @@ def test_cli_minica(client):
|
||||
def test_cert_page(client):
|
||||
|
||||
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):
|
||||
@ -239,7 +239,7 @@ def test_redeem_token(client):
|
||||
try:
|
||||
OpenSSL.crypto.load_pkcs12(rv.data, passphrase)
|
||||
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
|
||||
@pytest.mark.parametrize('cls', storables_to_test)
|
||||
@ -256,19 +256,19 @@ def test_crud(client, cls):
|
||||
|
||||
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:
|
||||
instance = cls.load(instance.handle_string) # reloads instance from database, making sure Read works
|
||||
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
|
||||
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
|
||||
@ -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)
|
||||
|
||||
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)
|
||||
|
||||
instance = cls()
|
||||
@ -306,7 +306,7 @@ def test_permission_grant(client, cls, permission_holder, op_info):
|
||||
else: # 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)
|
||||
|
||||
ug = poobrains.auth.UserGroup()
|
||||
@ -333,7 +333,7 @@ def test_permission_grant(client, cls, permission_holder, op_info):
|
||||
try:
|
||||
instance.permissions[op].check(u)
|
||||
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])
|
||||
@ -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)
|
||||
|
||||
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)
|
||||
|
||||
instance = cls()
|
||||
@ -366,7 +366,7 @@ def test_permission_deny(client, cls, permission_holder, op_info):
|
||||
else: # 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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
instance = cls()
|
||||
@ -427,7 +427,7 @@ def test_ownedpermission_instance(client, cls, permission_holder, op_info):
|
||||
else: # 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)
|
||||
|
||||
ug = poobrains.auth.UserGroup()
|
||||
@ -464,7 +464,7 @@ def test_ownedpermission_instance(client, cls, permission_holder, op_info):
|
||||
try:
|
||||
instance.permissions[op].check(u)
|
||||
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])
|
||||
@ -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)
|
||||
|
||||
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 = 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
|
||||
@ -502,7 +502,7 @@ def test_ownedpermission_own_instance(client, cls, permission_holder, op_info):
|
||||
else: # 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)
|
||||
|
||||
ug = poobrains.auth.UserGroup()
|
||||
@ -539,7 +539,7 @@ def test_ownedpermission_own_instance(client, cls, permission_holder, op_info):
|
||||
try:
|
||||
instance.permissions[op].check(u)
|
||||
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():
|
||||
@ -569,12 +569,12 @@ def run_all():
|
||||
pass
|
||||
|
||||
try:
|
||||
os.unlink(f'{poobrains.project_name}.ini')
|
||||
os.unlink('%s.ini' % poobrains.project_name)
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
os.unlink(f'{poobrains.project_name}.nginx.conf')
|
||||
os.unlink('%s.nginx.conf' % poobrains.project_name)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
$color_background_dark: rgba(8,8,8, 0.85);
|
||||
$color_background_light: rgba(255,255,255, 0.85);
|
||||
$color_highlight: rgba(0,128,255, 0.85);
|
||||
$color_background_dark: rgba(8,8,8, 0.8);
|
||||
$color_background_light: rgba(255,255,255, 0.8);
|
||||
$color_highlight: rgba(0,128,255, 0.8);
|
||||
/*$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_light: opacify($color_background_light, 10%);
|
||||
$color_background_form: opacify($color_background_light, -0.3);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -72,7 +72,6 @@ body.content-type-dataeditor {
|
||||
flex-flow: row wrap;
|
||||
|
||||
background: transparent;
|
||||
backdrop-filter: none;
|
||||
|
||||
.display-wrapper,
|
||||
.form-wrapper {
|
||||
@ -87,7 +86,6 @@ body.content-type-dataeditor {
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
backdrop-filter: none;
|
||||
|
||||
section.tab {
|
||||
overflow-y: auto;
|
||||
@ -138,7 +136,6 @@ body.content-type-dataeditor {
|
||||
padding: 0;
|
||||
min-height: 100%;
|
||||
background: transparent;
|
||||
backdrop-filter: none;
|
||||
border: none;
|
||||
|
||||
& > header,
|
||||
@ -163,7 +160,6 @@ body.content-type-dataeditor {
|
||||
|
||||
section.tab {
|
||||
background: $color_background_dark;
|
||||
backdrop-filter: blur($backdrop_blur);
|
||||
height: calc(100vh - 3rem);
|
||||
|
||||
fieldset.action-control {
|
||||
@ -174,7 +170,6 @@ body.content-type-dataeditor {
|
||||
margin: 0;
|
||||
border: 0 none;
|
||||
background: transparent;
|
||||
backdrop-filter: none;
|
||||
|
||||
& > header {
|
||||
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 */
|
||||
|
||||
background: transparent;
|
||||
backdrop-filter: none;
|
||||
|
||||
& > header {
|
||||
display: none;
|
||||
@ -268,7 +262,6 @@ body.content-type-dataeditor {
|
||||
|
||||
& > fieldset {
|
||||
background: transparent;
|
||||
backdrop-filter: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@
|
||||
{% include 'head.jinja' %}
|
||||
</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 %}
|
||||
{{ g.boxes.dashbar.render() }}
|
||||
|
@ -104,8 +104,7 @@
|
||||
@media (min-width: 56rem) {
|
||||
|
||||
#logo-link {
|
||||
display: block !important;
|
||||
padding: 4rem 0;
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
body > header {
|
||||
@ -114,6 +113,7 @@
|
||||
.sticky {
|
||||
width: 13rem;
|
||||
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%;
|
||||
min-height: 100vh;
|
||||
/*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');
|
||||
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/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-position: center;
|
||||
background-attachment: fixed;
|
||||
@ -225,18 +225,24 @@ body {
|
||||
flex-basis: 15rem;
|
||||
flex-shrink: 1;
|
||||
flex-grow: 15;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: stretch;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
max-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
padding: 1rem;
|
||||
z-index: 200;
|
||||
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);*/
|
||||
|
||||
overflow: auto;
|
||||
overflow: hidden;
|
||||
|
||||
&:nth-child(2) { /* means dashbar is rendered */
|
||||
margin-top: 3rem;
|
||||
@ -312,8 +318,6 @@ body {
|
||||
}
|
||||
|
||||
#logo {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
height: 7.5rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
@ -479,10 +483,7 @@ main {
|
||||
flex-direction: column;
|
||||
|
||||
*:target {
|
||||
/*scroll-margin-top: 15rem; */ /* offset :target'ed items to avoid having them under the sticky header */
|
||||
body.dashbar & {
|
||||
scroll-margin-top: 4rem;
|
||||
}
|
||||
scroll-margin-top: 15rem; /* offset :target'ed items to avoid having them under the sticky header */
|
||||
display: block;
|
||||
}
|
||||
|
||||
@ -511,7 +512,6 @@ main {
|
||||
div.content,
|
||||
section.controls {
|
||||
background: transparent;
|
||||
backdrop-filter: none;
|
||||
}
|
||||
|
||||
section.controls {
|
||||
@ -843,6 +843,10 @@ form {
|
||||
|
||||
& > div.content {
|
||||
background: $color_background_dark;
|
||||
|
||||
h2, h3, h4, h5, h6 {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -860,7 +864,6 @@ article {
|
||||
&.mode-inline {
|
||||
& > .content {
|
||||
background: transparent;
|
||||
backdrop-filter: none;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
@ -945,13 +948,11 @@ article {
|
||||
|
||||
& > div.content {
|
||||
background: transparent;
|
||||
backdrop-filter: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
& > footer {
|
||||
background: transparent !important;
|
||||
backdrop-filter: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@ -959,7 +960,6 @@ article {
|
||||
article > .content {
|
||||
/* nested containers shouldn't stack background color on top of each other */
|
||||
background: transparent;
|
||||
backdrop-filter: none;
|
||||
}
|
||||
}
|
||||
|
||||
@ -983,21 +983,18 @@ article {
|
||||
|
||||
form > footer {
|
||||
background: transparent;
|
||||
backdrop-filter: none;
|
||||
}
|
||||
}
|
||||
|
||||
& > div.content {
|
||||
|
||||
background: transparent;
|
||||
backdrop-filter: none;
|
||||
padding: 0;
|
||||
|
||||
}
|
||||
|
||||
& > footer {
|
||||
background: transparent !important;
|
||||
backdrop-filter: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1336,6 +1333,5 @@ span.debug-hint {
|
||||
@import 'modal';
|
||||
@import 'tabs';
|
||||
@import 'optin';
|
||||
@import 'documentation';
|
||||
@import 'editor';
|
||||
@import 'custom';
|
||||
|
@ -3,10 +3,6 @@
|
||||
$palette_size: 4;
|
||||
$palette_rotation: 180; /* overall hue rotation of the palette */
|
||||
$palette: ();
|
||||
|
||||
$backdrop_blur: 3px;
|
||||
|
||||
|
||||
/*@for $i from 1 through $palette_size {
|
||||
$palette: append($palette, adjust_hue($color_highlight, ($palette_rotation/$palette_size) * ($i - 1)));
|
||||
}*/
|
||||
|
@ -69,14 +69,14 @@ svg#hexascroll {
|
||||
animation: decorotion 5s linear infinite;
|
||||
|
||||
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 {
|
||||
fill: opacify($color_background_dark, -0.2);
|
||||
stroke: darken($color_highlight, 30%);
|
||||
stroke: $color_background_light;
|
||||
stroke-width: 2;
|
||||
|
||||
&.hexagon {
|
||||
|
@ -18,7 +18,6 @@
|
||||
cursor: pointer;
|
||||
padding: 0 1rem;
|
||||
background: $color_background_dark;
|
||||
backdrop-filter: blur($backdrop_blur);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import collections
|
||||
import peewee
|
||||
@ -46,7 +48,7 @@ class UploadForm(poobrains.auth.AutoForm):
|
||||
not extension in self.instance.extension_whitelist:
|
||||
raise poobrains.errors.CompoundError(
|
||||
[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
|
||||
filename = self.fields['filename'].value
|
||||
|
||||
if filename != '':
|
||||
if filename is not '':
|
||||
|
||||
file_path = os.path.join(self.instance.path, filename)
|
||||
|
||||
@ -65,19 +67,19 @@ class UploadForm(poobrains.auth.AutoForm):
|
||||
|
||||
if force:
|
||||
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
|
||||
file_path = os.path.join(self.instance.path, filename)
|
||||
|
||||
else:
|
||||
try:
|
||||
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:
|
||||
|
||||
flask.flash("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}")
|
||||
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('Unknown file clutter: %s/upload/%s/%s' % (app.root_path, self.instance.path, filename))
|
||||
|
||||
return self
|
||||
|
||||
@ -89,13 +91,12 @@ class UploadForm(poobrains.auth.AutoForm):
|
||||
try:
|
||||
os.remove(self.instance.file_path)
|
||||
except OSError as e:
|
||||
app.logger.debug(f"Could not delete old file '{self.filename}' for {self.__class__.__name__} '{self.name}': ")
|
||||
# FIXME: Doesn't the : imply there should be more info following? e.message?
|
||||
app.logger.debug(u"Could not delete old file '%s' for %s '%s': " % (self.filename, self.__class__.__name__, self.name))
|
||||
|
||||
except IOError as e:
|
||||
|
||||
flask.flash(f"Failed saving file '{filename}'.", 'error')
|
||||
app.logger.error(f"Failed saving file: {filename}\n{type(e).__name__}: {str(e)} / {e.strerror} / {e.filename}")
|
||||
flask.flash(u"Failed saving file '%s'." % filename, 'error')
|
||||
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
|
||||
|
||||
elif self.mode == 'edit':
|
||||
@ -106,9 +107,9 @@ class UploadForm(poobrains.auth.AutoForm):
|
||||
return super(UploadForm, self).process(submit, exceptions=True)
|
||||
|
||||
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))
|
||||
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)
|
||||
|
||||
return self
|
||||
@ -161,7 +162,7 @@ class File(poobrains.auth.NamedOwned):
|
||||
if mode == 'raw':
|
||||
|
||||
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
|
||||
response.cache_control.public = False
|
||||
@ -194,7 +195,7 @@ class File(poobrains.auth.NamedOwned):
|
||||
try:
|
||||
os.remove(os.path.join(self.path, self.filename))
|
||||
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
|
||||
|
||||
|
4
setup.py
4
setup.py
@ -16,11 +16,9 @@ setup(
|
||||
'pyScss',
|
||||
'pillow', # for image manipulation and generation (primarily to generate captchas)
|
||||
'markdown (>=3.0)',
|
||||
'pretty-bad-protocol', # formerly 'gnupg'
|
||||
'numpy',
|
||||
'pyproj', # map projection
|
||||
'pretty-bad-protocol', # formerly 'gnupg'
|
||||
'geojson',
|
||||
'Shapely',
|
||||
'bson',
|
||||
],
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user