def handle_response(self, response, commit=True): ''' handle the response from self.presenter.start() in self.start() ''' not_ok_msg = 'Are you sure you want to lose your changes?' self._return = None self.clean_model() if response == gtk.RESPONSE_OK or response in self.ok_responses: try: self._return = self.model if self.presenter.dirty() and commit: self.commit_changes() except DBAPIError, e: msg = _('Error committing changes.\n\n%s') % \ utils.xml_safe_utf8(unicode(e.orig)) utils.message_details_dialog(msg, str(e), gtk.MESSAGE_ERROR) self.session.rollback() return False except Exception, e: msg = _('Unknown error when committing changes. See the '\ 'details for more information.\n\n%s') \ % utils.xml_safe_utf8(e) debug(traceback.format_exc()) utils.message_details_dialog(msg, traceback.format_exc(), gtk.MESSAGE_ERROR) self.session.rollback() return False
def remove_callback(genera): """ The callback function to remove a genus from the genus context menu. """ genus = genera[0] from bauble.plugins.plants.species_model import Species session = db.Session() nsp = session.query(Species).filter_by(genus_id=genus.id).count() safe_str = utils.xml_safe_utf8(str(genus)) if nsp > 0: msg = _('The genus <i>%(genus)s</i> has %(num_species)s species. ' 'Are you sure you want to remove it?') \ % dict(genus=safe_str, num_species=nsp) else: msg = _("Are you sure you want to remove the genus <i>%s</i>?") \ % safe_str if not utils.yes_no_dialog(msg): return try: obj = session.query(Genus).get(genus.id) session.delete(obj) session.commit() except Exception, e: msg = _('Could not delete.\n\n%s') % utils.xml_safe_utf8(e) utils.message_details_dialog(msg, traceback.format_exc(), type=gtk.MESSAGE_ERROR)
def handle_response(self, response): """ @return: return True if the editor is realdy to be closes, False if we want to keep editing, if any changes are committed they are stored in self._committed """ # TODO: need to do a __cleanup_model before the commit to do things # like remove the insfraspecific information that's attached to the # model if the infraspecific rank is None not_ok_msg = 'Are you sure you want to lose your changes?' if response == gtk.RESPONSE_OK or response in self.ok_responses: try: if self.presenter.dirty(): self.commit_changes() self._committed.append(self.model) except DBAPIError, e: exc = traceback.format_exc() msg = _('Error committing changes.\n\n%s') % \ utils.xml_safe_utf8(e.orig) utils.message_details_dialog(msg, str(e), gtk.MESSAGE_ERROR) return False except Exception, e: msg = _('Unknown error when committing changes. See the '\ 'details for more information.\n\n%s') % \ utils.xml_safe_utf8(e) debug(traceback.format_exc()) #warning(traceback.format_exc()) utils.message_details_dialog(msg, traceback.format_exc(), gtk.MESSAGE_ERROR) return False
def remove_callback(families): """ The callback function to remove a family from the family context menu. """ family = families[0] from bauble.plugins.plants.genus import Genus session = db.Session() ngen = session.query(Genus).filter_by(family_id=family.id).count() safe_str = utils.xml_safe_utf8(str(family)) if ngen > 0: msg = _('The family <i>%(family)s</i> has %(num_genera)s genera. Are ' 'you sure you want to remove it?') % dict(family=safe_str, num_genera=ngen) else: msg = _("Are you sure you want to remove the family <i>%s</i>?") \ % safe_str if not utils.yes_no_dialog(msg): return try: obj = session.query(Family).get(family.id) session.delete(obj) session.commit() except Exception, e: msg = _('Could not delete.\n\n%s') % utils.xml_safe_utf8(e) utils.message_details_dialog(msg, traceback.format_exc(), type=gtk.MESSAGE_ERROR)
def get_Notes(self): if not self.plant.notes: return None notes = [] for note in self.plant.notes: notes.append(dict(date=utils.xml_safe_utf8(note.date.isoformat()), user=xml_safe_utf8(note.user), category=xml_safe_utf8(note.category), note=xml_safe_utf8(note.note))) return xml_safe_utf8(str(notes))
def plant_markup_func(plant): """ """ sp_str = plant.accession.taxon_str(markup=True) # dead_color = "#777" dead_color = "#9900ff" if plant.quantity <= 0: dead_markup = '<span foreground="%s">%s</span>' % (dead_color, utils.xml_safe_utf8(plant)) return dead_markup, sp_str else: return utils.xml_safe_utf8(plant), sp_str
def source_detail_remove_callback(details): detail = details[0] s = '%s: %s' % (detail.__class__.__name__, str(detail)) msg = _("Are you sure you want to remove %s?") % utils.xml_safe_utf8(s) if not utils.yes_no_dialog(msg): return try: session = db.Session() obj = session.query(SourceDetail).get(detail.id) session.delete(obj) session.commit() except Exception, e: msg = _('Could not delete.\n\n%s') % utils.xml_safe_utf8(e) utils.message_details_dialog(msg, traceback.format_exc(), type=gtk.MESSAGE_ERROR)
def on_activate(item, cb): result = False try: # have to get the selected values again here # because for some unknown reason using the # "selected" variable from the parent scope # will give us the objects but they won't be # in an session...maybe its a thread thing values = self.get_selected_values() result = cb(values) except Exception, e: msg = utils.xml_safe_utf8(str(e)) tb = utils.xml_safe_utf8(traceback.format_exc()) utils.message_details_dialog(msg, tb,gtk.MESSAGE_ERROR) warning(traceback.format_exc())
def run(self, filenames, metadata, force=False): ''' A generator method for importing filenames into the database. This method periodically yields control so that the GUI can update. :param filenames: :param metadata: :param force: default=False ''' transaction = None connection = None self.__error_exc = BaubleError(_('Unknown Error.')) try: # user a contextual connect in case whoever called this # method called it inside a transaction then we can pick # up the parent connection and the transaction connection = metadata.bind.connect() transaction = connection.begin() except Exception, e: msg = _('Error connecting to database.\n\n%s') % \ utils.xml_safe_utf8(e) utils.message_dialog(msg, gtk.MESSAGE_ERROR) return
def on_remove_button_clicked(self, button, data=None): """ Removes the currently selected vernacular name from the view. """ tree = self.view.widgets.vern_treeview path, col = tree.get_cursor() treemodel = tree.get_model() vn = treemodel[path][0] msg = _('Are you sure you want to remove the vernacular ' \ 'name <b>%s</b>?') % utils.xml_safe_utf8(vn.name) if vn.name and not vn in self.session.new and not \ utils.yes_no_dialog(msg, parent=self.view.get_window()): return treemodel.remove(treemodel.get_iter(path)) self.model.vernacular_names.remove(vn) utils.delete_or_expunge(vn) if not self.model.default_vernacular_name: # if there is only one value in the tree then set it as the # default vernacular name first = treemodel.get_iter_first() if first: # self.set_model_attr('default_vernacular_name', # tree_model[first][0]) self.model.default_vernacular_name = treemodel[first][0] self.parent_ref().refresh_sensitivity() self.__dirty = True
def __export_task(self, path, one_file=True): ntables = len(db.metadata.tables) steps_so_far = 0 if not one_file: tableset_el = etree.Element("tableset") for table_name, table in tables.iteritems(): if one_file: tableset_el = etree.Element("tableset") info("exporting %s..." % table_name) table_el = ElementFactory(tableset_el, "table", attrib={"name": table_name}) results = table.select().execute().fetchall() columns = table.c.keys() try: for row in results: row_el = ElementFactory(table_el, "row") for col in columns: ElementFactory(row_el, "column", attrib={"name": col}, text=row[col]) except ValueError, e: utils.message_details_dialog(utils.xml_safe_utf8(e), traceback.format_exc(), gtk.MESSAGE_ERROR) return else: if one_file: tree = etree.ElementTree(tableset_el) filename = os.path.join(path, "%s.xml" % table_name) # TODO: can figure out why this keeps crashing tree.write(filename, encoding="utf8", xml_declaration=True)
def extra_elements(self, unit): bg_unit = ABCDElement(unit, 'BotanicalGardenUnit') ABCDElement(bg_unit, 'AccessionSpecimenNumbers', text=xml_safe_utf8(self.plant.quantity)) ABCDElement(bg_unit, 'LocationInGarden', text=xml_safe_utf8(str(self.plant.location))) if self.for_labels: if self.species.label_distribution: etree.SubElement(unit, 'distribution').text=\ self.species.label_distribution elif self.species.distribution: etree.SubElement(unit, 'distribution').text=\ self.species.distribution_str() # TODO: AccessionStatus, AccessionMaterialtype, # ProvenanceCategory, AccessionLineage, DonorCategory, # PlantingDate, Propagation super(PlantABCDAdapter, self).extra_elements(unit)
def remove_callback(tags): """ :param tags: a list of :class:`Tag` objects. """ tag = tags[0] s = '%s: %s' % (tag.__class__.__name__, utils.xml_safe_utf8(tag)) msg = _("Are you sure you want to remove %s?") % s if not utils.yes_no_dialog(msg): return session = db.Session() try: obj = session.query(Tag).get(tag.id) session.delete(obj) session.commit() except Exception, e: msg = _('Could not delete.\n\n%s') % utils.xml_safe_utf8(e) utils.message_details_dialog(msg, traceback.format_exc(), type=gtk.MESSAGE_ERROR)
def remove_callback(plants): s = ', '.join([str(p) for p in plants]) msg = _("Are you sure you want to remove the following plants?\n\n%s") \ % utils.xml_safe_utf8(s) if not utils.yes_no_dialog(msg): return session = db.Session() for plant in plants: obj = session.query(Plant).get(plant.id) session.delete(obj) try: session.commit() except Exception, e: msg = _('Could not delete.\n\n%s') % utils.xml_safe_utf8(e) utils.message_details_dialog(msg, traceback.format_exc(), type=gtk.MESSAGE_ERROR)
def update(self, row): from textwrap import TextWrapper wrapper = TextWrapper(width=50, subsequent_indent=' ') self.set_widget_value('sd_name_data', '<big>%s</big>' % utils.xml_safe_utf8(row.name), markup=True) source_type = '' if row.source_type: source_type = utils.xml_safe_utf8(row.source_type) self.set_widget_value('sd_type_data', source_type) description = '' if row.description: description = utils.xml_safe_utf8(row.description) self.set_widget_value('sd_desc_data', description) source = Source.__table__ nacc = select([source.c.id], source.c.source_detail_id==row.id).\ count().execute().fetchone()[0] self.set_widget_value('sd_nacc_data', nacc)
def remove_callback(locations): loc = locations[0] s = '%s: %s' % (loc.__class__.__name__, str(loc)) if len(loc.plants) > 0: msg = _('Please remove the plants from <b>%(location)s</b> '\ 'before deleting it.') % {'location': loc} utils.message_dialog(msg, gtk.MESSAGE_WARNING) return msg = _("Are you sure you want to remove %s?") % \ utils.xml_safe_utf8(s) if not utils.yes_no_dialog(msg): return try: session = db.Session() obj = session.query(Location).get(loc.id) session.delete(obj) session.commit() except Exception, e: msg = _('Could not delete.\n\n%s') % utils.xml_safe_utf8(e) utils.message_details_dialog(msg, traceback.format_exc(), type=gtk.MESSAGE_ERROR)
def update_label(self): label = [] date_str = None if self.model.date and isinstance(self.model.date, datetime.date): format = prefs.prefs[prefs.date_format_pref] date_str =utils.xml_safe_utf8(self.model.date.strftime(format)) elif self.model.date: date_str = utils.xml_safe_utf8(self.model.date) else: date_str = self.widgets.date_entry.props.text if self.model.user and date_str:# and self.model.date: label.append(_('%(user)s on %(date)s') % \ dict(user=utils.xml_safe_utf8(self.model.user), date=date_str)) elif date_str: label.append('%s' % date_str) elif self.model.user: label.append('%s' % utils.xml_safe_utf8(self.model.user)) if self.model.category: label.append('(%s)' % utils.xml_safe_utf8(self.model.category)) if self.model.note: note_str = ' : %s' % utils.xml_safe_utf8(self.model.note).\ replace('\n', ' ') max_length = 25 # label.props.ellipsize doesn't work properly on a # label in an expander we just do it ourselves here if len(self.model.note) > max_length: label.append('%s ...' % note_str[0:max_length-1]) else: label.append(note_str) self.widgets.notes_expander.set_label(' '.join(label))
def on_file_menu_new(self, widget, data=None): msg = "If a database already exists at this connection then creating "\ "a new database could destroy your data.\n\n<i>Are you sure "\ "this is what you want to do?</i>" if not utils.yes_no_dialog(msg, yes_delay=2): return #if gui is not None and hasattr(gui, 'insert_menu'): submenu = self.insert_menu.get_submenu() for c in submenu.get_children(): submenu.remove(c) self.insert_menu.show() try: db.create() pluginmgr.init() except Exception, e: msg = _('Could not create a new database.\n\n%s' % \ utils.xml_safe_utf8(e)) tb = utils.xml_safe_utf8(traceback.format_exc()) utils.message_details_dialog(msg, tb, gtk.MESSAGE_ERROR) return
def handle_response(self, response): ''' handle the response from self.presenter.start() in self.start() ''' not_ok_msg = _('Are you sure you want to lose your changes?') if response == gtk.RESPONSE_OK or response in self.ok_responses: try: if self.presenter.dirty(): self.commit_changes() self._committed.append(self.model) except DBAPIError, e: msg = _('Error committing changes.\n\n%s' \ % utils.xml_safe_utf8(e.orig)) utils.message_details_dialog(msg, str(e), gtk.MESSAGE_ERROR) return False except Exception, e: msg = _('Unknown error when committing changes. See the '\ 'details for more information.\n\n%s' \ % utils.xml_safe_utf8(e)) utils.message_details_dialog(msg, traceback.format_exc(), gtk.MESSAGE_ERROR) return False
def handle_response(self, response): ''' @return: return a list if we want to tell start() to close the editor, the list should either be empty or the list of committed values, return None if we want to keep editing ''' not_ok_msg = 'Are you sure you want to lose your changes?' if response == gtk.RESPONSE_OK or response in self.ok_responses: try: if self.presenter.dirty(): self.commit_changes() self._committed.append(self.model) except DBAPIError, e: msg = _('Error committing changes.\n\n%s') % \ utils.xml_safe_utf8(e.orig) utils.message_details_dialog(msg, str(e), gtk.MESSAGE_ERROR) return False except Exception, e: msg = _('Unknown error when committing changes. See the ' \ 'details for more information.\n\n%s') % \ utils.xml_safe_utf8(e) utils.message_details_dialog(msg, traceback.format_exc(), gtk.MESSAGE_ERROR) return False
def load(path=None): """ Search the plugin path for modules that provide a plugin. If path is a directory then search the directory for plugins. If path is None then use the default plugins path, bauble.plugins. This method populates the pluginmgr.plugins dict and imports the plugins but doesn't do any plugin initialization. :param path: the path where to look for the plugins :type path: str """ if path is None: if bauble.main_is_frozen(): #path = os.path.join(paths.lib_dir(), 'library.zip') path = os.path.join(paths.main_dir(), 'library.zip') else: path = os.path.join(paths.lib_dir(), 'plugins') found, errors = _find_plugins(path) # show error dialog for plugins that couldn't be loaded...we only # give details for the first error and assume the others are the # same...and if not then it doesn't really help anyways if errors: name = ', '.join(sorted(errors.keys())) exc_info = errors.values()[0] exc_str = utils.xml_safe_utf8(exc_info[1]) tb_str = ''.join(traceback.format_tb(exc_info[2])) utils.message_details_dialog('Could not load plugin: ' '\n\n<i>%s</i>\n\n%s' \ % (name, exc_str), tb_str, type=gtk.MESSAGE_ERROR) if len(found) == 0: debug('No plugins found at path: %s' % path) for plugin in found: # TODO: should we include the module name of the plugin to allow # for plugin namespaces or just assume that the plugin class # name is unique plugins[plugin.__class__.__name__] = plugin
def handle_response(self, response): not_ok_msg = _('Are you sure you want to lose your changes?') if response == gtk.RESPONSE_OK or response in self.ok_responses: try: if self.presenter.dirty(): # commit_changes() will append the commited plants # to self._committed self.commit_changes() except DBAPIError, e: exc = traceback.format_exc() msg = _('Error committing changes.\n\n%s') % e.orig utils.message_details_dialog(msg, str(e), gtk.MESSAGE_ERROR) self.session.rollback() return False except Exception, e: msg = _('Unknown error when committing changes. See the '\ 'details for more information.\n\n%s') \ % utils.xml_safe_utf8(e) debug(traceback.format_exc()) utils.message_details_dialog(msg, traceback.format_exc(), gtk.MESSAGE_ERROR) self.session.rollback() return False
def _post_loop(): gtk.gdk.threads_enter() try: if isinstance(open_exc, err.DatabaseError): msg = _('Would you like to create a new Bauble database at ' \ 'the current connection?\n\n<i>Warning: If there is '\ 'already a database at this connection any existing '\ 'data will be destroyed!</i>') if utils.yes_no_dialog(msg, yes_delay=2): try: db.create() # db.create() creates all tables registered with # the default metadata so the pluginmgr should be # loaded after the database is created so we don't # inadvertantly create tables from the plugins pluginmgr.init() # set the default connection prefs[conn_default_pref] = conn_name except Exception, e: utils.message_details_dialog(utils.xml_safe_utf8(e), traceback.format_exc(), gtk.MESSAGE_ERROR) error(e) else:
# debug('SearchView.search(%s)' % text) error_msg = None error_details_msg = None self.session.close() # create a new session for each search...maybe we shouldn't # even have session as a class attribute self.session = db.Session() bold = '<b>%s</b>' results = [] try: results = search.search(text, self.session) except ParseException, err: error_msg = _('Error in search string at column %s') % err.column except (BaubleError, AttributeError, Exception, SyntaxError), e: #debug(traceback.format_exc()) error_msg = _('** Error: %s') % utils.xml_safe_utf8(e) error_details_msg = utils.xml_safe_utf8(traceback.format_exc()) if error_msg: bauble.gui.show_error_box(error_msg, error_details_msg) return # not error utils.clear_model(self.results_view) self.set_infobox_from_row(None) statusbar = bauble.gui.widgets.statusbar sbcontext_id = statusbar.get_context_id('searchview.nresults') statusbar.pop(sbcontext_id) if len(results) == 0: model = gtk.ListStore(str) msg = bold % _('Couldn\'t find anything for search: "%s"') \
# has a sequence doesn't update the sequence, we shortcut this # by setting the sequence manually to the max(column)+1 col = None try: for table, filename in sorted_tables: for col in table.c: utils.reset_sequence(col) except Exception, e: col_name = None try: col_name = col.name except Exception: pass msg = _('Error: Could not set the sequence for column: %s') \ % col_name utils.message_details_dialog(_(utils.xml_safe_utf8(msg)), traceback.format_exc(), type=gtk.MESSAGE_ERROR) # TODO: we don't use the progress dialog any more but we'll leave this # around to remind us when we support cancelling via the progress statusbar # # def _cancel_import(self, *args): # ''' # called by the progress dialog to cancel the current import # ''' # msg = _('Are you sure you want to cancel importing?\n\n<i>All ' # 'changes so far will be rolled back.</i>') # self.__pause = True # if utils.yes_no_dialog(msg, parent=self.__progress_dialog):
def get_UnitID(self): return xml_safe_utf8(str(self.plant))
last_handler = handler_cls() handler_view = last_handler.get_view() old_view = gui.get_view() if type(old_view) != type(handler_view) and handler_view: # remove the accel_group from the window if the previous view # had one if hasattr(old_view, 'accel_group'): gui.window.remove_accel_group(old_view.accel_group) # add the new view and its accel_group if it has one gui.set_view(handler_view) if hasattr(handler_view, 'accel_group'): gui.window.add_accel_group(handler_view.accel_group) try: last_handler(cmd, arg) except Exception, e: msg = utils.xml_safe_utf8(e) error('bauble.command_handler(): %s' % msg) utils.message_details_dialog(msg, traceback.format_exc(), gtk.MESSAGE_ERROR) conn_default_pref = "conn.default" conn_list_pref = "conn.list" def main(uri=None): """ Run the main Bauble application. :param uri: the URI of the database to connect to. For more information about database URIs see `<http://www.sqlalchemy.org/docs/05/dbengine.html#create-engine-url-arguments>`_ :type uri: str """
def update(self, row): ''' ''' self.table.foreach(self.table.remove) if not row.changes: return nrows = len(row.changes) self.table.resize(nrows, 2) date_format = prefs.prefs[prefs.date_format_pref] current_row = 0 def _cmp(x, y): """ Sort by change.date and then change._created. If they are equal then removals sort before transfers. """ if x.date < y.date: return -1 elif x.date > y.date: return 1 elif x.date == y.date and x._created < y._created: return -1 elif x.date == y.date and x._created > y._created: return 1 elif x.quantity < 0: return -1 else: return 1 for change in sorted(row.changes, cmp=_cmp, reverse=True): date = change.date.strftime(date_format) label = gtk.Label('%s:' % date) label.set_alignment(0, 0) self.table.attach(label, 0, 1, current_row, current_row+1, xoptions=gtk.FILL) if change.to_location and change.from_location: s = '%(quantity)s Transferred from %(from_loc)s to %(to)s' % \ dict(quantity=change.quantity, from_loc=change.from_location, to=change.to_location) elif change.quantity < 0: s = '%(quantity)s Removed from %(location)s' % \ dict(quantity=-change.quantity, location=change.from_location) elif change.quantity > 0: s = '%(quantity)s Added to %(location)s' % \ dict(quantity=change.quantity, location=change.to_location) else: s = '%s: %s -> %s' % (change.quantity, change.from_location, change.to_location) if change.reason is not None: s += '\n%s' % change_reasons[change.reason] label = gtk.Label(s) label.set_alignment(0, .5) self.table.attach(label, 1, 2, current_row, current_row+1, xoptions=gtk.FILL) current_row += 1 if change.parent_plant: s = _('<i>Branched from %(plant)s</i>') % \ dict(plant=utils.xml_safe_utf8(change.parent_plant)) label = gtk.Label() label.set_alignment(0, .5) label.set_markup(s) eb = gtk.EventBox() eb.add(label) self.table.attach(eb, 1, 2, current_row, current_row+1, xoptions=gtk.FILL) def on_clicked(widget, event, parent): select_in_search_results(parent) utils.make_label_clickable(label, on_clicked, change.parent_plant) current_row += 1 self.vbox.show_all()
def extra_elements(self, unit): super(AccessionABCDAdapter, self).extra_elements(unit) if self.for_labels: if self.species.label_distribution: etree.SubElement(unit, 'distribution').text=\ self.species.label_distribution elif self.species.distribution: etree.SubElement(unit, 'distribution').text=\ self.species.distribution_str() if self.accession.source and self.accession.source.collection: collection = self.accession.source.collection utf8 = xml_safe_utf8 gathering = ABCDElement(unit, 'Gathering') if collection.collectors_code: ABCDElement(gathering, 'Code', text=utf8(collection.collectors_code)) # TODO: get date pref for DayNumberBegin if collection.date: date_time = ABCDElement(gathering, 'DateTime') ABCDElement(date_time, 'DateText', xml_safe_utf8(collection.date.isoformat())) if collection.collector: agents = ABCDElement(gathering, 'Agents') agent = ABCDElement(agents, 'GatheringAgent') ABCDElement(agent, 'AgentText', text=utf8(collection.collector)) if collection.locale: ABCDElement(gathering, 'LocalityText', text=utf8(collection.locale)) if collection.region: named_areas = ABCDElement(gathering, 'NamedAreas') named_area = ABCDElement(named_areas, 'NamedArea') ABCDElement(named_area, 'AreaName', text=utf8(collection.region)) if collection.habitat: ABCDElement(gathering, 'AreaDetail', text=utf8(collection.habitat)) if collection.longitude or collection.latitude: site_coords = ABCDElement(gathering, 'SiteCoordinateSets') coord = ABCDElement(site_coords, 'SiteCoordinates') lat_long = ABCDElement(coord, 'CoordinatesLatLong') ABCDElement(lat_long, 'LongitudeDecimal', text=utf8(collection.longitude)) ABCDElement(lat_long, 'LatitudeDecimal', text=utf8(collection.latitude)) if collection.gps_datum: ABCDElement(lat_long, 'SpatialDatum', text=utf8(collection.gps_datum)) if collection.geo_accy: ABCDElement(coord, 'CoordinateErrorDistanceInMeters', text=utf8(collection.geo_accy)) if collection.elevation: altitude = ABCDElement(gathering, 'Altitude') if collection.elevation_accy: text = '%sm (+/- %sm)' % (collection.elevation, collection.elevation_accy) else: text = '%sm' % collection.elevation ABCDElement(altitude, 'MeasurementOrFactText', text=text) if collection.notes: ABCDElement(gathering, 'Notes', utf8(collection.notes))
def get_DateLastEdited(self): return utils.xml_safe_utf8(self.plant._last_updated.isoformat())