Browse Source

ripped plot definitions out of dataset classes and into their own models

master
phryk 2 days ago
parent
commit
c473420b8f
15 changed files with 260 additions and 219 deletions
  1. +3
    -5
      poobrains/__init__.py
  2. +37
    -46
      poobrains/analysis/data.py
  3. +130
    -77
      poobrains/analysis/editor.py
  4. +1
    -1
      poobrains/analysis/sources.py
  5. +52
    -52
      poobrains/analysis/visualization.py
  6. +4
    -8
      poobrains/auth/__init__.py
  7. +0
    -2
      poobrains/form/__init__.py
  8. +0
    -2
      poobrains/form/fields.py
  9. +10
    -3
      poobrains/helpers.py
  10. +0
    -2
      poobrains/storage/__init__.py
  11. +0
    -2
      poobrains/storage/fields.py
  12. +1
    -1
      poobrains/tagging.py
  13. +20
    -17
      poobrains/themes/default/editor.scss
  14. +1
    -1
      poobrains/themes/default/main.jinja
  15. +1
    -0
      poobrains/themes/default/svg/map-inline.jinja

+ 3
- 5
poobrains/__init__.py View File

@@ -138,17 +138,15 @@ class Response(flask.Response):
def __init__(self, *args, **kwargs):

super().__init__(*args, **kwargs)
if hasattr(config, 'CONTENT_SECURITY_POLICY'):
self.headers['Content-Security-Policy'] = config.CONTENT_SECURITY_POLICY
else:
self.headers['Content-Security-Policy'] = "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; font-src 'self'; worker-src 'none'; frame-ancestors 'self'"

self.headers['Content-Security-Policy'] = "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; font-src 'self'; worker-src 'none'; frame-ancestors 'self'" # CSP to disallow client-side scripting and third-party requests

if hasattr(config, 'HSTS_MAX_AGE'):
hsts_max_age = config.HSTS_MAX_AGE
else:
hsts_max_age = 3600

self.headers['Strict-Transport-Security'] = 'max-age={hsts_max_age}'
self.headers['Strict-Transport-Security'] = f'max-age={hsts_max_age}'

def set_cookie(self, *args, **kwargs):
# always set samesite for cookies, defaulting to 'strict' because security.


+ 37
- 46
poobrains/analysis/data.py View File

@@ -95,7 +95,6 @@ class EphemeralDataset(poobrains.auth.Protected):
self._name = name
self.title = title or type(self).__name__
self.description = description or ''
self.plot_data = {'kind': 'ScatterPlot', 'layers': {}}
self.data = data if not data is None else {}

def __len__(self):
@@ -109,13 +108,6 @@ class EphemeralDataset(poobrains.auth.Protected):

def __delitem__(self, column_name):
del(self.data[column_name])
if isinstance(self.plot_data, dict):
layers_to_delete = []
for layer_name, layer_info in self.plot_data['layers'].items():
if column_name in layer_info.values():
layers_to_delete.append(layer_name)
for layer_name in layers_to_delete:
del(self.plot_data['layers'][layer_name])

def __iter__(self):
return self.data.__iter__()
@@ -172,6 +164,10 @@ class EphemeralDataset(poobrains.auth.Protected):
def empty(self):
return 0 == sum([len(column['observations']) for column in self]) # sum length of all columns

@locked_cached_property
def plots(self):
return EphemeralDatasetPlot.select().where(EphemeralDatasetPlot.dataset == self.__class__.__name__.lower())

def validate(self):

""" raise ValueError if data is malformed. """
@@ -200,22 +196,6 @@ class EphemeralDataset(poobrains.auth.Protected):
if not isinstance(observation, __types__[column['dtype']]):
raise ValueError(f"Dataset '{self.name}': Type mismatch in data['{name}']['observations'][{index}] must be {column['dtype']}, but is {type(observation).__name__}.")

def add_plot_layer(self, x=None, y=None):

if y is None:
raise ValueError("add_plot_layer parameter 'y' must be passed and must not be None.")

if x is None:
self.plot_data['layers'][y] = {
'y': y
}

else:
self.plot_data['layers'][f'{x}-{y}'] = {
'x': x,
'y': y,
}

def column_index(self, column_name):
return [k for k in self[column_name]]

@@ -249,7 +229,6 @@ class EphemeralDataset(poobrains.auth.Protected):
ds = cls()
ds.title = d['title']
ds.description = poobrains.md.MarkdownString(d['description'])
ds.plot_data = d['plot_data']
ds.data = data

return ds
@@ -264,7 +243,6 @@ class EphemeralDataset(poobrains.auth.Protected):
'title': self.title,
'description': str(self.description),
'data': prepro_data,
'plot_data': self.plot_data
}

return prepro_data
@@ -294,7 +272,6 @@ class EphemeralDataset(poobrains.auth.Protected):
else:
raise ValueError(f"Invalid serialization format: {format}, must be 'json' or 'bson'")


return serializer(self.to_dict(whole=whole))

def column_to_pandas(self, column_name):
@@ -372,39 +349,41 @@ class EphemeralDataset(poobrains.auth.Protected):

@property
def plottable(self):
return isinstance(self.plot_data, dict) and 'kind' in self.plot_data and 'layers' in self.plot_data and len(self.plot_data['layers']) > 0
return self.plots.count() > 0 # WE WERE HERE

def plot(self, plot_info=None):

def plot(self, kind=None, layers=None):
if plot_info is None:
if self.plots.count == 0:
raise ExposedError("This dataset has no associated plot definitions.")
plot_info = self.plots[0].info

if not isinstance(self.plot_data, dict) and (kind is None or layers is None):
raise ValueError("No valid plot data and not enough passed information (kind, layers).")
if not isinstance(plot_info, dict) or not ('kind' in plot_info and 'layers' in plot_info):
raise ValueError("Invalid plot data.")

if kind is None:
kind = self.plot_data['kind']
kind = plot_info['kind']

plot_kinds = visualization.Plot.class_children_keyed()

if not kind in plot_kinds:
raise ValueError('Unknown plot kind: %s' % kind)

if layers is None:
layers = self.plot_data['layers']
else:
layers = custom_layers

return plot_kinds[kind](dataset=self, layers=self.plot_data['layers'])
return plot_kinds[kind](dataset=self, layers=plot_info['layers'], options=plot_info.get('options'))

def table(self, max_rows=4):

t = poobrains.rendering.Table(columns=self.keys())
for index in self.complete_index()[:max_rows]:

if max_rows == math.inf:
source = self.complete_index()
else:
source = self.complete_index()[:max_rows]

for index in source:
row_data = []
for column in self.values():
if index in column['observations']:
value = str(column['observations'][index])
if len(value) > 100:
value = value[:99] + '…'
value = poobrains.helpers.truncate_string(str(column['observations'][index]), 100)
else:
value = ""

@@ -461,8 +440,6 @@ class Dataset(EphemeralDataset, poobrains.commenting.Commentable):
title = poobrains.storage.fields.CharField()
description = poobrains.md.MarkdownField(null=True)
data = DataField()
#plot_data = poobrains.storage.fields.TextField(form_widget=None, null=True)
plot_data = poobrains.storage.fields.BSONField(default={})
lock = poobrains.storage.fields.ForeignKeyField(poobrains.storage.SessionData, null=True, on_delete="SET NULL")

form_add = editor.DataEditor
@@ -481,6 +458,10 @@ class Dataset(EphemeralDataset, poobrains.commenting.Commentable):
def load(self, handle):
return super(poobrains.commenting.Commentable, self).load(handle)

@locked_cached_property
def plots(self):
return DatasetPlot.select().where(EphemeralDatasetPlot.dataset == self.__class__.__name__.lower())

def form(self, mode=None):
f = super(poobrains.commenting.Commentable, self).form(mode=mode)
del(f['tags'])
@@ -488,3 +469,13 @@ class Dataset(EphemeralDataset, poobrains.commenting.Commentable):

def save(self, **kwargs):
return super(EphemeralDataset, self).save(**kwargs)


class EphemeralDatasetPlot(poobrains.storage.Named):

dataset = poobrains.storage.fields.CharField()
info = poobrains.storage.fields.BSONField(default={})

class DatasetPlot(EphemeralDatasetPlot):

dataset = poobrains.storage.fields.ForeignKeyField(Dataset, backref='plots')

+ 130
- 77
poobrains/analysis/editor.py View File

@@ -62,19 +62,38 @@ class EditorPlotActionControl(poobrains.form.Fieldset):

self.editor = editor


action = editor.session['plot_action']
plot_kind = self.editor.dataset.plot_data.get('kind', 'ScatterPlot')
plot_cls = poobrains.analysis.visualization.Plot.class_children_keyed()[plot_kind]
action_choices = [('plotdef_add', 'Add new plot definition')]

if self.editor.instance.plots.count() > 1:
action_choices.append(('plotdef_switch', 'Switch plot definition'))

if not editor.session['plot_definition_name'] is None:

self['action'] = poobrains.form.fields.Select(label='Action', choices=(('kind', 'Change plot kind'), ('options', 'Edit plot options'), ('layer_add', 'Add layer'), ('layer_edit', 'Edit layer'), ('layer_delete layer(s)', 'Delete')))
self['definition_message'] = poobrains.form.fields.Message(value=editor.session['plot_definition_name'])
action_choices += [('kind', 'Change plot kind'), ('options', 'Edit plot options'), ('layer_add', 'Add layer'), ('layer_edit', 'Edit layer'), ('layer_delete', 'Delete layer(s)')]
plot_kind = self.editor.session['plot_definition_info']['kind']
plot_cls = poobrains.analysis.visualization.Plot.class_children_keyed()[plot_kind]

self['action'] = poobrains.form.fields.Select(label='Action', choices=action_choices, default=action)
self.cancel = poobrains.form.Button('submit', label='Cancel')
if not action is None:
self['action'].value = action
self['action'].readonly = True

if action == 'kind':
if action == 'plotdef_add':
self['plotdef_name'] = poobrains.form.fields.Text(label='Name', help_text='machine-readable ([a-z], [0-9], -)', validators=[poobrains.form.validators.valid_name])
self['kind'] = poobrains.form.fields.Select(choices=plot_kind_choices())
self.plotdef_add = poobrains.form.Button('submit', label="Add plot definition")

elif action == 'plotdef_switch':

self['plot_definition'] = poobrains.form.fields.Select(label="Plot Definition", type=poobrains.form.types.StorableParamType(poobrains.analysis.data.EphemeralDatasetPlot), choices=[(x, x.title) for x in editor.instance.plots])
self.plotdef_switch = poobrains.form.Button('submit', label='Switch')

elif action == 'kind':
self['kind'] = poobrains.form.fields.Select(choices=plot_kind_choices(), default=plot_kind)
self.apply_kind = poobrains.form.Button('submit', label='Apply')

@@ -83,15 +102,15 @@ class EditorPlotActionControl(poobrains.form.Fieldset):
self.apply_options = poobrains.form.Button('submit', label='Apply')

elif action == 'layer_add':
self['layer_add'] = plot_cls.editor_layer_fieldset(self.editor)
self.add = poobrains.form.Button('submit', label='Add')
self['layer'] = plot_cls.editor_layer_fieldset(self.editor)
self.layer_add = poobrains.form.Button('submit', label='Add')

elif action == 'layer_edit':
self['layer_name'] = poobrains.form.fields.Select(label="Layer", choices=self.editor.layer_choices)
self.edit_choose = poobrains.form.Button('submit', label='Choose')

elif action == 'layer_edit.layer':
self['layer_edit'] = plot_cls.editor_layer_fieldset(self.editor)
self['layer'] = plot_cls.editor_layer_fieldset(self.editor)
self.edit_save = poobrains.form.Button('submit', label='Save')

elif action == 'layer_delete':
@@ -112,8 +131,26 @@ class EditorPlotActionControl(poobrains.form.Fieldset):
if submit == 'action_choose':
self.editor.session['plot_action'] = f"{self['action'].value}"

elif submit == 'add':
self['layer_add'].process(submit)
elif submit == 'plotdef_add':
app.debugger.set_trace()
plot_definition = poobrains.analysis.data.DatasetPlot()
plot_definition.name = self['plotdef_name'].value
plot_definition.dataset = self.editor.instance
plot_definition.info = {
'kind': self['kind'].value,
'layers': {}
}

plot_definition.save()

self.editor.session['plot_definition_name'] = plot_definition.name
self.editor.session['plot_definition_info'] = plot_definition.info
self.editor.session['plot_action'] = 'options' # show plot options after saving.
flash(f"Added new plot definition '{plot_definition.name}'.")

elif submit == 'layer_add':
self['layer'].process(submit)
self.editor.session['plot_action'] = None
self.editor.session['plot_action_data'] = None

@@ -125,14 +162,14 @@ class EditorPlotActionControl(poobrains.form.Fieldset):
flash(f"Now editing layer '{self['layer_name'].value}'.")

elif submit == 'edit_save':
self['layer_edit'].process(submit)
self['layer'].process(submit)
self.editor.session['plot_action'] = None
self.editor.session['plot_action_data'] = None

elif submit == 'delete':
n = 0
for layer_name in self['layers'].value:
del(self.editor.dataset.plot_data['layers'][layer_name])
del(self.editor.session['plot_definition_info']['layers'][layer_name])
n += 1

flash(f"Deleted {n} layers.")
@@ -141,18 +178,18 @@ class EditorPlotActionControl(poobrains.form.Fieldset):

elif submit == 'apply_kind':

old_plot_kind = self.editor.dataset.plot_data.get('kind', 'ScatterPlot')
old_plot_kind = self.editor.session['plot_definition_info']['kind']
old_plot_cls = poobrains.analysis.visualization.Plot.class_children_keyed()[old_plot_kind]

new_plot_kind = self['kind'].value
new_plot_cls = poobrains.analysis.visualization.Plot.class_children_keyed()[new_plot_kind]

self.editor.dataset.plot_data['kind'] = new_plot_kind
self.editor.session['plot_definition_info']['kind'] = new_plot_kind

flash(f"Changed plot kind to {new_plot_kind}.")

if not old_plot_cls.editor_layer_fieldset is new_plot_cls.editor_layer_fieldset:
self.editor.dataset.plot_data['layers'].clear()
self.editor.session['plot_definition_info']['layers'].clear()
flash("Removed old layer information since it's not compatible to the new plot kind.", 'warning')

self.editor.session['plot_action'] = None
@@ -225,7 +262,7 @@ class EditorDatasetEdit(poobrains.form.Fieldset):
class EditorColumnAdd(poobrains.form.Fieldset):

title = "Add new column"
column_name = poobrains.form.fields.Text(label="Name", help_text='machine readable ([a-z], [0-9], -)', validators=[poobrains.form.validators.valid_name])
column_name = poobrains.form.fields.Text(label="Name", help_text='machine-readable ([a-z], [0-9], -)', validators=[poobrains.form.validators.valid_name])
column_title = poobrains.form.fields.Text(label="Title")
column_dtype = poobrains.form.fields.Select(label="Data type", choices=dtype_choices)
column_description = poobrains.form.fields.TextArea(label="Description")
@@ -432,7 +469,7 @@ class EditorDatasetJoin(poobrains.form.Fieldset):
except ValueError as e:
flash(f"Error when joining: {str(e)}", 'error')
else:
self.editor.dataset.plot_data = other.plot_data # TODO: make this opt-in
for key in other:
if 'rsuffix' in kw:
lkey = f"{key}{kw['rsuffix']}"
@@ -516,12 +553,16 @@ class EditorColumnEdit(poobrains.form.Fieldset):
del(self.editor.dataset[column_name])

if self.editor.dataset.plottable:
# rename any occurence of this renamed column in plot_data
for layer_name, layer_info in self.editor.dataset.plot_data['layers'].items():
if layer_info['x'] == column_name:
layer_info['x'] = authoritative_column_name
if layer_info['y'] == column_name:
layer_info['y'] = authoritative_column_name
# rename any occurence of this renamed column in plot definitions
for plot_definition in self.editor.dataset.plots:

plot_kind = plot_definition.info['kind']
plot_cls = Plot.class_children_keyed()[plot_kind]

for layer_name, layer_info in plot_definition.info['layers'].items():
for key in plot_cls.layer_column_fields:
if layer_info[key] == column_name:
layer_info[key] = authoritative_column_name

flash(f"Renamed column from '{column_name}' to '{authoritative_column_name}'.")

@@ -775,68 +816,38 @@ class DataEditor(poobrains.auth.BoundForm):
self['new'] = EditorDatasetNew(self)

else:

self.title = f"Edit dataset '{self.instance.name}'."
if self.has_lock:

if not self.instance.name in session['editor-sessions']:

# set up a fresh editor session holding current data
self.session = {
'action': None,
'action_data': None,
'plot_action': None,
'plot_action_data': None,
'dataset': self.dataset.to_dict(whole=True),
'tab_display': 'plot-wrapper',
'tab_form': 'editoractioncontrol',
}

session['editor-sessions'][self.instance.name] = self.session
flash(f"Added editor session '{self.instance.name}'.")
self.session = session['editor-sessions'][self.instance.name]
self.dataset = poobrains.analysis.data.EphemeralDataset.from_dict(self.session['dataset'], whole=True)

if self.session['plot_definition_name'] is None:
self.plot_definition = None
else:
self.session = session['editor-sessions'][self.instance.name]
self.dataset = poobrains.analysis.data.EphemeralDataset.from_dict(self.session['dataset'], whole=True)

if self.dataset.plottable:
plot = self.dataset.plot()
else:
plot = poobrains.rendering.RenderString("This dataset has no associated plot information.")

table = self.dataset.table()
self.plot_definition = poobrains.analysis.data.DatasetPlot.load(self.session['plot_definition_name'])

plot_fieldset = poobrains.form.Fieldset(name="plot-wrapper")

table_fieldset = poobrains.form.Fieldset(name="table-wrapper")

self['display'] = poobrains.form.TabbedFieldsets([plot_fieldset, table_fieldset])
table_fieldset['table'] = poobrains.form.fields.RenderableWrapper(value=table, title=table.title)
plot_fieldset['plot'] = poobrains.form.fields.RenderableWrapper(value=plot, title=plot.title)
table_fieldset.title = table.title
plot_fieldset.title = plot.title
self['display'] = self.display_tab()
self['display'].value = self.session['tab_display']

self['form-tabs'] = poobrains.form.TabbedFieldsets([EditorPlotActionControl(self), EditorActionControl(self)])
self['form-tabs'].value = self.session['tab_form']

#flash(self.session['action'], 'warning')
#self['action_control'] = EditorActionControl(self)

#if not self.empty:
# self['plot_action_control'] = EditorPlotActionControl(self)
self['form'] = poobrains.form.TabbedFieldsets([EditorPlotActionControl(self), EditorActionControl(self)])
self['form'].value = self.session['tab_form']

if self.session['action'] is None and self.session['plot_action'] is None:
self.reset = poobrains.form.Button(label='Reset changes', type='submit')
self.save = poobrains.form.Button(type='submit', label='Save')
self['form']['editoractioncontrol'].reset = poobrains.form.Button(label='Reset changes', type='submit')
self['form']['editoractioncontrol'].save = poobrains.form.Button(type='submit', label='Save')

else:

self['display'] = self.display_tab()
self['form'] = poobrains.form.Fieldset(title='Ready to edit')

if self.locked:
flash("This dataset is currently being edited in another session.", 'warning')

else:
self.start_session = poobrains.form.Button(type='submit', label='Start editing')
self['form'].start_session = poobrains.form.Button(type='submit', label='Start editing')

@property
def action_choices(self):
@@ -856,7 +867,7 @@ class DataEditor(poobrains.auth.BoundForm):

@property
def layer_choices(self):
return [(layer_name, layer_name) for layer_name in self.dataset.plot_data['layers'].keys()]
return [(layer_name, layer_name) for layer_name in self.session['plot_definition_info']['layers'].keys()]

@property
def locked(self):
@@ -896,6 +907,27 @@ class DataEditor(poobrains.auth.BoundForm):
return sum([len(column['observations']) for column in self.dataset.values()]) == 0
return True

def display_tab(self):

if isinstance(self.session['plot_definition_info'], dict):
plot = self.dataset.plot(self.session['plot_definition_info'])
else:
plot = poobrains.rendering.RenderString("No plot definition.")

table = self.dataset.table(max_rows=math.inf)

plot_fieldset = poobrains.form.Fieldset(name="plot-wrapper")

table_fieldset = poobrains.form.Fieldset(name="table-wrapper")

tabs = poobrains.form.TabbedFieldsets([plot_fieldset, table_fieldset])
table_fieldset['table'] = poobrains.form.fields.RenderableWrapper(value=table, title=table.title)
plot_fieldset['plot'] = poobrains.form.fields.RenderableWrapper(value=plot, title=plot.title)
table_fieldset.title = table.title
plot_fieldset.title = plot.title

return tabs

def summon_action_parameter_fieldset(self, source):

if issubclass(source, poobrains.analysis.sources.CustomFormDataset):
@@ -917,9 +949,32 @@ class DataEditor(poobrains.auth.BoundForm):
def process(self, submit):

if not self.instance.name in session['editor-sessions']:
if submit == 'start_session':
if submit == 'form.start_session':
self.instance.lock = session.sessiondata
self.instance.save()

# set up a fresh editor session holding current data
editor_session = {
'dataset': self.dataset.to_dict(whole=True),
'action': None,
'action_data': None,
'plot_action': None,
'plot_action_data': None,
'tab_display': self['display'].value,
'tab_form': 'editoractioncontrol',
}

if self.instance.plots.count() > 0:
plot_def = self.instance.plots[0].name
editor_session['plot_definition_name'] = plot_def.name
editor_session['plot_definition_info'] = plot_def.info
else:
editor_session['plot_definition_name'] = None
editor_session['plot_definition_info'] = None
session['editor-sessions'][self.instance.name] = editor_session

flash(f"Added editor session '{self.instance.name}'.")
return redirect(self.instance.url('edit'))

elif submit.startswith('new.'):
@@ -932,13 +987,11 @@ class DataEditor(poobrains.auth.BoundForm):
else:

session['editor-sessions'][self.instance.name]['tab_display'] = self['display'].value
session['editor-sessions'][self.instance.name]['tab_form'] = self['form-tabs'].value
session['editor-sessions'][self.instance.name]['tab_form'] = self['form'].value


if submit == 'form.save':

if submit == 'save':
#self.instance.plot_kind = self.session['plot_kind']
#self.instance.layers = self.session['layers']
self.instance.plot_data = self.dataset.plot_data
self.instance.data = self.dataset.data
self.instance.lock = None
self.instance.save()
@@ -947,7 +1000,7 @@ class DataEditor(poobrains.auth.BoundForm):
flash(f"Saved data to database and removed session '{self.instance.name}'.")
return redirect(self.instance.url('edit'))

elif submit == 'reset':
elif submit == 'form.reset':
self.instance.lock = None
self.instance.save()
del(session['editor-sessions'][self.instance.name])


+ 1
- 1
poobrains/analysis/sources.py View File

@@ -122,7 +122,7 @@ class FileFieldset(poobrains.form.Fieldset):
self.editor.session['action_data']['raw'] = raw

if consumer == 'geojson':
features = geojson.loads(raw.decode('utf-8'))
features = geojson.loads(raw)
ds = poobrains.analysis.EphemeralDataset()

if features.get("type") == 'FeatureCollection':


+ 52
- 52
poobrains/analysis/visualization.py View File

@@ -48,7 +48,7 @@ class Plot(poobrains.svg.SVG):
'inline': 'read'
}

def __init__(self, handle=None, mode=None, dataset=None, layers=None, **kwargs):
def __init__(self, handle=None, mode=None, dataset=None, layers=None, options=None, **kwargs):

super().__init__(handle=handle, mode=mode, **kwargs)

@@ -71,10 +71,11 @@ class Plot(poobrains.svg.SVG):
else:
self.dataset = poobrains.analysis.data.load_dataset(handle)

if len(self.dataset) == 0:
raise ValueError("Empty Dataset can't be visualized")
#if len(self.dataset) == 0:
# raise ValueError("Empty Dataset can't be visualized")

self.layers = layers
self.options = options

self.preprocessed_data = None

@@ -140,9 +141,9 @@ class XYLayerFieldset(poobrains.form.Fieldset):
if isinstance(self.editor.session['action_data'], dict) and 'layer_name' in self.editor.session['action_data']:
self['layer_name'].value = self.editor.session['action_data']['layer_name']
if self['layer_name'].value in self.editor.dataset.plot_data['layers']:
self['column_x'].value = self.editor.dataset.plot_data['layers'][self['layer_name'].value]['x']
self['column_y'].value = self.editor.dataset.plot_data['layers'][self['layer_name'].value]['y']
if self['layer_name'].value in self.editor.session['plot_definition_info']['layers']:
self['column_x'].value = self.editor.session['plot_definition_info']['layers'][self['layer_name'].value]['x']
self['column_y'].value = self.editor.session['plot_definition_info']['layers'][self['layer_name'].value]['y']

def validate(self, submit):

@@ -159,13 +160,13 @@ class XYLayerFieldset(poobrains.form.Fieldset):
errors.append(poobrains.errors.ValidationError("You must choose an Y-column for this layer.", self['column_y']))

if submit == 'add':
if layer_name in self.editor.dataset.plot_data['layers']:
if layer_name in self.editor.session['plot_definition_info']['layers']:
errors.append(poobrains.errors.ValidationError(f"Layer with name '{layer_name}' already exists!", self['layer_name']))

elif submit == 'edit_save':

old_name = self.editor.session['action_data']['layer_name']
if layer_name != old_name and layer_name in self.editor.dataset.plot_data['layers']:
if layer_name != old_name and layer_name in self.editor.session['plot_definition_info']['layers']:
errors.append(poobrains.errors.ValidationError(f"Layer with name '{layer_name}' already exists!", self['layer_name']))

if len(errors):
@@ -177,8 +178,8 @@ class XYLayerFieldset(poobrains.form.Fieldset):
column_x = self['column_x'].value
column_y = self['column_y'].value

if submit == 'add':
self.editor.dataset.plot_data['layers'][layer_name] = {
if submit == 'layer_add':
self.editor.session['plot_definition_info']['layers'][layer_name] = {
'x': column_x,
'y': column_y,
}
@@ -187,8 +188,8 @@ class XYLayerFieldset(poobrains.form.Fieldset):

elif submit == 'edit_save':
old_name = self.editor.session['action_data']['layer_name']
del(self.editor.dataset.plot_data['layers'][old_name])
self.editor.dataset.plot_data['layers'][layer_name] = {
del(self.editor.session['plot_definition_info']['layers'][old_name])
self.editor.session['plot_definition_info']['layers'][layer_name] = {
'x': column_x,
'y': column_y,
}
@@ -208,35 +209,33 @@ class ScatterPlot(Plot):
'datetime64'
]

layer_column_fields = ['x', 'y'] # keys in layer definitions that reference columns, needed in case of column renames.

editor_fieldset = XYFieldset
editor_layer_fieldset = XYLayerFieldset


def __init__(self, handle=None, mode=None, dataset=None, layers=None, **kwargs):
def __init__(self, handle=None, mode=None, dataset=None, layers=None, options=None, **kwargs):

super().__init__(handle=handle, mode=mode, dataset=dataset, layers=layers, **kwargs)
super().__init__(handle=handle, mode=mode, dataset=dataset, layers=layers, options=options, **kwargs)

if self.layers is None:

if isinstance(self.dataset.plot_data, dict):
self.layers = self.dataset.plot_data['layers']
self.layers = {}
if len(self.dataset) >= 2: # take first two columns as x and y axis, respectively
iter_keys = iter(self.dataset.keys())
x = next(iter_keys)
y = next(iter_keys)
self.layers[f'{x}-{y}'] = {
'x': x,
'y': y
}

else:
self.layers = {}
if len(self.dataset) >= 2: # take first two columns as x and y axis, respectively
iter_keys = iter(self.dataset.keys())
x = next(iter_keys)
y = next(iter_keys)
self.layers[f'{x}-{y}'] = {
'x': x,
'y': y
}

else: # len(dataset) == 1; take only column as y with index as x; I don't think this is implemented yet…
y = next(iter(self.dataset.keys()))
self.layers[f'{y}'] = {
'y': y,
}
else: # len(dataset) == 1; take only column as y with index as x; I don't think this is implemented yet…
y = next(iter(self.dataset.keys()))
self.layers[f'{y}'] = {
'y': y,
}

def preprocess_data(self):

@@ -499,21 +498,24 @@ class MapFieldset(poobrains.form.Fieldset):

self.plot_cls = plot_cls
self.editor = editor
if 'options' in self.editor.session['plot_definition_info']:
projection_default = self.editor.session['plot_definition_info']['options'].get('projection')
else:
projection_default=None

self['projection'] = poobrains.form.fields.Select(
label="Map projection",
choices=projection_choices,
default=projection_default
)

if 'options' in self.editor.dataset.plot_data:
self['projection'].value = self.editor.dataset.plot_data['options'].get('projection')

def validate(self, submit):
pass

def process(self, submit):
if submit == 'apply_options':
self.editor.dataset.plot_data['options'] = {
self.editor.session['plot_definition_info']['options'] = {
'projection': self['projection'].value
}

@@ -559,9 +561,9 @@ class MapLayerFieldset(poobrains.form.Fieldset):
if isinstance(self.editor.session['action_data'], dict) and 'layer_name' in self.editor.session['action_data']:
self['layer_name'].value = self.editor.session['action_data']['layer_name']
if self['layer_name'].value in self.editor.dataset.plot_data['layers']:
if self['layer_name'].value in self.editor.session['plot_definition_info']['layers']:
layer_name = self['layer_name'].value
layer_info = self.editor.dataset.plot_data['layers'][layer_name]
layer_info = self.editor.session['plot_definition_info']['layers'][layer_name]
self['column_geometry'].value = layer_info['geometry']
self['column_chloropleth_a'].value = layer_info['chloropleth_a']
self['column_chloropleth_b'].value = layer_info['chloropleth_b']
@@ -571,13 +573,13 @@ class MapLayerFieldset(poobrains.form.Fieldset):
errors = poobrains.errors.CompoundError()

if submit == 'add':
if layer_name in self.editor.dataset.plot_data['layers']:
if layer_name in self.editor.session['plot_definition_info']['layers']:
errors.append(poobrains.errors.ValidationError(f"Layer with name '{layer_name}' already exists!", self['layer_name']))

elif submit == 'edit_save':

old_name = self.editor.session['action_data']['layer_name']
if layer_name != old_name and layer_name in self.editor.dataset.plot_data['layers']:
if layer_name != old_name and layer_name in self.editor.session['plot_definition_info']['layers']:
errors.append(poobrains.errors.ValidationError(f"Layer with name '{layer_name}' already exists!", self['layer_name']))

if len(errors):
@@ -588,7 +590,7 @@ class MapLayerFieldset(poobrains.form.Fieldset):
layer_name = self['layer_name'].value

if submit == 'add':
self.editor.dataset.plot_data['layers'][layer_name] = {
self.editor.session['plot_definition_info']['layers'][layer_name] = {
'geometry': self['column_geometry'].value,
'chloropleth_a': self['column_chloropleth_a'].value,
'chloropleth_b': self['column_chloropleth_b'].value,
@@ -596,8 +598,8 @@ class MapLayerFieldset(poobrains.form.Fieldset):
elif submit == 'edit_save':
old_name = self.editor.session['action_data']['layer_name']
del(self.editor.dataset.plot_data['layers'][old_name])
self.editor.dataset.plot_data['layers'][layer_name] = {
del(self.editor.session['plot_definition_info']['layers'][old_name])
self.editor.session['plot_definition_info']['layers'][layer_name] = {
'geometry': self['column_geometry'].value,
'chloropleth_a': self['column_chloropleth_a'].value,
'chloropleth_b': self['column_chloropleth_b'].value,
@@ -605,24 +607,22 @@ class MapLayerFieldset(poobrains.form.Fieldset):

@app.expose('/svg/map')
class Map(Plot):

__supported_dtypes__ = ['geometry']

layer_column_fields = ['geometries', 'chloropleth_a', 'chloropleth_b'] # keys in layer definitions that reference columns, needed in case of column renames.
editor_fieldset = MapFieldset
editor_layer_fieldset = MapLayerFieldset

def __init__(self, handle=None, mode=None, dataset=None, layers=None, projection=None, **kwargs):
def __init__(self, handle=None, mode=None, dataset=None, layers=None, options=None, **kwargs):

super().__init__(handle=handle, mode=mode, dataset=dataset, layers=layers, **kwargs)
if self.layers is None:
if not 'layers' in self.dataset.plot_data:
raise poobrains.errors.ExposedError(f"No plot layer information for dataset '{dataset.title}'.")
self.layers = self.dataset.plot_data['layers']

if projection is None:
if 'options' in self.dataset.plot_data and 'projection' in self.dataset.plot_data['options']:
projection = self.dataset.plot_data['options']['projection']
else:
projection = 'webmerc' # Web Pseudo Mercator. epsg:3857, ""WGS84""
if options is None or not 'projection' in options:
projection = 'webmerc' # Web Pseudo Mercator. epsg:3857, ""WGS84""
else:
projection = options['projection']
self.projection = pyproj.Proj(f"+proj={projection}")

self.bbox = None


+ 4
- 8
poobrains/auth/__init__.py View File

@@ -1104,8 +1104,6 @@ class BaseAdministerable(PermissionInjection, poobrains.storage.ModelBase):

class Protected(poobrains.rendering.Renderable, metaclass=PermissionInjection):

__metaclass__ = PermissionInjection

class Meta:
abstract = True
permission_class = Permission
@@ -1147,8 +1145,6 @@ class ProtectedForm(Protected, poobrains.form.Form, metaclass=ProtectedFormMeta)

class Administerable(poobrains.storage.Storable, Protected, metaclass=BaseAdministerable):
__metaclass__ = BaseAdministerable

form_add = AutoForm # TODO: move form_ into class Meta?
form_edit = AutoForm
form_delete = DeleteForm
@@ -1339,14 +1335,14 @@ class Administerable(poobrains.storage.Storable, Protected, metaclass=BaseAdmini
return f.view()


class Named(Administerable, poobrains.storage.Named):
class ProtectedNamed(Administerable, poobrains.storage.Named):

class Meta:
abstract = True
handle_fields = ['name']


class User(Named):
class User(ProtectedNamed):

mail = poobrains.storage.fields.CharField(null=True) # FIXME: implement an EmailField
pgp_fingerprint = poobrains.storage.fields.CharField(null=True)
@@ -1631,7 +1627,7 @@ class UserPermission(Administerable):
return f


class Group(Named):
class Group(ProtectedNamed):

@locked_cached_property
def own_permissions(self):
@@ -1887,7 +1883,7 @@ class Owned(Administerable):
return super(Owned, cls).class_view(mode=mode, handle=handle, **kwargs)


class NamedOwned(Owned, Named):
class NamedOwned(Owned, ProtectedNamed):
class Meta:
abstract = True


+ 0
- 2
poobrains/form/__init__.py View File

@@ -30,8 +30,6 @@ class FormMeta(poobrains.helpers.MetaCompatibility, poobrains.helpers.ClassOrIns

class BaseForm(poobrains.rendering.Renderable, metaclass=FormMeta):

__metaclass__ = FormMeta

class Meta:
abstract = True



+ 0
- 2
poobrains/form/fields.py View File

@@ -34,8 +34,6 @@ class BoundFieldMeta(poobrains.helpers.MetaCompatibility, poobrains.helpers.Clas

class BaseField(object, metaclass=poobrains.helpers.MetaCompatibility):

__metaclass__ = poobrains.helpers.MetaCompatibility

_created = None
_default = None
_choices = None


+ 10
- 3
poobrains/helpers.py View File

@@ -91,6 +91,16 @@ def random_string_light(length=8):

return string

def truncate_string(string, max_length):

""""
Truncate a given string to the specified maximum length and adds an
ellipsis ('…') if it was indeed truncated. Works with `math.inf`.
"""

if len(string) > max_length: # never true if max_length == math.inf
return string[:max_length] + '…'
return string

def flatten_nested_multidict(v):

@@ -319,9 +329,6 @@ class MetaCompatibility(type):

class ChildAware(object, metaclass=MetaCompatibility):

__metaclass__ = MetaCompatibility


@classmethod
def class_tree(cls, abstract=False):



+ 0
- 2
poobrains/storage/__init__.py View File

@@ -95,8 +95,6 @@ class ModelBase(poobrains.helpers.MetaCompatibility, peewee.ModelBase):

class Model(peewee.Model, poobrains.helpers.ChildAware, metaclass=ModelBase):

__metaclass__ = ModelBase

class Meta:

database = app.db


+ 0
- 2
poobrains/storage/fields.py View File

@@ -84,8 +84,6 @@ class ForeignKeyChoice(poobrains.form.fields.Text, metaclass=poobrains.form.fiel
Note: This is a FORM field, not a storage field.
"""

__metaclass__ = poobrains.form.fields.BoundFieldMeta

storable = None

def __init__(self, fkfield, choices=None, **kwargs):


+ 1
- 1
poobrains/tagging.py View File

@@ -17,7 +17,7 @@ import poobrains.md


#@app.expose('/tag/', mode='full')
class Tag(poobrains.auth.Named):
class Tag(poobrains.auth.ProtectedNamed):

"""
A tag. Tags can form hierarchies. You can build a forest, but loops are forbidden.


+ 20
- 17
poobrains/themes/default/editor.scss View File

@@ -73,12 +73,30 @@ body.content-type-dataeditor {

&.idx-0 { /* first tab of display is always the plot (if any) has sizing/overflow issues */
overflow: hidden;

& > fieldset {
height: 100%;

& > .renderable-wrapper {
height: 100%;

& > article {
height: 100%;

& > .content {
margin: 0;
padding: 0;
height: 100%;
}
}
}
}
}

fieldset {
& > fieldset {
margin: 0;
padding: 0;
height: 100%;
min-height: 100%;
background: transparent;
border: none;

@@ -87,22 +105,7 @@ body.content-type-dataeditor {
display: none;
}

.renderable-wrapper {
height: 100%;
}

article {
height: 100%;

& > .content {
margin: 0;
padding: 0;
height: 100%;
}
}

&.table-wrapper {
height: 100%;
background: $color_background_dark;
}
}


+ 1
- 1
poobrains/themes/default/main.jinja View File

@@ -4,7 +4,7 @@
{% include 'head.jinja' %}
</head>
<body class="content-type-{{ content.__class__.__name__.lower() }}" title="Did YOU know that starting with HTML5 you can put the title attribute on EVERYTHING!?">
<body class="content-type-{{ content.__class__.__name__.lower() }}">

{% if g.boxes.dashbar %}
{{ g.boxes.dashbar.render() }}


+ 1
- 0
poobrains/themes/default/svg/map-inline.jinja View File

@@ -22,6 +22,7 @@

{% if config.DEBUG %}
<text x="0" y="15">BBOX: {{ content.bbox }}</text>
<text x="0" y="30">REFERENCE SPANS: {{ content.reference_span_x }} / {{ content.reference_span_y }}</text>
{% endif %}

{# beginning of layers #}


Loading…
Cancel
Save