Compare commits

..

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

42 changed files with 1008 additions and 1036 deletions

View File

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
""" A webframework for aspiring media terrorists. """
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'):

View File

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

View File

@ -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):

View File

@ -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

View File

@ -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')

View File

@ -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)

View File

@ -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):

View File

@ -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')

View File

@ -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] == '':

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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()

View File

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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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):

View File

@ -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'):

View File

@ -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')

View File

@ -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

View File

@ -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')

View File

@ -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})"

View File

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

View File

@ -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)

View File

@ -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

View File

@ -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);

View File

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

View File

@ -72,7 +72,6 @@ body.content-type-dataeditor {
flex-flow: row wrap;
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;
}
}
}

View File

@ -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() }}

View File

@ -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';

View File

@ -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)));
}*/

View File

@ -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 {

View File

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

View File

@ -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

View File

@ -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',
],