Compare commits

..

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

  1. 79
      poobrains/__init__.py
  2. 2
      poobrains/analysis/__init__.py
  3. 14
      poobrains/analysis/data.py
  4. 14
      poobrains/analysis/editor.py
  5. 69
      poobrains/analysis/sources.py
  6. 7
      poobrains/analysis/util.py
  7. 6
      poobrains/analysis/visualization.py
  8. 144
      poobrains/auth/__init__.py
  9. 67
      poobrains/cli/__init__.py
  10. 16
      poobrains/commenting.py
  11. 8
      poobrains/defaults.py
  12. 951
      poobrains/doc/__init__.py
  13. 160
      poobrains/doc/quickstart.md
  14. 4
      poobrains/errors.py
  15. 22
      poobrains/form/__init__.py
  16. 35
      poobrains/form/fields.py
  17. 16
      poobrains/form/types.py
  18. 2
      poobrains/formapp.py
  19. 34
      poobrains/helpers.py
  20. 2
      poobrains/mailing.py
  21. 8
      poobrains/md/__init__.py
  22. 4
      poobrains/md_default.py
  23. 16
      poobrains/profile.py
  24. 12
      poobrains/rendering.py
  25. 6
      poobrains/search.py
  26. 34
      poobrains/storage/__init__.py
  27. 6
      poobrains/storage/fields.py
  28. 8
      poobrains/svg/__init__.py
  29. 14
      poobrains/svg/color.py
  30. 2
      poobrains/svg/palettes.py
  31. 10
      poobrains/tagging.py
  32. 42
      poobrains/testing.py
  33. 8
      poobrains/themes/default/colors.scss
  34. 33
      poobrains/themes/default/documentation.scss
  35. 7
      poobrains/themes/default/editor.scss
  36. 2
      poobrains/themes/default/main.jinja
  37. 38
      poobrains/themes/default/main.scss
  38. 4
      poobrains/themes/default/palette.scss
  39. 4
      poobrains/themes/default/svg.scss
  40. 1
      poobrains/themes/default/tabs.scss
  41. 27
      poobrains/upload/__init__.py
  42. 4
      setup.py

79
poobrains/__init__.py

@ -1,3 +1,5 @@ @@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
""" A webframework for aspiring media terrorists. """
import os
@ -447,21 +449,13 @@ class Poobrain(flask.Flask): @@ -447,21 +449,13 @@ class Poobrain(flask.Flask):
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.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
@ -490,7 +484,7 @@ class Poobrain(flask.Flask): @@ -490,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
@ -504,7 +498,7 @@ class Poobrain(flask.Flask): @@ -504,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
@ -527,10 +521,10 @@ class Poobrain(flask.Flask): @@ -527,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)
@ -668,7 +662,7 @@ class Poobrain(flask.Flask): @@ -668,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:
@ -708,12 +702,12 @@ class Poobrain(flask.Flask): @@ -708,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>/'))
@ -773,18 +767,21 @@ class Poobrain(flask.Flask): @@ -773,18 +767,21 @@ class Poobrain(flask.Flask):
except LookupError:
pass
raise LookupError("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.")
@ -826,9 +823,9 @@ class Pooprint(flask.Blueprint): @@ -826,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
@ -872,7 +869,7 @@ class Pooprint(flask.Blueprint): @@ -872,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] = []
@ -893,7 +890,7 @@ class Pooprint(flask.Blueprint): @@ -893,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'
@ -955,7 +952,7 @@ class Pooprint(flask.Blueprint): @@ -955,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)
@ -995,8 +992,7 @@ class Pooprint(flask.Blueprint): @@ -995,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):
@ -1015,12 +1011,13 @@ class Pooprint(flask.Blueprint): @@ -1015,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:
@ -1049,12 +1046,12 @@ class Pooprint(flask.Blueprint): @@ -1049,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:
@ -1063,7 +1060,7 @@ class Pooprint(flask.Blueprint): @@ -1063,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)
@ -1078,12 +1075,12 @@ class Pooprint(flask.Blueprint): @@ -1078,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)
@ -1091,7 +1088,7 @@ class Pooprint(flask.Blueprint): @@ -1091,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':
@ -1100,16 +1097,16 @@ class Pooprint(flask.Blueprint): @@ -1100,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
@ -1167,7 +1164,7 @@ class ErrorPage(rendering.Renderable): @@ -1167,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)
@ -1190,7 +1187,7 @@ class ErrorPage(rendering.Renderable): @@ -1190,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'):

2
poobrains/analysis/__init__.py

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

14
poobrains/analysis/data.py

@ -1,3 +1,5 @@ @@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
import collections
import io
import re
@ -50,7 +52,7 @@ def load_dataset(handle): @@ -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): @@ -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): @@ -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): @@ -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)
@ -598,7 +600,7 @@ class Dataset(EphemeralDataset, poobrains.commenting.Commentable): @@ -598,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):

14
poobrains/analysis/editor.py

@ -1,3 +1,5 @@ @@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
import math
import re
@ -41,7 +43,7 @@ def datasource_choices(): @@ -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): @@ -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

69
poobrains/analysis/sources.py

@ -77,15 +77,12 @@ class FileFieldset(poobrains.formapp.MultistateFieldset): @@ -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): @@ -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): @@ -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,
}
flash("Changed CSV dialect options.")
else:
dialect = dialect_from_dict(self.session['csv_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]);
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,
}
self.parent.session['other'] = ds.to_dict(whole=True)
self.parent.state = 'merge'
flash("Changed CSV dialect options.")
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): @@ -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')

7
poobrains/analysis/util.py

@ -1,4 +1,3 @@ @@ -1,4 +1,3 @@
import functools
import datetime
import numpy
import shapely.geometry
@ -28,14 +27,12 @@ def pretty_si(number): @@ -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): @@ -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)

6
poobrains/analysis/visualization.py

@ -1,3 +1,5 @@ @@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
import math
import pyproj
@ -92,10 +94,10 @@ class Plot(poobrains.svg.SVG): @@ -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):

144
poobrains/auth/__init__.py

@ -1,3 +1,5 @@ @@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
"""
The authentication system.
@ -156,28 +158,28 @@ class AutoForm(BoundForm): @@ -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): @@ -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): @@ -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): @@ -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): @@ -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): @@ -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): @@ -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): @@ -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): @@ -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): @@ -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): @@ -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): @@ -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(): @@ -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(): @@ -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): @@ -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): @@ -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): @@ -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): @@ -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): @@ -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): @@ -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): @@ -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): @@ -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 @@ -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 @@ -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 @@ -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): @@ -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): @@ -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): @@ -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): @@ -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): @@ -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): @@ -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): @@ -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): @@ -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): @@ -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): @@ -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): @@ -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(): @@ -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. @@ -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. @@ -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