def json_message(success=True, statuscode=None, message=None, **kwargs): """ Provide a nicely-formatted JSON Message @param success: action succeeded or failed @param status_code: the HTTP status code @param message: the message text @param kwargs: other elements for the message @keyword tree: error tree to include as JSON object (rather than as string) for easy decoding """ if statuscode is None: statuscode = success and 200 or 404 status = success and "success" or "failed" code = str(statuscode) output = {"status": status, "statuscode": str(code)} tree = kwargs.get("tree", None) if message: output["message"] = s3_str(message) for k, v in kwargs.items(): if k != "tree": output[k] = v output = json.dumps(output) if message and tree: output = output[:-1] + ', "tree": %s}' % tree return output
def strings(self): """ Add CRUD strings for mobile form @return: a dict with CRUD strings for the resource """ tablename = self.resource.tablename # Use the title specified in deployment setting config = self.config title = config.get("title") # Fall back to CRUD title_list if not title: crud_strings = current.response.s3.crud_strings.get(tablename) if crud_strings: title = crud_strings.get("title_list") # Fall back to capitalized table name if not title: name = tablename.split("_", 1)[-1] title = " ".join(word.capitalize() for word in name.split("_")) # Build strings-dict strings = {} if title: title = s3_str(title) strings["name"] = title strings["namePlural"] = title return strings
def subheadings_l10n(cls, setting): """ Helper to translate form subheadings @param setting: the subheadings-setting (a dict) @return: the subheadings dict with translated headers """ if setting is None: return None T = current.T output = {} for header, fields in setting.items(): if isinstance(fields, dict): # Nested format => recurse subheadings = fields.get("subheadings") fields = { "fields": fields.get("fields"), } if subheadings: fields["subheadings"] = cls.subheadings_l10n(subheadings) output[s3_str(T(header))] = fields return output
def selector(rules): """ Generate the rule selector for anonymize-form @param rules: the list of configured rules @return: the selector (DIV) """ T = current.T selector = DIV(_class="anonymize-select") for rule in rules: name = rule.get("name") if not name: continue title = T(rule.get("title", name)) selector.append(DIV(INPUT(value = "on", _name = s3_str(name), _type = "checkbox", _class = "anonymize-rule", ), LABEL(title), _class = "anonymize-option", )) return selector
def subheadings_l10n(cls, setting): """ Helper to translate form subheadings @param setting: the subheadings-setting (a dict) @return: the subheadings dict with translated headers """ if setting is None: return None T = current.T output = {} for header, fields in setting.items(): if isinstance(fields, dict): # Nested format => recurse subheadings = fields.get("subheadings") fields = {"fields": fields.get("fields"), } if subheadings: fields["subheadings"] = cls.subheadings_l10n(subheadings) output[s3_str(T(header))] = fields return output
def get_options(self, field, lookup=None): """ Get the options for a field with IS_IN_SET @param field: the Field @param lookup: the look-up table name (if field is a foreign key) @return: a list of tuples (key, label) with the field options """ requires = field.requires if not requires: return None if isinstance(requires, (list, tuple)): requires = requires[0] if isinstance(requires, IS_EMPTY_OR): requires = requires.other fieldtype = str(field.type) if fieldtype[:9] == "reference": # For writable foreign keys, if the referenced table # does not expose a mobile form itself, look up all # valid options and report them as schema references: if field.writable and not self.has_mobile_form(lookup): add = self._references[lookup].add # @note: introspection only works with e.g. IS_ONE_OF, # but not with widget-specific validators like # IS_ADD_PERSON_WIDGET2 => should change these # widgets to apply the conversion internally on # the dummy input (like S3LocationSelector), and # then have regular IS_ONE_OF's for the fields if hasattr(requires, "options"): for value, label in requires.options(): if value: add(long(value)) # Foreign keys have no fixed options, however return None elif fieldtype in ("string", "integer"): # Check for IS_IN_SET, and extract the options if isinstance(requires, IS_IN_SET): options = [] for value, label in requires.options(): if value is not None: options.append((value, s3_str(label))) return options else: return None else: # @todo: add other types (may require special option key encoding) return None
def strings(self): """ Add CRUD strings for mobile form @return: a dict with CRUD strings for the resource """ tablename = self.resource.tablename # Use the label/plural specified in deployment setting config = self.config options = config["options"] label = options.get("label") plural = options.get("plural") # Fall back to CRUD title_list if not plural or not label: crud_strings = current.response.s3.crud_strings.get(tablename) if crud_strings: if not label: label = crud_strings.get("title_display") if not plural: plural = crud_strings.get("title_list") # Fall back to the title specified in deployment setting if not plural: plural = config.get("title") # Fall back to capitalized table name if not label: name = tablename.split("_", 1)[-1] label = " ".join(word.capitalize() for word in name.split("_")) # Build strings-dict strings = {} if label: strings["label"] = s3_str(label) if plural: strings["plural"] = s3_str(plural) return strings
def apply_method(self, r, **attr): """ RESTful method handler @param r: the S3Request instance @param attr: controller attributes for the request """ output = {} resource = r.resource if resource.tablename == self.TABLENAME: return resource.crud.select(r, **attr) elif resource.tablename == "sync_repository": # READ for sync log for this repository (currently not needed) pass else: if r.interactive: # READ for sync log for this resource here = "%s.%s" % (r.controller, r.function) sync_log = current.s3db[self.TABLENAME] sync_log.resource_name.readable = False query = (sync_log.resource_name == resource.tablename) r = r.factory(prefix="sync", name="log", args=[]) s3 = current.response.s3 s3.filter = query s3.prep = None s3.postp = None s3.actions = [ { "label": s3_str(current.T("Details")), "_class": "action-btn", "url": URL( c="sync", f="log", args=["[id]"], vars={"return": here}, ) }, ] output = r(subtitle=None, rheader=self.rheader) else: r.error(415, current.ERROR.BAD_FORMAT) return output
def get_options(self, field, lookup=None): """ Get the options for a field with IS_IN_SET @param field: the Field @param lookup: the name of the lookup table @return: a list of tuples (key, label) with the field options """ requires = field.requires if not requires: return None if isinstance(requires, (list, tuple)): requires = requires[0] if isinstance(requires, IS_EMPTY_OR): requires = requires.other fieldtype = str(field.type) if fieldtype[:9] == "reference": # Foreign keys have no fixed options # => must expose the lookup table with data=True in order # to share current field options with the mobile client; # this is better done explicitly in order to run the # data download through the lookup table's controller # for proper authorization, customise_* and filtering # @todo: deliver store uuid<=>label map instead, so that the # mobile client has labels for fk options - unless the # field has a base-class S3Represent with a field list # that can be encoded in the field description return None elif fieldtype in ("string", "integer"): # Check for IS_IN_SET, and extract the options if isinstance(requires, IS_IN_SET): options = [] for value, label in requires.options(): if value is not None: options.append((value, s3_str(label))) return options else: return None else: # @todo: add other types (may require special option key encoding) return None
def apply_method(self, r, **attr): """ RESTful method handler @param r: the S3Request instance @param attr: controller attributes for the request """ output = {} resource = r.resource if resource.tablename == self.TABLENAME: return resource.crud.select(r, **attr) elif resource.tablename == "sync_repository": # READ for sync log for this repository (currently not needed) pass else: if r.interactive: # READ for sync log for this resource here = "%s.%s" % (r.controller, r.function) sync_log = current.s3db[self.TABLENAME] sync_log.resource_name.readable = False query = (sync_log.resource_name == resource.tablename) r = r.factory(prefix="sync", name="log", args=[]) s3 = current.response.s3 s3.filter = query s3.prep = None s3.postp = None s3.actions = [{"label": s3_str(current.T("Details")), "_class": "action-btn", "url": URL(c = "sync", f = "log", args = ["[id]"], vars = {"return":here}, ) }, ] output = r(subtitle=None, rheader=self.rheader) else: r.error(415, current.ERROR.BAD_FORMAT) return output
def add(self, name, obj): """ Add an object to the archive @param name: the file name for the object inside the archive @param obj: the object to add (string or file-like object) @raises UserWarning: when adding a duplicate name (overwrites the existing object in the archive) @raises RuntimeError: if the archive is not writable, or no valid object name has been provided @raises TypeError: if the object is not a unicode, str or file-like object """ # Make sure the object name is an utf-8 encoded str if not name: raise RuntimeError("name is required") elif type(name) is not str: name = s3_str(name) # Make sure the archive is available archive = self.archive if not archive: raise RuntimeError("cannot add to closed archive") # Convert unicode objects to str if type(obj) is unicode: obj = obj.encode("utf-8") # Write the object if type(obj) is str: archive.writestr(name, obj) elif hasattr(obj, "read"): if hasattr(obj, "seek"): obj.seek(0) archive.writestr(name, obj.read()) else: raise TypeError("invalid object type")
def inject_script(cls, agent_id, version=None, widget_class="dashboardWidget", options=None): """ Helper method to inject the init script for a particular agent, usually called by widget() method. @param agent_id: the agent ID @param version: the config version key @param widget_class: the widget class to instantiate @param options: JSON-serializable dict of options to pass to the widget instance """ s3 = current.response.s3 if not agent_id or not widget_class: return if not options: options = {} # Add the widget title (for the configuration popup) title = cls.title if title: options["title"] = s3_str(current.T(title)) # Add the dashboard URL dashboard_url = URL(args=[], vars={}) options["dashboardURL"] = dashboard_url # Add the config version key options["version"] = version script = """$("#%(agent_id)s").%(widget_class)s(%(options)s)""" % \ {"agent_id": agent_id, "widget_class": widget_class, "options": json.dumps(options), } s3.jquery_ready.append(script)
def describe(self, rfield): """ Generate a description of a resource field (for mobile schemas) @param rfield: the S3ResourceField @returns: a JSON-serializable dict describing the field """ field = rfield.field if not field: # Virtual field return None # Basic field description description = { "type": rfield.ftype, "label": s3_str(field.label), } # Field settings if field.notnull: description["notnull"] = True if not field.readable: description["readable"] = False if not field.writable: description["writable"] = False # @todo: options # @todo: minimum # @todo: maximum # @todo: default value # @todo: readable, writable # @todo: placeholder? # @todo: required return description
def describe(self, rfield): """ Generate a description of a resource field (for mobile schemas) @param rfield: the S3ResourceField @returns: a JSON-serializable dict describing the field """ field = rfield.field if not field: # Virtual field return None # Basic field description description = {"type": rfield.ftype, "label": s3_str(field.label), } # Field settings if field.notnull: description["notnull"] = True if not field.readable: description["readable"] = False if not field.writable: description["writable"] = False # @todo: options # @todo: minimum # @todo: maximum # @todo: default value # @todo: readable, writable # @todo: placeholder? # @todo: required return description
def __init__(self): """ Constructor """ T = current.T s3db = current.s3db settings = current.deployment_settings formlist = [] formdict = {} forms = settings.get_mobile_forms() if forms: keys = set() for item in forms: # Parse the configuration options = {} if isinstance(item, (tuple, list)): if len(item) == 2: title, tablename = item if isinstance(tablename, dict): tablename, options = title, tablename title = None elif len(item) == 3: title, tablename, options = item else: continue else: title, tablename = None, item # Make sure table exists table = s3db.table(tablename) if not table: current.log.warning( "Mobile forms: non-existent resource %s" % tablename) continue # Determine controller and function c, f = tablename.split("_", 1) c = options.get("c") or c f = options.get("f") or f # Only expose if target module is enabled if not settings.has_module(c): continue # Determine the form name name = options.get("name") if not name: name = "%s_%s" % (c, f) # Stringify URL query vars url_vars = options.get("vars") if url_vars: items = [] for k in url_vars: v = s3_str(url_vars[k]) url_vars[k] = v items.append("%s=%s" % (k, v)) query = "&".join(sorted(items)) else: query = "" # Deduplicate by target URL key = (c, f, query) if key in keys: continue keys.add(key) # Determine form title if title is None: title = " ".join(w.capitalize() for w in f.split("_")) if isinstance(title, basestring): title = T(title) # Provides (master-)data for download? data = True if options.get("data") else False # Append to form list url = {"c": c, "f": f} if url_vars: url["v"] = url_vars mform = { "n": name, "l": s3_str(title), "t": tablename, "r": url, "d": data, } formlist.append(mform) formdict[name] = mform dynamic_tables = settings.get_mobile_dynamic_tables() if dynamic_tables: # Select all dynamic tables which have mobile_form=True ttable = s3db.s3_table query = (ttable.mobile_form == True) & \ (ttable.deleted != True) rows = current.db(query).select( ttable.name, ttable.title, ) for row in rows: tablename = row.name suffix = tablename.split("_", 1)[-1] # Form title title = row.title if not title: title = " ".join(s.capitalize() for s in suffix.split("_")) # URL # @todo: make c+f configurable? url = { "c": "default", "f": "table/%s" % suffix, } # Append to form list mform = { "n": tablename, "l": title, "t": tablename, "r": url, } formlist.append(mform) formdict[name] = mform self.formlist = formlist self.forms = formdict
def __init__(self): """ Constructor """ T = current.T s3db = current.s3db settings = current.deployment_settings formlist = [] formdict = {} forms = settings.get_mobile_forms() if forms: keys = set() for item in forms: # Parse the configuration options = {} if isinstance(item, (tuple, list)): if len(item) == 2: title, tablename = item if isinstance(tablename, dict): tablename, options = title, tablename title = None elif len(item) == 3: title, tablename, options = item else: continue else: title, tablename = None, item # Make sure table exists table = s3db.table(tablename) if not table: current.log.warning("Mobile forms: non-existent resource %s" % tablename) continue # Determine controller and function c, f = tablename.split("_", 1) c = options.get("c") or c f = options.get("f") or f # Only expose if target module is enabled if not settings.has_module(c): continue # Determine the form name name = options.get("name") if not name: name = "%s_%s" % (c, f) # Stringify URL query vars url_vars = options.get("vars") if url_vars: items = [] for k in url_vars: v = s3_str(url_vars[k]) url_vars[k] = v items.append("%s=%s" % (k, v)) query = "&".join(sorted(items)) else: query = "" # Deduplicate by target URL key = (c, f, query) if key in keys: continue keys.add(key) # Determine form title if title is None: title = " ".join(w.capitalize() for w in f.split("_")) if isinstance(title, basestring): title = T(title) # Provides (master-)data for download? data = True if options.get("data") else False # Exposed for data entry (or just for reference)? main = False if options.get("data_only", False) else True # Append to form list url = {"c": c, "f": f} if url_vars: url["v"] = url_vars mform = {"n": name, "l": s3_str(title), "t": tablename, "r": url, "d": data, "m": main, } formlist.append(mform) formdict[name] = mform dynamic_tables = settings.get_mobile_dynamic_tables() if dynamic_tables: # Select all dynamic tables which have mobile_form=True ttable = s3db.s3_table query = (ttable.mobile_form == True) & \ (ttable.deleted != True) rows = current.db(query).select(ttable.name, ttable.title, ttable.mobile_data, ) for row in rows: tablename = row.name suffix = tablename.split("_", 1)[-1] # Form title title = row.title if not title: title = " ".join(s.capitalize() for s in suffix.split("_")) # URL # @todo: make c+f configurable? url = {"c": "default", "f": "table/%s" % suffix, } # Append to form list mform = {"n": tablename, "l": title, "t": tablename, "r": url, "d": row.mobile_data, } formlist.append(mform) formdict[name] = mform self.formlist = formlist self.forms = formdict
def components(self): """ Add component declarations to the mobile form @return: a dict with component declarations for the resource """ resource = self.resource tablename = resource.tablename pkey = resource._id.name options = self.config.get("options") aliases = set() components = {} # Dynamic components, exposed if: # - "dynamic_components" is True for the master table, and # - "mobile_component" for the component key is not set to False dynamic_components = resource.get_config("dynamic_components") if dynamic_components: # Dynamic components of this table and all its super-entities tablenames = [tablename] supertables = resource.get_config("super_entity") if supertables: if isinstance(supertables, (list, tuple)): tablenames.extend(supertables) elif supertables: tablenames.append(supertables) # Look up corresponding component keys in s3_fields s3db = current.s3db ftable = s3db.s3_field ttable = s3db.s3_table join = ttable.on(ttable.id == ftable.table_id) query = (ftable.component_key == True) & \ (ftable.master.belongs(tablenames)) & \ (ftable.deleted == False) rows = current.db(query).select(ftable.name, ftable.component_alias, ftable.settings, ttable.name, join = join, ) for row in rows: component_key = row.s3_field # Skip if mobile_component is set to False settings = component_key.settings if settings and settings.get("mobile_component") is False: continue alias = component_key.component_alias if not alias: # Default component alias alias = row.s3_table.name.split("_", 1)[-1] aliases.add(alias) # Static components, exposed if # - configured in "components" option of settings.mobile.forms static = options.get("components") if options else None if static: aliases |= set(static) # Construct component descriptions for schema export if aliases: T = current.T hooks = current.s3db.get_components(tablename, names=aliases) for alias, hook in hooks.items(): description = {"table": hook.tablename, "multiple": hook.multiple, } if hook.label: description["label"] = s3_str(T(hook.label)) if hook.plural: description["plural"] = s3_str(T(hook.plural)) if hook.pkey != pkey: description["pkey"] = hook.pkey linktable = hook.linktable if linktable: description.update({"link": str(linktable), "joinby": hook.lkey, "key": hook.rkey, }) if hook.fkey != "id": description["fkey"] = hook.fkey else: description["joinby"] = hook.fkey components[alias] = description return components
def htmlConfig(html, id, orderby, rfields = None, cache = None, **attr ): """ Method to wrap the html for a dataTable in a form, add the export formats and the config details required by dataTables @param html: The html table @param id: The id of the table @param orderby: the sort details see http://datatables.net/reference/option/order @param rfields: The list of resource fields @param attr: dictionary of attributes which can be passed in dt_lengthMenu: The menu options for the number of records to be shown dt_pageLength : The default number of records that will be shown dt_dom : The Datatable DOM initialisation variable, describing the order in which elements are displayed. See http://datatables.net/ref for more details. dt_pagination : Is pagination enabled, dafault 'true' dt_pagingType : How the pagination buttons are displayed dt_searching: Enable or disable filtering of data. dt_ajax_url: The URL to be used for the Ajax call dt_action_col: The column where the action buttons will be placed dt_bulk_actions: list of labels for the bulk actions. dt_bulk_col: The column in which the checkboxes will appear, by default it will be the column immediately before the first data item dt_group: The column(s) that is(are) used to group the data dt_group_totals: The number of record in each group. This will be displayed in parenthesis after the group title. dt_group_titles: The titles to be used for each group. These are a list of lists with the inner list consisting of two values, the repr from the db and the label to display. This can be more than the actual number of groups (giving an empty group). dt_group_space: Insert a space between the group heading and the next group dt_bulk_selected: A list of selected items dt_actions: dictionary of actions dt_styles: dictionary of styles to be applied to a list of ids for example: {"warning" : [1,3,6,7,9], "alert" : [2,10,13]} dt_text_maximum_len: The maximum length of text before it is condensed dt_text_condense_len: The length displayed text is condensed down to dt_shrink_groups: If set then the rows within a group will be hidden two types are supported, 'individual' and 'accordion' dt_group_types: The type of indicator for groups that can be 'shrunk' Permitted valies are: 'icon' (the default) 'text' and 'none' dt_base_url: base URL to construct export format URLs, resource default URL without any URL method or query part @global current.response.s3.actions used to get the RowActions """ from gluon.serializers import json as jsons s3 = current.response.s3 settings = current.deployment_settings dataTableID = s3.dataTableID if not dataTableID or not isinstance(dataTableID, list): dataTableID = s3.dataTableID = [id] elif id not in dataTableID: dataTableID.append(id) # The configuration parameter from the server to the client will be # sent in a json object stored in an hidden input field. This object # will then be parsed by s3.dataTable.js and the values used. config = Storage() config.id = id _aget = attr.get config.dom = _aget("dt_dom", settings.get_ui_datatables_dom()) config.lengthMenu = _aget("dt_lengthMenu", [[25, 50, -1], [25, 50, s3_str(current.T("All"))] ] ) config.pageLength = _aget("dt_pageLength", s3.ROWSPERPAGE) config.pagination = _aget("dt_pagination", "true") config.pagingType = _aget("dt_pagingType", settings.get_ui_datatables_pagingType()) config.searching = _aget("dt_searching", "true") ajaxUrl = _aget("dt_ajax_url", None) if not ajaxUrl: request = current.request url = URL(c=request.controller, f=request.function, args=request.args, vars=request.get_vars, ) ajaxUrl = s3_set_extension(url, "aadata") config.ajaxUrl = ajaxUrl config.rowStyles = _aget("dt_styles", []) rowActions = _aget("dt_row_actions", s3.actions) if rowActions: config.rowActions = rowActions else: config.rowActions = [] bulkActions = _aget("dt_bulk_actions", None) if bulkActions and not isinstance(bulkActions, list): bulkActions = [bulkActions] config.bulkActions = bulkActions config.bulkCol = bulkCol = _aget("dt_bulk_col", 0) action_col = _aget("dt_action_col", 0) if bulkActions and bulkCol <= action_col: action_col += 1 config.actionCol = action_col group_list = _aget("dt_group", []) if not isinstance(group_list, list): group_list = [group_list] dt_group = [] for group in group_list: if bulkActions and bulkCol <= group: group += 1 if action_col >= group: group -= 1 dt_group.append([group, "asc"]) config.group = dt_group config.groupTotals = _aget("dt_group_totals", []) config.groupTitles = _aget("dt_group_titles", []) config.groupSpacing = _aget("dt_group_space", "false") for order in orderby: if bulkActions: if bulkCol <= order[0]: order[0] += 1 if action_col > 0 and action_col >= order[0]: order[0] -= 1 config.order = orderby config.textMaxLength = _aget("dt_text_maximum_len", 80) config.textShrinkLength = _aget("dt_text_condense_len", 75) config.shrinkGroupedRows = _aget("dt_shrink_groups", "false") config.groupIcon = _aget("dt_group_types", []) # Wrap the table in a form and add some data in hidden fields form = FORM(_class="dt-wrapper") if not s3.no_formats: # @todo: move export-format update into drawCallback() # @todo: poor UX with onclick-JS, better to render real # links which can be bookmarked, and then update them # in drawCallback() permalink = _aget("dt_permalink", None) base_url = _aget("dt_base_url", None) export_formats = S3DataTable.export_formats(rfields, permalink=permalink, base_url=base_url) # Nb These can be moved around in initComplete() form.append(export_formats) form.append(html) # Add the configuration details for this dataTable form.append(INPUT(_type="hidden", _id="%s_configurations" % id, _name="config", _value=jsons(config))) # If we have a cache set up then pass it in if cache: form.append(INPUT(_type="hidden", _id="%s_dataTable_cache" %id, _name="cache", _value=jsons(cache))) # If we have bulk actions then add the hidden fields if bulkActions: form.append(INPUT(_type="hidden", _id="%s_dataTable_bulkMode" % id, _name="mode", _value="Inclusive")) bulk_selected = _aget("dt_bulk_selected", "") if isinstance(bulk_selected, list): bulk_selected = ",".join(bulk_selected) form.append(INPUT(_type="hidden", _id="%s_dataTable_bulkSelection" % id, _name="selected", _value="[%s]" % bulk_selected)) form.append(INPUT(_type="hidden", _id="%s_dataTable_filterURL" % id, _class="dataTable_filterURL", _name="filterURL", _value="%s" % config.ajaxUrl)) # Set callback? initComplete = settings.get_ui_datatables_initComplete() if initComplete: # Processed in views/dataTables.html s3.dataTable_initComplete = initComplete return form
def __init__(self): """ Constructor """ T = current.T s3db = current.s3db settings = current.deployment_settings formlist = [] forms = settings.get_mobile_forms() if forms: keys = set() for item in forms: options = {} if isinstance(item, (tuple, list)): if len(item) == 2: title, tablename = item if isinstance(tablename, dict): tablename, options = title, tablename title = None elif len(item) == 3: title, tablename, options = item else: continue else: title, tablename = None, item # Make sure table exists table = s3db.table(tablename) if not table: current.log.warning( "Mobile forms: non-existent resource %s" % tablename) continue # Determine controller and function c, f = tablename.split("_", 1) c = options.get("c") or c f = options.get("f") or f # Only expose if target module is enabled if not settings.has_module(c): continue # Determine the form name name = options.get("name") if not name: name = "%s_%s" % (c, f) # Stringify URL query vars url_vars = options.get("vars") if url_vars: items = [] for k in url_vars: v = s3_str(url_vars[k]) url_vars[k] = v items.append("%s=%s" % (k, v)) query = "&".join(sorted(items)) else: query = "" # Deduplicate by target URL key = (c, f, query) if key in keys: continue keys.add(key) # Determine form title if title is None: title = " ".join(w.capitalize() for w in f.split("_")) if isinstance(title, basestring): title = T(title) # Append to form list url = {"c": c, "f": f} if url_vars: url["v"] = url_vars formlist.append({ "n": name, "l": s3_str(title), "t": tablename, "r": url, }) self.formlist = formlist
def _datatable(self, r, widget, **attr): """ Generate a data table. @param r: the S3Request instance @param widget: the widget definition as dict @param attr: controller attributes for the request @todo: fix export formats """ widget_get = widget.get # Parse context context = widget_get("context") tablename = widget_get("tablename") resource, context = self._resolve_context(r, tablename, context) # List fields list_fields = widget_get("list_fields") if not list_fields: # @ToDo: Set the parent so that the fkey gets removed from the list_fields #resource.parent = s3db.resource("") list_fields = resource.list_fields() # Widget filter option widget_filter = widget_get("filter") if widget_filter: resource.add_filter(widget_filter) # Use the widget-index to create a unique ID list_id = "profile-list-%s-%s" % (tablename, widget["index"]) # Default ORDERBY # - first field actually in this table def default_orderby(): for f in list_fields: selector = f[1] if isinstance(f, tuple) else f if selector == "id": continue rfield = resource.resolve_selector(selector) if rfield.field: return rfield.field return None # Pagination representation = r.representation get_vars = self.request.get_vars if representation == "aadata": start = get_vars.get("displayStart", None) limit = get_vars.get("pageLength", 0) else: start = get_vars.get("start", None) limit = get_vars.get("limit", 0) if limit: if limit.lower() == "none": limit = None else: try: start = int(start) limit = int(limit) except (ValueError, TypeError): start = None limit = 0 # use default else: # Use defaults start = None dtargs = attr.get("dtargs", {}) if r.interactive: s3 = current.response.s3 # How many records per page? if s3.dataTable_pageLength: display_length = s3.dataTable_pageLength else: display_length = widget.get("pagesize", 10) dtargs["dt_lengthMenu"] = [[10, 25, 50, -1], [10, 25, 50, s3_str(current.T("All"))]] # ORDERBY fallbacks: widget->resource->default orderby = widget_get("orderby") if not orderby: orderby = resource.get_config("orderby") if not orderby: orderby = default_orderby() # Server-side pagination? if not s3.no_sspag: dt_pagination = "true" if not limit and display_length is not None: limit = 2 * display_length else: limit = None else: dt_pagination = "false" # Get the data table dt, totalrows, ids = resource.datatable(fields=list_fields, start=start, limit=limit, orderby=orderby) displayrows = totalrows if dt.empty: empty_str = self.crud_string(tablename, "msg_list_empty") else: empty_str = self.crud_string(tablename, "msg_no_match") empty = DIV(empty_str, _class="empty") dtargs["dt_pagination"] = dt_pagination dtargs["dt_pageLength"] = display_length # @todo: fix base URL (make configurable?) to fix export options s3.no_formats = True dtargs["dt_base_url"] = r.url(method="", vars={}) dtargs["dt_ajax_url"] = r.url(vars={"update": widget["index"]}, representation="aadata") actions = widget_get("actions") if callable(actions): actions = actions(r, list_id) if actions: dtargs["dt_row_actions"] = actions datatable = dt.html(totalrows, displayrows, id=list_id, **dtargs) if dt.data: empty.update(_style="display:none") else: datatable.update(_style="display:none") contents = DIV(datatable, empty, _class="dt-contents") # Link for create-popup create_popup = self._create_popup(r, widget, list_id, resource, context, totalrows) # Card holder label and icon label = widget_get("label", "") # Activate if-required #if label and isinstance(label, basestring): if label: label = current.T(label) else: label = self.crud_string(tablename, "title_list") icon = widget_get("icon", "") if icon: icon = ICON(icon) _class = self._lookup_class(r, widget) # Render the widget output = DIV( create_popup, H4(icon, label, _class="profile-sub-header"), DIV(contents, _class="card-holder"), _class=_class, ) return output elif representation == "aadata": # Parse datatable filter/sort query searchq, orderby, left = resource.datatable_filter( list_fields, get_vars) # ORDERBY fallbacks - datatable->widget->resource->default if not orderby: orderby = widget_get("orderby") if not orderby: orderby = resource.get_config("orderby") if not orderby: orderby = default_orderby() # DataTable filtering if searchq is not None: totalrows = resource.count() resource.add_filter(searchq) else: totalrows = None # Get the data table if totalrows != 0: dt, displayrows, ids = resource.datatable(fields=list_fields, start=start, limit=limit, left=left, orderby=orderby, getids=False) else: dt, displayrows = None, 0 if totalrows is None: totalrows = displayrows # Echo draw = int(get_vars.get("draw") or 0) # Representation if dt is not None: data = dt.json(totalrows, displayrows, list_id, draw, **dtargs) else: data = '{"recordsTotal":%s,' \ '"recordsFiltered":0,' \ '"dataTable_id":"%s",' \ '"draw":%s,' \ '"data":[]}' % (totalrows, list_id, draw) return data else: # Really raise an exception here? r.error(415, current.ERROR.BAD_FORMAT)
def merge(self, r, **attr): """ Merge form for two records @param r: the S3Request @param **attr: the controller attributes for the request @note: this method can always only be POSTed, and requires both "selected" and "mode" in post_vars, as well as the duplicate bookmarks list in session.s3 """ T = current.T session = current.session response = current.response output = dict() tablename = self.tablename # Get the duplicate bookmarks s3 = session.s3 DEDUPLICATE = self.DEDUPLICATE if DEDUPLICATE in s3: bookmarks = s3[DEDUPLICATE] if tablename in bookmarks: record_ids = bookmarks[tablename] # Process the post variables post_vars = r.post_vars mode = post_vars.get("mode") selected = post_vars.get("selected", "") selected = selected.split(",") if mode == "Inclusive": ids = selected elif mode == "Exclusive": ids = [i for i in record_ids if i not in selected] else: # Error ids = [] if len(ids) != 2: r.error(501, T("Please select exactly two records"), next=r.url(id=0, vars={})) # Get the selected records table = self.table query = (table._id == ids[0]) | (table._id == ids[1]) orderby = table.created_on if "created_on" in table else None rows = current.db(query).select(orderby=orderby, limitby=(0, 2)) if len(rows) != 2: r.error(404, current.ERROR.BAD_RECORD, next=r.url(id=0, vars={})) original = rows[0] duplicate = rows[1] # Prepare form construction formfields = [f for f in table if f.readable or f.writable] ORIGINAL, DUPLICATE, KEEP = self.ORIGINAL, self.DUPLICATE, self.KEEP keep_o = KEEP.o in post_vars and post_vars[KEEP.o] keep_d = KEEP.d in post_vars and post_vars[KEEP.d] trs = [] init_requires = self.init_requires index = 1 num_fields = len(formfields) for f in formfields: # Render the widgets oid = "%s_%s" % (ORIGINAL, f.name) did = "%s_%s" % (DUPLICATE, f.name) sid = "swap_%s" % f.name init_requires(f, original[f], duplicate[f]) if keep_o or not any((keep_o, keep_d)): owidget = self.widget(f, original[f], _name=oid, _id=oid, _tabindex=index) else: try: owidget = s3_represent_value(f, value=original[f]) except: owidget = s3_str(original[f]) if keep_d or not any((keep_o, keep_d)): dwidget = self.widget(f, duplicate[f], _name=did, _id=did) else: try: dwidget = s3_represent_value(f, value=duplicate[f]) except: dwidget = s3_str(duplicate[f]) # Swap button if not any((keep_o, keep_d)): swap = INPUT(_value="<-->", _class="swap-button", _id=sid, _type="button", _tabindex=index + num_fields) else: swap = DIV(_class="swap-button") if owidget is None or dwidget is None: continue # Render label row label = f.label trs.append( TR(TD(label, _class="w2p_fl"), TD(), TD(label, _class="w2p_fl"))) # Append widget row trs.append( TR(TD(owidget, _class="mwidget"), TD(swap), TD(dwidget, _class="mwidget"))) index = index + 1 # Show created_on/created_by for each record if "created_on" in table: original_date = original.created_on duplicate_date = duplicate.created_on if "created_by" in table: represent = table.created_by.represent original_author = represent(original.created_by) duplicate_author = represent(duplicate.created_by) created = T("Created on %s by %s") original_created = created % (original_date, original_author) duplicate_created = created % (duplicate_date, duplicate_author) else: created = T("Created on %s") original_created = created % original_date duplicate_created = created % duplicate_date else: original_created = "" duplicate_created = "" # Page title and subtitle output["title"] = T("Merge records") #output["subtitle"] = self.crud_string(tablename, "title_list") # Submit buttons if keep_o or not any((keep_o, keep_d)): submit_original = INPUT(_value=T("Keep Original"), _type="submit", _name=KEEP.o, _id=KEEP.o) else: submit_original = "" if keep_d or not any((keep_o, keep_d)): submit_duplicate = INPUT(_value=T("Keep Duplicate"), _type="submit", _name=KEEP.d, _id=KEEP.d) else: submit_duplicate = "" # Build the form form = FORM( TABLE( THEAD( TR( TH(H3(T("Original"))), TH(), TH(H3(T("Duplicate"))), ), TR( TD(original_created), TD(), TD(duplicate_created), _class="authorinfo", ), ), TBODY(trs), TFOOT(TR( TD(submit_original), TD(), TD(submit_duplicate), ), ), ), # Append mode and selected - required to get back here! hidden={ "mode": "Inclusive", "selected": ",".join(ids), }) output["form"] = form # Add RESET and CANCEL options output["reset"] = FORM( INPUT(_value=T("Reset"), _type="submit", _name="reset", _id="form-reset"), A(T("Cancel"), _href=r.url(id=0, vars={}), _class="action-lnk"), hidden={ "mode": mode, "selected": ",".join(ids), }, ) # Process the merge form formname = "merge_%s_%s_%s" % (tablename, original[table._id], duplicate[table._id]) if form.accepts( post_vars, session, formname=formname, onvalidation=lambda form: self.onvalidation(tablename, form), keepvalues=False, hideerror=False): s3db = current.s3db if form.vars[KEEP.d]: prefix = "%s_" % DUPLICATE original, duplicate = duplicate, original else: prefix = "%s_" % ORIGINAL data = Storage() for key in form.vars: if key.startswith(prefix): fname = key.split("_", 1)[1] data[fname] = form.vars[key] search = False resource = s3db.resource(tablename) try: resource.merge(original[table._id], duplicate[table._id], update=data) except current.auth.permission.error: r.unauthorized() except KeyError: r.error(404, current.ERROR.BAD_RECORD) except Exception: import sys r.error(424, T("Could not merge records. (Internal Error: %s)") % sys.exc_info()[1], next=r.url()) else: # Cleanup bookmark list if mode == "Inclusive": bookmarks[tablename] = [ i for i in record_ids if i not in ids ] if not bookmarks[tablename]: del bookmarks[tablename] search = True elif mode == "Exclusive": bookmarks[tablename].extend(ids) if not selected: search = True # Confirmation message # @todo: Having the link to the merged record in the confirmation # message would be nice, but it's currently not clickable there :/ #result = A(T("Open the merged record"), #_href=r.url(method="read", #id=original[table._id], #vars={})) response.confirmation = T("Records merged successfully.") # Go back to bookmark list if search: self.next = r.url(method="", id=0, vars={}) else: self.next = r.url(id=0, vars={}) # View response.view = self._view(r, "merge.html") return output
def organizer(self, r, **attr): """ Render the organizer view (HTML method) @param r: the S3Request instance @param attr: controller attributes @returns: dict of values for the view """ output = {} resource = self.resource get_config = resource.get_config # Parse resource configuration config = self.parse_config(resource) start = config["start"] end = config["end"] widget_id = "organizer" # Filter Defaults hide_filter = self.hide_filter filter_widgets = get_config("filter_widgets", None) show_filter_form = False default_filters = None if filter_widgets and not hide_filter: # Drop all filter widgets for start/end fields # (so they don't clash with the organizer's own filters) fw = [] prefix_selector = self.prefix_selector for filter_widget in filter_widgets: if not filter_widget: continue filter_field = filter_widget.field if isinstance(filter_field, basestring): filter_field = prefix_selector(resource, filter_field) if start and start.selector == filter_field or \ end and end.selector == filter_field: continue fw.append(filter_widget) filter_widgets = fw if filter_widgets: show_filter_form = True # Apply filter defaults (before rendering the data!) from s3filter import S3FilterForm default_filters = S3FilterForm.apply_filter_defaults( r, resource) # Filter Form if show_filter_form: get_vars = r.get_vars # Where to retrieve filtered data from filter_submit_url = attr.get("filter_submit_url") if not filter_submit_url: get_vars_ = self._remove_filters(get_vars) filter_submit_url = r.url(vars=get_vars_) # Where to retrieve updated filter options from: filter_ajax_url = attr.get("filter_ajax_url") if filter_ajax_url is None: filter_ajax_url = r.url( method="filter", vars={}, representation="options", ) filter_clear = get_config( "filter_clear", current.deployment_settings.get_ui_filter_clear()) filter_formstyle = get_config("filter_formstyle", None) filter_submit = get_config("filter_submit", True) filter_form = S3FilterForm(filter_widgets, clear=filter_clear, formstyle=filter_formstyle, submit=filter_submit, ajax=True, url=filter_submit_url, ajaxurl=filter_ajax_url, _class="filter-form", _id="%s-filter-form" % widget_id) fresource = current.s3db.resource( resource.tablename) # Use a clean resource alias = resource.alias if r.component else None output["list_filter_form"] = filter_form.html(fresource, get_vars, target=widget_id, alias=alias) else: # Render as empty string to avoid the exception in the view output["list_filter_form"] = "" # Page Title crud_string = self.crud_string if r.representation != "iframe": if r.component: title = crud_string(r.tablename, "title_display") else: title = crud_string(self.tablename, "title_list") output["title"] = title # Configure Resource permitted = self._permitted resource_config = {"ajaxURL": r.url(representation="json"), "useTime": config.get("use_time"), "baseURL": r.url(method=""), "labelCreate": s3_str(crud_string(self.tablename, "label_create")), "insertable": resource.get_config("insertable", True) and \ permitted("create"), "editable": resource.get_config("editable", True) and \ permitted("update"), "startEditable": start.field and start.field.writable, "durationEditable": end and end.field and end.field.writable, "deletable": resource.get_config("deletable", True) and \ permitted("delete"), # Forced reload on update, e.g. if onaccept changes # other data that are visible in the organizer "reloadOnUpdate": config.get("reload_on_update", False), } # Start and End Field resource_config["start"] = start.selector if start else None resource_config["end"] = end.selector if end else None # Description Labels labels = [] for rfield in config["description"]: label = rfield.label if label is not None: label = s3_str(label) labels.append((rfield.colname, label)) resource_config["columns"] = labels # Colors color = config.get("color") if color: resource_config["color"] = color.colname resource_config["colors"] = config.get("colors") # Generate form key formkey = uuid.uuid4().get_hex() # Store form key in session session = current.session keyname = "_formkey[%s]" % self.formname(r) session[keyname] = session.get(keyname, [])[-9:] + [formkey] # Instantiate Organizer Widget widget = S3OrganizerWidget([resource_config]) output["organizer"] = widget.html( widget_id=widget_id, formkey=formkey, ) # View current.response.view = self._view(r, "organize.html") return output
def get_json_data(self, r, **attr): """ Extract the resource data and return them as JSON (Ajax method) @param r: the S3Request instance @param attr: controller attributes TODO correct documentation! @returns: JSON string containing an array of items, format: [{"id": the record ID, "title": the record title, "start": start date as ISO8601 string, "end": end date as ISO8601 string (if resource has end dates), "description": array of item values to render a description, TODO: "editable": item date/duration can be changed (true|false), "deletable": item can be deleted (true|false), }, ... ] """ db = current.db auth = current.auth resource = self.resource table = resource.table id_col = str(resource._id) config = self.parse_config(resource) # Determine fields to load fields = [resource._id.name] start_rfield = config["start"] fields.append(start_rfield) end_rfield = config["end"] if end_rfield: fields.append(end_rfield) represent = config["title"] if hasattr(represent, "selector"): title_field = represent.colname fields.append(represent) else: title_field = None description = config["description"] if description: fields.extend(description) columns = [rfield.colname for rfield in description] else: columns = None color = config["color"] if color: fields.append(color) # Add date filter start, end = self.parse_interval(r.get_vars.get("$interval")) if start and end: from s3query import FS start_fs = FS(start_rfield.selector) if not end_rfield: query = (start_fs >= start) & (start_fs < end) else: end_fs = FS(end_rfield.selector) query = (start_fs < end) & (end_fs >= start) | \ (start_fs >= start) & (start_fs < end) & (end_fs == None) resource.add_filter(query) else: r.error(400, "Invalid interval parameter") # Extract the records data = resource.select( fields, limit=None, raw_data=True, represent=True, ) rows = data.rows # Bulk-represent the records record_ids = [row._row[id_col] for row in rows] if hasattr(represent, "bulk"): representations = represent.bulk(record_ids) else: representations = None # Determine which records can be updated/deleted query = table.id.belongs(record_ids) q = query & auth.s3_accessible_query("update", table) accessible_rows = db(q).select( table._id, limitby=(0, len(record_ids)), ) editable = set(row[id_col] for row in accessible_rows) q = query & auth.s3_accessible_query("delete", table) accessible_rows = db(q).select( table._id, limitby=(0, len(record_ids)), ) deletable = set(row[id_col] for row in accessible_rows) # Encode the items items = [] for row in rows: raw = row._row record_id = raw[id_col] # Get the start date if start_rfield: start_date = self.isoformat(raw[start_rfield.colname]) else: start_date = None if start_date is None: # Undated item => skip continue # Construct item title if title_field: title = row[title_field] elif representations: title = representations.get(record_id) elif callable(represent): title = represent(record_id) else: # Fallback: record ID title = row[id_col] # Build the item item = { "id": record_id, "t": s3_str(title), "s": start_date, "pe": 1 if record_id in editable else 0, "pd": 1 if record_id in deletable else 0, } if end_rfield: end_date = self.isoformat(raw[end_rfield.colname]) item["e"] = end_date if columns: data = [] for colname in columns: value = row[colname] if value is not None: value = s3_str(value) data.append(value) item["d"] = data if color: item["c"] = raw[color.colname] items.append(item) return json.dumps({"c": columns, "r": items})
def __init__(self): """ Constructor """ T = current.T s3db = current.s3db settings = current.deployment_settings formlist = [] forms = settings.get_mobile_forms() if forms: keys = set() for item in forms: options = {} if isinstance(item, (tuple, list)): if len(item) == 2: title, tablename = item if isinstance(tablename, dict): tablename, options = title, tablename title = None elif len(item) == 3: title, tablename, options = item else: continue else: title, tablename = None, item # Make sure table exists table = s3db.table(tablename) if not table: current.log.warning("Mobile forms: non-existent resource %s" % tablename) continue # Determine controller and function c, f = tablename.split("_", 1) c = options.get("c") or c f = options.get("f") or f # Only expose if target module is enabled if not settings.has_module(c): continue # Determine the form name name = options.get("name") if not name: name = "%s_%s" % (c, f) # Stringify URL query vars url_vars = options.get("vars") if url_vars: items = [] for k in url_vars: v = s3_str(url_vars[k]) url_vars[k] = v items.append("%s=%s" % (k, v)) query = "&".join(sorted(items)) else: query = "" # Deduplicate by target URL key = (c, f, query) if key in keys: continue keys.add(key) # Determine form title if title is None: title = " ".join(w.capitalize() for w in f.split("_")) if isinstance(title, basestring): title = T(title) # Append to form list url = {"c": c, "f": f} if url_vars: url["v"] = url_vars formlist.append({"n": name, "l": s3_str(title), "t": tablename, "r": url, }) self.formlist = formlist
def _datatable(self, r, widget, **attr): """ Generate a data table. @param r: the S3Request instance @param widget: the widget definition as dict @param attr: controller attributes for the request @todo: fix export formats """ widget_get = widget.get # Parse context context = widget_get("context") tablename = widget_get("tablename") resource, context = self._resolve_context(r, tablename, context) # List fields list_fields = widget_get("list_fields") if not list_fields: # @ToDo: Set the parent so that the fkey gets removed from the list_fields #resource.parent = s3db.resource("") list_fields = resource.list_fields() # Widget filter option widget_filter = widget_get("filter") if widget_filter: resource.add_filter(widget_filter) # Use the widget-index to create a unique ID list_id = "profile-list-%s-%s" % (tablename, widget["index"]) # Default ORDERBY # - first field actually in this table def default_orderby(): for f in list_fields: selector = f[1] if isinstance(f, tuple) else f if selector == "id": continue rfield = resource.resolve_selector(selector) if rfield.field: return rfield.field return None # Pagination representation = r.representation get_vars = self.request.get_vars if representation == "aadata": start = get_vars.get("displayStart", None) limit = get_vars.get("pageLength", 0) else: start = get_vars.get("start", None) limit = get_vars.get("limit", 0) if limit: if limit.lower() == "none": limit = None else: try: start = int(start) limit = int(limit) except (ValueError, TypeError): start = None limit = 0 # use default else: # Use defaults start = None dtargs = attr.get("dtargs", {}) if r.interactive: s3 = current.response.s3 # How many records per page? if s3.dataTable_pageLength: display_length = s3.dataTable_pageLength else: display_length = widget.get("pagesize", 10) dtargs["dt_lengthMenu"] = [[10, 25, 50, -1], [10, 25, 50, s3_str(current.T("All"))] ] # ORDERBY fallbacks: widget->resource->default orderby = widget_get("orderby") if not orderby: orderby = resource.get_config("orderby") if not orderby: orderby = default_orderby() # Server-side pagination? if not s3.no_sspag: dt_pagination = "true" if not limit and display_length is not None: limit = 2 * display_length else: limit = None else: dt_pagination = "false" # Get the data table dt, totalrows, ids = resource.datatable(fields=list_fields, start=start, limit=limit, orderby=orderby) displayrows = totalrows if dt.empty: empty_str = self.crud_string(tablename, "msg_list_empty") else: empty_str = self.crud_string(tablename, "msg_no_match") empty = DIV(empty_str, _class="empty") dtargs["dt_pagination"] = dt_pagination dtargs["dt_pageLength"] = display_length # @todo: fix base URL (make configurable?) to fix export options s3.no_formats = True dtargs["dt_base_url"] = r.url(method="", vars={}) dtargs["dt_ajax_url"] = r.url(vars={"update": widget["index"]}, representation="aadata") actions = widget_get("actions") if callable(actions): actions = actions(r, list_id) if actions: dtargs["dt_row_actions"] = actions datatable = dt.html(totalrows, displayrows, id=list_id, **dtargs) if dt.data: empty.update(_style="display:none") else: datatable.update(_style="display:none") contents = DIV(datatable, empty, _class="dt-contents") # Link for create-popup create_popup = self._create_popup(r, widget, list_id, resource, context, totalrows) # Card holder label and icon label = widget_get("label", "") # Activate if-required #if label and isinstance(label, basestring): if label: label = current.T(label) else: label = self.crud_string(tablename, "title_list") icon = widget_get("icon", "") if icon: icon = ICON(icon) _class = self._lookup_class(r, widget) # Render the widget output = DIV(create_popup, H4(icon, label, _class="profile-sub-header"), DIV(contents, _class="card-holder"), _class=_class, ) return output elif representation == "aadata": # Parse datatable filter/sort query searchq, orderby, left = resource.datatable_filter(list_fields, get_vars) # ORDERBY fallbacks - datatable->widget->resource->default if not orderby: orderby = widget_get("orderby") if not orderby: orderby = resource.get_config("orderby") if not orderby: orderby = default_orderby() # DataTable filtering if searchq is not None: totalrows = resource.count() resource.add_filter(searchq) else: totalrows = None # Get the data table if totalrows != 0: dt, displayrows, ids = resource.datatable(fields=list_fields, start=start, limit=limit, left=left, orderby=orderby, getids=False) else: dt, displayrows = None, 0 if totalrows is None: totalrows = displayrows # Echo draw = int(get_vars.get("draw") or 0) # Representation if dt is not None: data = dt.json(totalrows, displayrows, list_id, draw, **dtargs) else: data = '{"recordsTotal":%s,' \ '"recordsFiltered":0,' \ '"dataTable_id":"%s",' \ '"draw":%s,' \ '"data":[]}' % (totalrows, list_id, draw) return data else: # Really raise an exception here? r.error(415, current.ERROR.BAD_FORMAT)
def send(cls, r, resource): """ Method to retrieve updates for a subscription, render the notification message and send it - responds to POST?format=msg requests to the respective resource. @param r: the S3Request @param resource: the S3Resource """ _debug = current.log.debug _debug("S3Notifications.send()") json_message = current.xml.json_message # Read subscription data source = r.body source.seek(0) data = source.read() subscription = json.loads(data) #_debug("Notify PE #%s by %s on %s of %s since %s" % \ # (subscription["pe_id"], # str(subscription["method"]), # str(subscription["notify_on"]), # subscription["resource"], # subscription["last_check_time"], # )) # Check notification settings notify_on = subscription["notify_on"] methods = subscription["method"] if not notify_on or not methods: return json_message(message="No notifications configured " "for this subscription") # Authorization (pe_id must not be None) pe_id = subscription["pe_id"] if not pe_id: r.unauthorised() # Fields to extract fields = resource.list_fields(key="notify_fields") if "created_on" not in fields: fields.append("created_on") # Extract the data data = resource.select(fields, represent=True, raw_data=True) rows = data["rows"] # How many records do we have? numrows = len(rows) if not numrows: return json_message(message="No records found") #_debug("%s rows:" % numrows) # Prepare meta-data get_config = resource.get_config settings = current.deployment_settings page_url = subscription["page_url"] crud_strings = current.response.s3.crud_strings.get(resource.tablename) if crud_strings: resource_name = crud_strings.title_list else: resource_name = string.capwords(resource.name, "_") last_check_time = s3_decode_iso_datetime( subscription["last_check_time"]) email_format = subscription["email_format"] if not email_format: email_format = settings.get_msg_notify_email_format() filter_query = subscription.get("filter_query") meta_data = { "systemname": settings.get_system_name(), "systemname_short": settings.get_system_name_short(), "resource": resource_name, "page_url": page_url, "notify_on": notify_on, "last_check_time": last_check_time, "filter_query": filter_query, "total_rows": numrows, } # Render contents for the message template(s) renderer = get_config("notify_renderer") if not renderer: renderer = settings.get_msg_notify_renderer() if not renderer: renderer = cls._render contents = {} if email_format == "html" and "EMAIL" in methods: contents["html"] = renderer(resource, data, meta_data, "html") contents["default"] = contents["html"] if email_format != "html" or "EMAIL" not in methods or len( methods) > 1: contents["text"] = renderer(resource, data, meta_data, "text") contents["default"] = contents["text"] # Subject line subject = get_config("notify_subject") if not subject: subject = settings.get_msg_notify_subject() if callable(subject): subject = subject(resource, data, meta_data) from string import Template subject = Template(subject).safe_substitute(S="%(systemname)s", s="%(systemname_short)s", r="%(resource)s") subject = subject % meta_data # Attachment attachment = subscription.get("attachment", False) document_ids = None if attachment: attachment_fnc = settings.get_msg_notify_attachment() if attachment_fnc: document_ids = attachment_fnc(resource, data, meta_data) # **data for send_by_pe_id function in s3msg send_data = {} send_data_fnc = settings.get_msg_notify_send_data() if callable(send_data_fnc): send_data = send_data_fnc(resource, data, meta_data) # Helper function to find templates from a priority list join = lambda *f: os.path.join(current.request.folder, *f) def get_template(path, filenames): for fn in filenames: filepath = join(path, fn) if os.path.exists(filepath): try: return open(filepath, "rb") except: pass return None # Render and send the message(s) themes = settings.get_template() prefix = resource.get_config("notify_template", "notify") send = current.msg.send_by_pe_id success = False errors = [] for method in methods: error = None # Get the message template template = None filenames = ["%s_%s.html" % (prefix, method.lower())] if method == "EMAIL" and email_format: filenames.insert(0, "%s_email_%s.html" % (prefix, email_format)) if themes != "default": location = settings.get_template_location() if not isinstance(themes, (tuple, list)): themes = (themes, ) for theme in themes[::-1]: path = join(location, "templates", theme, "views", "msg") template = get_template(path, filenames) if template is not None: break if template is None: path = join("views", "msg") template = get_template(path, filenames) if template is None: template = StringIO( s3_str(current.T("New updates are available."))) # Select contents format if method == "EMAIL" and email_format == "html": output = contents["html"] else: output = contents["text"] # Render the message try: message = current.response.render(template, output) except: exc_info = sys.exc_info()[:2] error = ("%s: %s" % (exc_info[0].__name__, exc_info[1])) errors.append(error) continue if not message: continue # Send the message #_debug("Sending message per %s" % method) #_debug(message) try: sent = send( pe_id, # RFC 2822 subject=s3_truncate(subject, 78), message=message, contact_method=method, system_generated=True, document_ids=document_ids, **send_data) except: exc_info = sys.exc_info()[:2] error = ("%s: %s" % (exc_info[0].__name__, exc_info[1])) sent = False if sent: # Successful if at least one notification went out success = True else: if not error: error = current.session.error if isinstance(error, list): error = "/".join(error) if error: errors.append(error) # Done if errors: message = ", ".join(errors) else: message = "Success" return json_message(success=success, statuscode=200 if success else 403, message=message)
def merge(self, r, **attr): """ Merge form for two records @param r: the S3Request @param **attr: the controller attributes for the request @note: this method can always only be POSTed, and requires both "selected" and "mode" in post_vars, as well as the duplicate bookmarks list in session.s3 """ T = current.T session = current.session response = current.response output = dict() tablename = self.tablename # Get the duplicate bookmarks s3 = session.s3 DEDUPLICATE = self.DEDUPLICATE if DEDUPLICATE in s3: bookmarks = s3[DEDUPLICATE] if tablename in bookmarks: record_ids = bookmarks[tablename] # Process the post variables post_vars = r.post_vars mode = post_vars.get("mode") selected = post_vars.get("selected", "") selected = selected.split(",") if mode == "Inclusive": ids = selected elif mode == "Exclusive": ids = [i for i in record_ids if i not in selected] else: # Error ids = [] if len(ids) != 2: r.error(501, T("Please select exactly two records"), next = r.url(id=0, vars={})) # Get the selected records table = self.table query = (table._id == ids[0]) | (table._id == ids[1]) orderby = table.created_on if "created_on" in table else None rows = current.db(query).select(orderby=orderby, limitby=(0, 2)) if len(rows) != 2: r.error(404, current.ERROR.BAD_RECORD, next = r.url(id=0, vars={})) original = rows[0] duplicate = rows[1] # Prepare form construction formfields = [f for f in table if f.readable or f.writable] ORIGINAL, DUPLICATE, KEEP = self.ORIGINAL, self.DUPLICATE, self.KEEP keep_o = KEEP.o in post_vars and post_vars[KEEP.o] keep_d = KEEP.d in post_vars and post_vars[KEEP.d] trs = [] init_requires = self.init_requires index = 1 num_fields = len(formfields) for f in formfields: # Render the widgets oid = "%s_%s" % (ORIGINAL, f.name) did = "%s_%s" % (DUPLICATE, f.name) sid = "swap_%s" % f.name init_requires(f, original[f], duplicate[f]) if keep_o or not any((keep_o, keep_d)): owidget = self.widget(f, original[f], _name=oid, _id=oid, _tabindex=index) else: try: owidget = s3_represent_value(f, value=original[f]) except: owidget = s3_str(original[f]) if keep_d or not any((keep_o, keep_d)): dwidget = self.widget(f, duplicate[f], _name=did, _id=did) else: try: dwidget = s3_represent_value(f, value=duplicate[f]) except: dwidget = s3_str(duplicate[f]) # Swap button if not any((keep_o, keep_d)): swap = INPUT(_value="<-->", _class="swap-button", _id=sid, _type="button", _tabindex = index+num_fields) else: swap = DIV(_class="swap-button") if owidget is None or dwidget is None: continue # Render label row label = f.label trs.append(TR(TD(label, _class="w2p_fl"), TD(), TD(label, _class="w2p_fl"))) # Append widget row trs.append(TR(TD(owidget, _class="mwidget"), TD(swap), TD(dwidget, _class="mwidget"))) index = index + 1 # Show created_on/created_by for each record if "created_on" in table: original_date = original.created_on duplicate_date = duplicate.created_on if "created_by" in table: represent = table.created_by.represent original_author = represent(original.created_by) duplicate_author = represent(duplicate.created_by) created = T("Created on %s by %s") original_created = created % (original_date, original_author) duplicate_created = created % (duplicate_date, duplicate_author) else: created = T("Created on %s") original_created = created % original_date duplicate_created = created % duplicate_date else: original_created = "" duplicate_created = "" # Page title and subtitle output["title"] = T("Merge records") #output["subtitle"] = self.crud_string(tablename, "title_list") # Submit buttons if keep_o or not any((keep_o, keep_d)): submit_original = INPUT(_value=T("Keep Original"), _type="submit", _name=KEEP.o, _id=KEEP.o) else: submit_original = "" if keep_d or not any((keep_o, keep_d)): submit_duplicate = INPUT(_value=T("Keep Duplicate"), _type="submit", _name=KEEP.d, _id=KEEP.d) else: submit_duplicate = "" # Build the form form = FORM(TABLE( THEAD( TR(TH(H3(T("Original"))), TH(), TH(H3(T("Duplicate"))), ), TR(TD(original_created), TD(), TD(duplicate_created), _class="authorinfo", ), ), TBODY(trs), TFOOT( TR(TD(submit_original), TD(), TD(submit_duplicate), ), ), ), # Append mode and selected - required to get back here! hidden = { "mode": "Inclusive", "selected": ",".join(ids), } ) output["form"] = form # Add RESET and CANCEL options output["reset"] = FORM(INPUT(_value=T("Reset"), _type="submit", _name="reset", _id="form-reset"), A(T("Cancel"), _href=r.url(id=0, vars={}), _class="action-lnk"), hidden = {"mode": mode, "selected": ",".join(ids), }, ) # Process the merge form formname = "merge_%s_%s_%s" % (tablename, original[table._id], duplicate[table._id]) if form.accepts(post_vars, session, formname=formname, onvalidation=lambda form: self.onvalidation(tablename, form), keepvalues=False, hideerror=False): s3db = current.s3db if form.vars[KEEP.d]: prefix = "%s_" % DUPLICATE original, duplicate = duplicate, original else: prefix = "%s_" % ORIGINAL data = Storage() for key in form.vars: if key.startswith(prefix): fname = key.split("_", 1)[1] data[fname] = form.vars[key] search = False resource = s3db.resource(tablename) try: resource.merge(original[table._id], duplicate[table._id], update=data) except current.auth.permission.error: r.unauthorized() except KeyError: r.error(404, current.ERROR.BAD_RECORD) except Exception: import sys r.error(424, T("Could not merge records. (Internal Error: %s)") % sys.exc_info()[1], next=r.url()) else: # Cleanup bookmark list if mode == "Inclusive": bookmarks[tablename] = [i for i in record_ids if i not in ids] if not bookmarks[tablename]: del bookmarks[tablename] search = True elif mode == "Exclusive": bookmarks[tablename].extend(ids) if not selected: search = True # Confirmation message # @todo: Having the link to the merged record in the confirmation # message would be nice, but it's currently not clickable there :/ #result = A(T("Open the merged record"), #_href=r.url(method="read", #id=original[table._id], #vars={})) response.confirmation = T("Records merged successfully.") # Go back to bookmark list if search: self.next = r.url(method="", id=0, vars={}) else: self.next = r.url(id=0, vars={}) # View response.view = self._view(r, "merge.html") return output
def htmlConfig(html, id, orderby, rfields=None, cache=None, **attr): """ Method to wrap the html for a dataTable in a form, add the export formats and the config details required by dataTables @param html: The html table @param id: The id of the table @param orderby: the sort details see http://datatables.net/reference/option/order @param rfields: The list of resource fields @param attr: dictionary of attributes which can be passed in dt_lengthMenu: The menu options for the number of records to be shown dt_pageLength : The default number of records that will be shown dt_dom : The Datatable DOM initialisation variable, describing the order in which elements are displayed. See http://datatables.net/ref for more details. dt_pagination : Is pagination enabled, dafault 'true' dt_pagingType : How the pagination buttons are displayed dt_searching: Enable or disable filtering of data. dt_ajax_url: The URL to be used for the Ajax call dt_action_col: The column where the action buttons will be placed dt_bulk_actions: list of labels for the bulk actions. dt_bulk_col: The column in which the checkboxes will appear, by default it will be the column immediately before the first data item dt_group: The column(s) that is(are) used to group the data dt_group_totals: The number of record in each group. This will be displayed in parenthesis after the group title. dt_group_titles: The titles to be used for each group. These are a list of lists with the inner list consisting of two values, the repr from the db and the label to display. This can be more than the actual number of groups (giving an empty group). dt_group_space: Insert a space between the group heading and the next group dt_bulk_selected: A list of selected items dt_actions: dictionary of actions dt_styles: dictionary of styles to be applied to a list of ids for example: {"warning" : [1,3,6,7,9], "alert" : [2,10,13]} dt_text_maximum_len: The maximum length of text before it is condensed dt_text_condense_len: The length displayed text is condensed down to dt_shrink_groups: If set then the rows within a group will be hidden two types are supported, 'individual' and 'accordion' dt_group_types: The type of indicator for groups that can be 'shrunk' Permitted valies are: 'icon' (the default) 'text' and 'none' dt_base_url: base URL to construct export format URLs, resource default URL without any URL method or query part @global current.response.s3.actions used to get the RowActions """ from gluon.serializers import json as jsons s3 = current.response.s3 settings = current.deployment_settings dataTableID = s3.dataTableID if not dataTableID or not isinstance(dataTableID, list): dataTableID = s3.dataTableID = [id] elif id not in dataTableID: dataTableID.append(id) # The configuration parameter from the server to the client will be # sent in a json object stored in an hidden input field. This object # will then be parsed by s3.dataTable.js and the values used. config = Storage() config.id = id _aget = attr.get config.dom = _aget("dt_dom", settings.get_ui_datatables_dom()) config.lengthMenu = _aget( "dt_lengthMenu", [[25, 50, -1], [25, 50, s3_str(current.T("All"))]]) config.pageLength = _aget("dt_pageLength", s3.ROWSPERPAGE) config.pagination = _aget("dt_pagination", "true") config.pagingType = _aget("dt_pagingType", settings.get_ui_datatables_pagingType()) config.searching = _aget("dt_searching", "true") ajaxUrl = _aget("dt_ajax_url", None) if not ajaxUrl: request = current.request url = URL( c=request.controller, f=request.function, args=request.args, vars=request.get_vars, ) ajaxUrl = s3_set_extension(url, "aadata") config.ajaxUrl = ajaxUrl config.rowStyles = _aget("dt_styles", []) rowActions = _aget("dt_row_actions", s3.actions) if rowActions: config.rowActions = rowActions else: config.rowActions = [] bulkActions = _aget("dt_bulk_actions", None) if bulkActions and not isinstance(bulkActions, list): bulkActions = [bulkActions] config.bulkActions = bulkActions config.bulkCol = bulkCol = _aget("dt_bulk_col", 0) action_col = _aget("dt_action_col", 0) if bulkActions and bulkCol <= action_col: action_col += 1 config.actionCol = action_col group_list = _aget("dt_group", []) if not isinstance(group_list, list): group_list = [group_list] dt_group = [] for group in group_list: if bulkActions and bulkCol <= group: group += 1 if action_col >= group: group -= 1 dt_group.append([group, "asc"]) config.group = dt_group config.groupTotals = _aget("dt_group_totals", []) config.groupTitles = _aget("dt_group_titles", []) config.groupSpacing = _aget("dt_group_space", "false") for order in orderby: if bulkActions: if bulkCol <= order[0]: order[0] += 1 if action_col > 0 and action_col >= order[0]: order[0] -= 1 config.order = orderby config.textMaxLength = _aget("dt_text_maximum_len", 80) config.textShrinkLength = _aget("dt_text_condense_len", 75) config.shrinkGroupedRows = _aget("dt_shrink_groups", "false") config.groupIcon = _aget("dt_group_types", []) # Wrap the table in a form and add some data in hidden fields form = FORM(_class="dt-wrapper") if not s3.no_formats: # @todo: move export-format update into drawCallback() # @todo: poor UX with onclick-JS, better to render real # links which can be bookmarked, and then update them # in drawCallback() permalink = _aget("dt_permalink", None) base_url = _aget("dt_base_url", None) export_formats = S3DataTable.export_formats(rfields, permalink=permalink, base_url=base_url) # Nb These can be moved around in initComplete() form.append(export_formats) form.append(html) # Add the configuration details for this dataTable form.append( INPUT(_type="hidden", _id="%s_configurations" % id, _name="config", _value=jsons(config))) # If we have a cache set up then pass it in if cache: form.append( INPUT(_type="hidden", _id="%s_dataTable_cache" % id, _name="cache", _value=jsons(cache))) # If we have bulk actions then add the hidden fields if bulkActions: form.append( INPUT(_type="hidden", _id="%s_dataTable_bulkMode" % id, _name="mode", _value="Inclusive")) bulk_selected = _aget("dt_bulk_selected", "") if isinstance(bulk_selected, list): bulk_selected = ",".join(bulk_selected) form.append( INPUT(_type="hidden", _id="%s_dataTable_bulkSelection" % id, _name="selected", _value="[%s]" % bulk_selected)) form.append( INPUT(_type="hidden", _id="%s_dataTable_filterURL" % id, _class="dataTable_filterURL", _name="filterURL", _value="%s" % config.ajaxUrl)) # Set callback? initComplete = settings.get_ui_datatables_initComplete() if initComplete: # Processed in views/dataTables.html s3.dataTable_initComplete = initComplete return form
def send(cls, r, resource): """ Method to retrieve updates for a subscription, render the notification message and send it - responds to POST?format=msg requests to the respective resource. @param r: the S3Request @param resource: the S3Resource """ _debug = current.log.debug _debug("S3Notifications.send()") json_message = current.xml.json_message # Read subscription data source = r.body source.seek(0) data = source.read() subscription = json.loads(data) #_debug("Notify PE #%s by %s on %s of %s since %s" % \ # (subscription["pe_id"], # str(subscription["method"]), # str(subscription["notify_on"]), # subscription["resource"], # subscription["last_check_time"], # )) # Check notification settings notify_on = subscription["notify_on"] methods = subscription["method"] if not notify_on or not methods: return json_message(message="No notifications configured " "for this subscription") # Authorization (pe_id must not be None) pe_id = subscription["pe_id"] if not pe_id: r.unauthorised() # Fields to extract fields = resource.list_fields(key="notify_fields") if "created_on" not in fields: fields.append("created_on") # Extract the data data = resource.select(fields, represent=True, raw_data=True) rows = data["rows"] # How many records do we have? numrows = len(rows) if not numrows: return json_message(message="No records found") #_debug("%s rows:" % numrows) # Prepare meta-data get_config = resource.get_config settings = current.deployment_settings page_url = subscription["page_url"] crud_strings = current.response.s3.crud_strings.get(resource.tablename) if crud_strings: resource_name = crud_strings.title_list else: resource_name = string.capwords(resource.name, "_") last_check_time = s3_decode_iso_datetime(subscription["last_check_time"]) email_format = subscription["email_format"] if not email_format: email_format = settings.get_msg_notify_email_format() filter_query = subscription.get("filter_query") meta_data = {"systemname": settings.get_system_name(), "systemname_short": settings.get_system_name_short(), "resource": resource_name, "page_url": page_url, "notify_on": notify_on, "last_check_time": last_check_time, "filter_query": filter_query, "total_rows": numrows, } # Render contents for the message template(s) renderer = get_config("notify_renderer") if not renderer: renderer = settings.get_msg_notify_renderer() if not renderer: renderer = cls._render contents = {} if email_format == "html" and "EMAIL" in methods: contents["html"] = renderer(resource, data, meta_data, "html") contents["default"] = contents["html"] if email_format != "html" or "EMAIL" not in methods or len(methods) > 1: contents["text"] = renderer(resource, data, meta_data, "text") contents["default"] = contents["text"] # Subject line subject = get_config("notify_subject") if not subject: subject = settings.get_msg_notify_subject() if callable(subject): subject = subject(resource, data, meta_data) from string import Template subject = Template(subject).safe_substitute(S="%(systemname)s", s="%(systemname_short)s", r="%(resource)s") subject = subject % meta_data # Attachment attachment = subscription.get("attachment", False) document_ids = None if attachment: attachment_fnc = settings.get_msg_notify_attachment() if attachment_fnc: document_ids = attachment_fnc(resource, data, meta_data) # Helper function to find templates from a priority list join = lambda *f: os.path.join(current.request.folder, *f) def get_template(path, filenames): for fn in filenames: filepath = join(path, fn) if os.path.exists(filepath): try: return open(filepath, "rb") except: pass return None # Render and send the message(s) themes = settings.get_template() prefix = resource.get_config("notify_template", "notify") send = current.msg.send_by_pe_id success = False errors = [] for method in methods: error = None # Get the message template template = None filenames = ["%s_%s.html" % (prefix, method.lower())] if method == "EMAIL" and email_format: filenames.insert(0, "%s_email_%s.html" % (prefix, email_format)) if themes != "default": location = settings.get_template_location() if not isinstance(themes, (tuple, list)): themes = (themes,) for theme in themes[::-1]: path = join(location, "templates", theme, "views", "msg") template = get_template(path, filenames) if template is not None: break if template is None: path = join("views", "msg") template = get_template(path, filenames) if template is None: template = StringIO(s3_str(current.T("New updates are available."))) # Select contents format if method == "EMAIL" and email_format == "html": output = contents["html"] else: output = contents["text"] # Render the message try: message = current.response.render(template, output) except: exc_info = sys.exc_info()[:2] error = ("%s: %s" % (exc_info[0].__name__, exc_info[1])) errors.append(error) continue if not message: continue # Send the message #_debug("Sending message per %s" % method) #_debug(message) try: sent = send(pe_id, subject=s3_truncate(subject, 78), message=message, contact_method=method, system_generated=True, document_ids=document_ids) except: exc_info = sys.exc_info()[:2] error = ("%s: %s" % (exc_info[0].__name__, exc_info[1])) sent = False if sent: # Successful if at least one notification went out success = True else: if not error: error = current.session.error if isinstance(error, list): error = "/".join(error) if error: errors.append(error) # Done if errors: message = ", ".join(errors) else: message = "Success" return json_message(success=success, statuscode=200 if success else 403, message=message)