A web framework for aspiring media terrorists – PRE-ALPHA – DO NOT DEPLOY!
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

864 lines
25KB

  1. import collections
  2. import math
  3. import time
  4. import datetime
  5. import json
  6. #import pickle
  7. import inspect
  8. import pandas
  9. from poobrains import Markup, app, request, abort, flash, redirect, g, session, locked_cached_property, new_session
  10. import poobrains.helpers
  11. import poobrains.errors
  12. import poobrains.form
  13. import poobrains.storage
  14. import poobrains.auth
  15. import poobrains.commenting
  16. from . import base
  17. def dynamic_datasets():
  18. datasets = dict([(name.lower(), cls) for name, cls in Dataset.class_children_keyed().items()]) # collects *all* descendants of Dataset including StoredDataset and its descendants
  19. for name in [name.lower() for name in datasets['storeddataset'].class_children_keyed().keys()]: # remove descendants of StoredDataset
  20. del(datasets[name])
  21. del(datasets['storeddataset'])
  22. return datasets
  23. #def get_dataset_parameters(dataset_name):
  24. # """ get parameters for dynamic datasets' `fill` function """
  25. def load_dataset(handle):
  26. if handle.startswith('_'): # underscore marks dynamic datasets in URLs
  27. name, *args = handle.split('.')
  28. name = name[1:] # remove leading underscore
  29. valid_datasets = dynamic_datasets()
  30. if len(name) > 0 and name in valid_datasets:
  31. cls = valid_datasets[name]
  32. #cls.permissions['read'].check(g.user) # make sure everything below this is only executed if user is allowed to see the results (i.e. don't do remote API requests etc if user isn't allowed to see the results anyhow
  33. ds = cls()
  34. ds.fill(*args)
  35. else:
  36. raise poobrains.errors.ExposedError('Unknown dynamic dataset: %s' % func)
  37. else:
  38. ds = StoredDataset.load(handle)
  39. ds.permissions['read'].check(g.user)
  40. return ds
  41. def dataset_choices():
  42. dynamic = []
  43. for name, cls in dynamic_datasets().items():
  44. try:
  45. cls.permissions['read'].check(g.user)
  46. name = '_%s' % name
  47. dynamic.append((name, cls.__name__))
  48. except poobrains.auth.AccessDenied:
  49. pass
  50. stored = []
  51. for dataset in StoredDataset.list('read', g.user):
  52. stored.append((dataset.name, dataset.title))
  53. choices = [(dynamic, 'Dynamic Datasets'), (stored, 'Stored Datasets')]
  54. return choices
  55. def dynamic_dataset_parameters(dataset_class):
  56. r = {}
  57. signature = inspect.signature(dataset_class.fill)
  58. for param in signature.parameters.values():
  59. if param.name == 'self':
  60. continue # next loop iteration
  61. if param.annotation == param.empty:
  62. param_type = str
  63. else:
  64. param_type = param.annotation
  65. r[param.name] = {
  66. 'type': param_type,
  67. 'default': param.default
  68. }
  69. return r
  70. def validate_handle_not_in_session(handle):
  71. if handle in session['editor-sessions']:
  72. raise poobrains.errors.ValidationError("Editor Session named '%s' already exists!" % handle)
  73. #
  74. class Dataset(poobrains.auth.Protected):
  75. title = None # to override Renderable's @property
  76. description = poobrains.md.MarkdownString()
  77. def __init__(self, name=None, title=None, description=None, data=None, label_x=None, label_y=None):
  78. self._name = name
  79. self.title = title or type(self).__name__
  80. self.description = description or ''
  81. self.label_x = label_x or 'X'
  82. self.label_y = label_y or 'Y'
  83. if not data is None:
  84. self.data = data
  85. else:
  86. self.data = pandas.DataFrame()
  87. def __len__(self):
  88. return len(self.data)
  89. @property
  90. def name(self):
  91. return self._name or type(self).__name__.lower()
  92. @name.setter
  93. def name(self, value):
  94. self._name = value
  95. @locked_cached_property
  96. def ref_id(self):
  97. return "dataset-%s" % self.name
  98. def datapoint_id(self, x):
  99. return "dataset-%s-%s" % (self.name, x)
  100. def render(self, mode=None):
  101. if mode == 'json':
  102. return self.data.to_json()
  103. return super(Dataset, self).render(mode=mode)
  104. def plot(self, kind='scatter'):
  105. if not kind in plot_kinds:
  106. raise ValueError('wrong plot kind: %s' % kind)
  107. return plot_kinds[kind](dataset=self).render('raw')
  108. def save(self, name=None, owner=None):
  109. """
  110. Convert this Dataset into a StoredDataset and save it.
  111. """
  112. ds = StoredDataset()
  113. #ds.owner = owner or poobrains.auth.User.get(poobrains.auth.User.id == 1)
  114. ds.owner = owner or g.user
  115. now = int(time.time())
  116. ds.name = name or poobrains.helpers.clean_string("%s-%d" % (self.name, now))
  117. ds.title = '%s@%s' % (self.title, str(datetime.datetime.fromtimestamp(now)))
  118. ds.label_x = self.label_x
  119. ds.label_y = self.label_y
  120. ds.description = self.description
  121. ds.data = self.data
  122. ds.save(force_insert=True)
  123. return ds
  124. class EditorNewSessionFieldset(poobrains.form.Fieldset):
  125. handle = poobrains.form.fields.Text(required=True, validators=[validate_handle_not_in_session])
  126. new_session = poobrains.form.Button(label='Create', type='submit')
  127. def process(self, submit, instance):
  128. app.debugger.set_trace()
  129. handle = self.fields['handle'].value
  130. instance.owner = g.user
  131. instance.name = handle
  132. instance.title = handle.capitalize()
  133. instance.label_x = 'x'
  134. instance.label_y = 'y'
  135. instance.save()
  136. return instance.url('edit')
  137. class EditorLoadFieldset(poobrains.form.Fieldset):
  138. key = poobrains.form.fields.Text()
  139. dataset = poobrains.form.fields.Select(choices=dataset_choices)
  140. load = poobrains.form.Button(label='Load', type='submit')
  141. def process(self, submit, instance):
  142. app.debugger.set_trace()
  143. ds = load_dataset(self.fields['dataset'].value)
  144. session['editor-sessions'][instance.name]['data'] = session['editor-sessions'][instance.name]['data'].append(ds.data)
  145. class DataEditor(poobrains.auth.BoundForm):
  146. def __init__(self, instance, mode='add'):
  147. super().__init__(instance, mode=mode)
  148. app.debugger.set_trace()
  149. if mode == 'add':
  150. self.new = EditorNewSessionFieldset()
  151. else:
  152. if not self.instance.name in session['editor-sessions']:
  153. # set up a fresh editor session holding current data
  154. editor_session = {
  155. 'handle': self.instance.name,
  156. 'action': None,
  157. 'action_data': {},
  158. 'title': self.instance.title,
  159. 'description': self.instance.description,
  160. 'data': self.instance.data,
  161. }
  162. session['editor-sessions'][self.instance.name] = editor_session
  163. else:
  164. editor_session = session['editor-sessions'][self.instance.name]
  165. self.load = EditorLoadFieldset(self.handle_string)
  166. if not editor_session['data'].empty:
  167. self.plot = Plot(dataset=Dataset(data=editor_session['data']))
  168. def process(self, submit):
  169. app.debugger.set_trace()
  170. if submit.startswith('new.'):
  171. self.fields['new'].process(submit, self.instance)
  172. elif submit.startswith('load.'):
  173. self.fields['load'].process(submit, self.instance)
  174. return self
  175. class StoredDataset(Dataset, poobrains.commenting.Commentable):
  176. title = poobrains.storage.fields.CharField()
  177. description = poobrains.md.MarkdownField(null=True)
  178. label_x = poobrains.storage.fields.CharField(verbose_name="Label for the x-axis")
  179. label_y = poobrains.storage.fields.CharField(verbose_name="Label for the y-axis")
  180. data = poobrains.storage.fields.TextField() # Fuck it, just use JSON
  181. #data = poobrains.storage.fields.BlobField(form_widget=None)
  182. form_add = DataEditor
  183. form_edit = DataEditor
  184. def __init__(self, *args, **kwargs):
  185. self._data = None
  186. return super(Dataset, self).__init__(*args, **kwargs)
  187. def __getattribute__(self, name):
  188. if name == 'data':
  189. #app.debugger.set_trace()
  190. if self._data is None:
  191. if 'data' in self.__data__:
  192. self._data = pandas.DataFrame.from_dict(json.loads(self.__data__['data']))
  193. else:
  194. self._data = pandas.DataFrame()
  195. self.__data__['data'] = self._data.to_dict()
  196. return self._data
  197. return super(StoredDataset, self).__getattribute__(name)
  198. def __setattr__(self, name, value):
  199. if name == 'data':
  200. if type(value) == pandas.DataFrame:
  201. #value = pickle.dumps(value)
  202. #value = json.dumps(value)
  203. value = value.to_json()
  204. super(StoredDataset, self).__setattr__(name, value)
  205. @locked_cached_property
  206. def ref_id(self):
  207. return "dataset-%s" % self.name
  208. def save(self, **kwargs):
  209. dataobj = self.data
  210. #self.data = dataobj.to_json()
  211. r = super(Dataset, self).save(**kwargs)
  212. self.data = dataobj
  213. return r
  214. # def view(self, mode='teaser', handle=None, **kwargs):
  215. #
  216. # if mode in ['add', 'edit']:
  217. #
  218. # editor = DataEditor()
  219. # return editor.view('full')
  220. #
  221. # super().view(mode=mode, handle=handle, **kwargs)
  222. @app.expose('/svg/plot')
  223. class Plot(base.SVG):
  224. padding = None
  225. width = None
  226. height = None
  227. inner_width = None
  228. inner_height = None
  229. plot_width = None
  230. plot_height = None
  231. description_height = None
  232. dataset = None
  233. length = None
  234. # TODO: deprecate all this by just statically computed bounds?
  235. min_x = None
  236. max_x = None
  237. min_y = None
  238. max_y = None
  239. span_x = None
  240. span_y = None
  241. bounds = None
  242. class Meta:
  243. modes = collections.OrderedDict([
  244. ('teaser', 'read'),
  245. ('full', 'read'),
  246. ('raw', 'read'),
  247. ('json', 'read'),
  248. ('inline', 'read')
  249. ])
  250. def __init__(self, handle=None, mode=None, dataset=None, **kwargs):
  251. super(Plot, self).__init__(handle=handle, mode=mode, **kwargs)
  252. app.debugger.set_trace()
  253. if handle is None and dataset is None:
  254. abort(404, "No dataset selected")
  255. self.padding = app.config['SVG_PLOT_PADDING']
  256. self.plot_width = app.config['SVG_PLOT_WIDTH']
  257. self.plot_height = app.config['SVG_PLOT_HEIGHT']
  258. self.description_height = app.config['SVG_PLOT_DESCRIPTION_HEIGHT']
  259. self.width = self.plot_width + (2 * self.padding)
  260. self.height = self.plot_height + self.description_height + (3 * self.padding)
  261. self.inner_width = self.width - (2 * self.padding)
  262. self.inner_height = self.height - (2 * self.padding)
  263. if not dataset is None:
  264. self.dataset = dataset
  265. else:
  266. self.dataset = load_dataset(handle)
  267. def is_dataframe(self, x):
  268. return type(x) is pandas.DataFrame
  269. def cast_value(self, value):
  270. cls = type(value)
  271. if cls in [int, float]:
  272. return value
  273. casters = {
  274. numpy.datetime64: lambda x: pandas.to_datetime(x).timestamp()
  275. }
  276. if type(value) in casters:
  277. return casters[type(value)](value)
  278. raise ValueError('Value of type %s not castable for plot: %s' % (cls, value))
  279. @locked_cached_property
  280. def min_x(self):
  281. mins = []
  282. if isinstance(self.dataset.data, pandas.DataFrame):
  283. submins = []
  284. for subset_index in self.dataset.data:
  285. submins.append(min(self.dataset.data[subset_index].index))
  286. mins.append(min(submins))
  287. else:
  288. mins.append(min(self.dataset.data.index))
  289. return min(mins)
  290. @locked_cached_property
  291. def max_x(self):
  292. maxes = []
  293. for subset_index in self.dataset.data:
  294. maxes.append(max(self.dataset.data[subset_index].index))
  295. return max(maxes)
  296. @locked_cached_property
  297. def min_y(self):
  298. mins = []
  299. for subset_index in self.dataset.data:
  300. mins.append(min(self.dataset.data[subset_index]))
  301. return min(mins)
  302. @locked_cached_property
  303. def max_y(self):
  304. maxes = []
  305. for subset_index in self.dataset.data:
  306. maxes.append(max(self.dataset.data[subset_index]))
  307. return max(maxes)
  308. def normalize_x(self, value):
  309. if self.span_x == 0.0:
  310. return self.plot_width / 2.0
  311. return (value - self.min_x) * (self.plot_width / self.span_x)
  312. def normalize_y(self, value):
  313. if self.span_y == 0.0:
  314. return self.plot_height / 2.0
  315. return self.plot_height - (value - self.min_y) * (self.plot_height / self.span_y)
  316. @locked_cached_property
  317. def label_x(self):
  318. #return u' / '.join([dataset.label_x for dataset in self.datasets])
  319. return self.dataset.label_x
  320. @locked_cached_property
  321. def label_y(self):
  322. #return u' / '.join([dataset.label_y for dataset in self.datasets])
  323. return self.dataset.label_y
  324. @locked_cached_property
  325. def has_grid(self):
  326. #app.debugger.set_trace()
  327. valid_dtypes = [
  328. int,
  329. float
  330. ]
  331. for dtype in valid_dtypes:
  332. x = False
  333. y = False
  334. if self.dataset.data.index.dtype in valid_dtypes:
  335. x = True # x axis is numerical, can be gridded
  336. if dtype in self.dataset.data.dtypes.values:
  337. y = True # y axais is numeric, can be gridded
  338. if x and y:
  339. return True
  340. return False
  341. @locked_cached_property
  342. def span_x(self):
  343. if not self.has_grid:
  344. return 0 # why, tho?
  345. return max(self.dataset.data.index) - min(self.dataset.data.index)
  346. @locked_cached_property
  347. def span_y(self):
  348. if not self.has_grid:
  349. return 0 # why, tho?
  350. absolute_minimum = None
  351. absolute_maximum = None
  352. for column in self.dataset.data.columns:
  353. for value in self.dataset.data[column].values:
  354. if absolute_minimum is None or value < absolute_minimum:
  355. absolute_minimum = value
  356. if absolute_maximum is None or value > absolute_maximum:
  357. absolute_maximum = value
  358. if not absolute_minimum is None:
  359. return absolute_maximum - absolute_minimum
  360. @locked_cached_property
  361. def grid_x(self):
  362. #app.debugger.set_trace()
  363. if self.span_x == 0:
  364. return [self.min_x]
  365. grid_step = 10 ** (int(math.log10(self.span_x)) - 1)
  366. offset = (self.min_x % grid_step) * grid_step # distance from start of plot to first line on the grid
  367. start = self.min_x + offset
  368. x = start
  369. coords = [x]
  370. while x <= self.max_x:
  371. coords.append(x)
  372. x += grid_step
  373. return coords
  374. @locked_cached_property
  375. def grid_y(self):
  376. if self.span_y == 0:
  377. return [self.min_y]
  378. grid_step = 10 ** (int(math.log10(self.span_y)) - 1)
  379. #offset = (self.min_y % grid_step) * grid_step # distance from start of plot to first line on the grid
  380. offset = grid_step - self.min_y % grid_step
  381. start = self.min_y + offset
  382. y = start
  383. coords = [y]
  384. while y <= self.max_y:
  385. coords.append(y)
  386. y += grid_step
  387. return coords
  388. @app.expose('/svg/lineplot')
  389. class LinePlot(Plot):
  390. pass
  391. @app.expose('/svg/barchart')
  392. class BarChart(Plot):
  393. @locked_cached_property
  394. def bar_width(self):
  395. #return 10
  396. return (self.plot_width - self.padding) / self.length - 1
  397. plot_kinds = {
  398. 'scatter': Plot,
  399. 'line': LinePlot,
  400. 'bar': BarChart
  401. }
  402. @new_session
  403. def editor_session(session):
  404. session['editor-sessions'] = {}
  405. #class PlotKindFieldset(poobrains.form.Fieldset):
  406. #
  407. # def __init__(self, editor_handle, **kwargs):
  408. # super(PlotKindFieldset, self).__init__(**kwargs)
  409. #
  410. # self.editor_handle = editor_handle
  411. # action = session['editor-sessions'][self.editor_handle]['action']
  412. #
  413. # self.kind = poobrains.form.fields.Select(choices=[(kind, cls.__name__) for (kind, cls) in plot_kinds.items()], default=session['editor-sessions'][editor_handle]['plot_kind'])
  414. #
  415. # self.apply = poobrains.form.Button(type='submit', label='Apply')
  416. #
  417. #
  418. # def process(self, submit, instance):
  419. # session['editor-sessions'][instance.handle_string]['plot_kind'] = self.fields['kind'].value
  420. # instance.dataset.plot_kind = self.fields['kind'].value
  421. class EditorLoadFieldset_old(poobrains.form.Fieldset):
  422. title = "Add new dataset to session"
  423. dataset = poobrains.form.fields.Select(choices=dataset_choices)
  424. dataset_name = poobrains.form.fields.Text(label='Dataset name')
  425. dataset_title = poobrains.form.fields.Text(label='Dataset title')
  426. dataset_description = poobrains.form.fields.TextArea(label='Dataset description')
  427. load_dataset = poobrains.form.Button(label='Load', type='submit')
  428. def __init__(self, editor_handle, **kwargs):
  429. datasets = dynamic_datasets()
  430. super(EditorLoadFieldset, self).__init__(**kwargs)
  431. self.editor_handle = editor_handle
  432. editor_session = session['editor-sessions'][self.editor_handle]
  433. if editor_session['action'].startswith('load.dynamic.'):
  434. dataset_name = editor_session['action'].split('.')[2]
  435. self.fields['dataset'].value = '_%s' % dataset_name
  436. self.fields['dataset'].readonly = True
  437. self.fields['dataset_name'].value = editor_session['action_data']['dataset_name']
  438. self.fields['dataset_name'].readonly = True
  439. self.fields['dataset_description'].value = editor_session['action_data']['dataset_description']
  440. self.fields['dataset_description'].readonly = True
  441. if dataset_name in datasets:
  442. dataset_class = datasets[dataset_name]
  443. self.summon_parameter_fieldset(dataset_class)
  444. else:
  445. flash('Unknown dynamic dataset: %s' % dataset_name, 'error')
  446. def summon_parameter_fieldset(self, dataset_class):
  447. subform = poobrains.form.Fieldset(name='dynamic')
  448. params = dynamic_dataset_parameters(dataset_class)
  449. for param_name, param_info in params.items():
  450. param_type = poobrains.form.types.lookup_table[param_info['type']]()
  451. subform.add_field(poobrains.form.fields.Text(name=param_name, label=param_name, type=param_type, default=param_info['default']))
  452. subform.cancel = poobrains.form.Button(type='submit', label='Cancel')
  453. self.add_field(subform)
  454. def process(self, submit, instance):
  455. datasets = dynamic_datasets()
  456. action = session['editor-sessions'][self.editor_handle]['action']
  457. dataset_name = self.fields['dataset'].value
  458. if action == 'load':
  459. if submit == '%s.load_dataset' % self.name:
  460. session['editor-sessions'][self.editor_handle]['action_data'] = {
  461. 'dataset_name': self.fields['dataset_name'].value,
  462. 'dataset_description': self.fields['dataset_description'].value
  463. }
  464. if dataset_name.startswith('_'):
  465. dataset_name = dataset_name[1:]
  466. if not dataset_name in datasets:
  467. flash('Unknown dynamic dataset: %s' % dataset_name, 'error')
  468. else:
  469. session['editor-sessions'][instance.handle_string]['action'] = 'load.dynamic.%s' % dataset_name
  470. self.summon_parameter_fieldset(datasets[dataset_name])
  471. self.fields['dataset'].readonly = True
  472. self.fields['dataset_name'].readonly = True
  473. else: # directly loadable dataset
  474. try:
  475. dataset = StoredDataset.load(dataset_name)
  476. dataset.permissions['read'].check(g.user)
  477. dataset.name = self.fields['dataset_name'].value
  478. instance.datasets[dataset.name] = dataset
  479. session['editor-sessions'][instance.handle_string]['action'] = None
  480. session['editor-sessions'][instance.handle_string]['data'][dataset.name]['title'] = dataset.title
  481. session['editor-sessions'][instance.handle_string]['data'][dataset.name]['description'] = dataset.description
  482. session['editor-sessions'][instance.handle_string]['data'][dataset.name]['data'] = dataset.data.to_dict()
  483. except StoredDataset.DoesNotExist:
  484. flash('Unknown stored dataset: %s' % dataset_name, 'error')
  485. if action.startswith('load.dynamic.'):
  486. if submit == '%s.dynamic.cancel' % self.name:
  487. session['editor-sessions'][instance.handle_string]['action'] = 'load'
  488. del(self.fields['dynamic']) # actively remove the fieldset so it doesn't get rendered in the response directly to this request
  489. self.fields['dataset'].readonly = False
  490. elif submit == '%s.load_dataset' % self.name:
  491. dataset_name = dataset_name[1:]
  492. dataset_class = datasets[dataset_name]
  493. params = dynamic_dataset_parameters(dataset_class)
  494. param_values = {}
  495. for param_name in params:
  496. if param_name in self.fields['dynamic'].fields:
  497. param_values[param_name] = self.fields['dynamic'].fields[param_name].value
  498. if len(param_values) == len(params):
  499. dataset = dataset_class()
  500. dataset.fill(**param_values)
  501. dataset.name = self.fields['dataset_name'].value
  502. dataset.title = self.fields['dataset_title'].value
  503. dataset.description = self.fields['dataset_description'].value
  504. dataset.permissions['read'].check(g.user)
  505. instance.dataset = dataset
  506. session['editor-sessions'][instance.handle_string]['action'] = None
  507. session['editor-sessions'][instance.handle_string]['datasets'][dataset.name] = {
  508. 'title': dataset.title,
  509. 'description': dataset.description,
  510. 'data': dataset.data.to_dict(),
  511. 'plot_kind': 'scatter',
  512. }
  513. del(self.fields['dynamic'])
  514. self.fields['dataset'].readonly = False
  515. else:
  516. flash('Incomplete parameters for dynamic dataset %s!' % dataset_name, 'error')
  517. #@app.expose('/svg/plot/editor/', force_secure=True)
  518. class DataEditor_old(poobrains.auth.ProtectedForm):
  519. #save_dataset = poobrains.form.Button(label="Save", type="submit")
  520. def __init__(self, handle=None, mode=None, **kwargs):
  521. super(DataEditor, self).__init__(handle=handle, mode=mode, **kwargs)
  522. self.menu_actions = poobrains.rendering.Menu('data-editor-sessions', title='Editor sessions')
  523. for editor_session_id in session['editor-sessions'].keys():
  524. self.menu_actions.append(self.__class__.url(mode='full', handle=editor_session_id), editor_session_id)
  525. if not self.handle_string or not self.handle_string in session['editor-sessions']: # new editor session
  526. self.new = EditorNewSessionFieldset(self.handle_string)
  527. else:
  528. editor_session = session['editor-sessions'][self.handle_string]
  529. self.editor_action = editor_session['action']
  530. self.dataset = Dataset()
  531. self.dataset.name = self.handle_string
  532. #self.dataset.plot_kind = editor_session['plot_kind'] or 'scatter'
  533. self.dataset.plot_kind = 'scatter'
  534. self.dataset.title = editor_session['title']
  535. self.dataset.description = editor_session['description']
  536. self.dataset.data = pandas.DataFrame.from_dict(editor_session['data'])
  537. #if len(self.dataset.data):
  538. # self.plot_kind = PlotKindFieldset(self.handle_string)
  539. if editor_session['action'] is None:
  540. editor_session['action'] = 'load'
  541. if editor_session['action'].startswith('load'):
  542. #self.add_field(EditorLoadFieldset(self.handle_string))
  543. self.load = EditorLoadFieldset(self.handle_string)
  544. self.delete_session = poobrains.form.Button(type='submit', label='Delete session')
  545. self.save_dataset = poobrains.form.Button(type='submit', label='Save dataset')
  546. def process(self, submit):
  547. if submit.startswith('new.'):
  548. return self.fields['new'].process(submit, self)
  549. elif submit.startswith('load.'):
  550. self.fields['load'].process(submit, self)
  551. if len(self.dataset):
  552. #self.plot_kind = PlotKindFieldset(self.handle_string)
  553. self.plot = Plot(dataset=self.dataset)
  554. elif submit.startswith('plot_kind.'):
  555. self.fields['plot_kind'].process(submit, self)
  556. elif submit == 'save_dataset':
  557. stored_ds = self.dataset.save()
  558. return redirect(stored_ds.url('edit'))
  559. elif submit == 'delete_session':
  560. del(session['editor-sessions'][self.handle_string])
  561. return redirect(self.__class__.url('full'))
  562. return self