def _resolve_context(context, id): """ Resolve a context filter @param context: the context (as a string) @param id: the record_id """ if context == "location": # Show records linked to this Location & all it's Child Locations s = "(location)$path" # This version doesn't serialize_url #m = ("%(id)s/*,*/%(id)s/*" % dict(id=id)).split(",") #filter = (S3FieldSelector(s).like(m)) | (S3FieldSelector(s) == id) m = ("%(id)s,%(id)s/*,*/%(id)s/*,*/%(id)s" % dict(id=id)).split(",") m = [f.replace("*", "%") for f in m] filter = S3FieldSelector(s).like(m) # @ToDo: #elif context == "organisation": # # Show records linked to this Organisation and all it's Branches # s = "(%s)" % context # filter = S3FieldSelector(s) == id else: # Normal: show just records linked directly to this master resource s = "(%s)" % context filter = S3FieldSelector(s) == id return filter
def _resolve_context(r, tablename, context): """ Resolve a context filter @param context: the context (as a string) @param id: the record_id """ record_id = r.id if not record_id: return None s3db = current.s3db if not context: query = None elif type(context) is tuple: context, field = context query = S3FieldSelector(context) == r.record[field] elif context == "location": # Show records linked to this Location & all it's Child Locations s = "(location)$path" # This version doesn't serialize_url #m = ("%(id)s/*,*/%(id)s/*" % dict(id=id)).split(",") #filter = (S3FieldSelector(s).like(m)) | (S3FieldSelector(s) == id) m = ("%(id)s,%(id)s/*,*/%(id)s/*,*/%(id)s" % dict(id=record_id)).split(",") m = [f.replace("*", "%") for f in m] query = S3FieldSelector(s).like(m) # @ToDo: #elif context == "organisation": # # Show records linked to this Organisation and all it's Branches # s = "(%s)" % context # query = S3FieldSelector(s) == id else: # Normal: show just records linked directly to this master resource s = "(%s)" % context query = S3FieldSelector(s) == record_id # Define target resource resource = s3db.resource(tablename, filter=query) return resource, query
def duplicates(self, r, **attr): """ Renders a list of all currently duplicate-bookmarked records in this resource, with option to select two and initiate the merge process from here @param r: the S3Request @param attr: the controller attributes for the request """ s3 = current.session.s3 resource = self.resource tablename = self.tablename if r.http == "POST": return self.merge(r, **attr) # Bookmarks record_ids = [] DEDUPLICATE = self.DEDUPLICATE if DEDUPLICATE in s3: bookmarks = s3[DEDUPLICATE] if tablename in bookmarks: record_ids = bookmarks[tablename] query = S3FieldSelector(resource._id.name).belongs(record_ids) resource.add_filter(query) # Representation representation = r.representation # List fields list_fields = resource.list_fields() # Start/Limit vars = r.get_vars if representation == "aadata": start = vars.get("iDisplayStart", None) limit = vars.get("iDisplayLength", None) sEcho = int(vars.sEcho or 0) else: # catch all start = 0 limit = current.manager.ROWSPERPAGE if limit is not None: try: start = int(start) limit = int(limit) except ValueError: start = None limit = None # use default else: start = None # use default if current.response.s3.dataTable_iDisplayLength: display_length = current.response.s3.dataTable_iDisplayLength else: display_length = 25 if limit is None: limit = 2 * display_length # Datatable Filter totalrows = None if representation == "aadata": searchq, orderby, left = resource.datatable_filter( list_fields, vars) if searchq is not None: totalrows = resource.count() resource.add_filter(searchq) else: orderby, left = None, None # Get the records data = resource.select(list_fields, start=start, limit=limit, orderby=orderby, left=left, count=True, represent=True) displayrows = data["numrows"] if totalrows is None: totalrows = displayrows # Generate a datatable dt = S3DataTable(data["rfields"], data["rows"]) datatable_id = "s3merge_1" response = current.response if representation == "aadata": output = dt.json(totalrows, displayrows, datatable_id, sEcho, dt_bulk_actions=[(current.T("Merge"), "merge", "pair-action")]) elif representation == "html": # Initial HTML response T = current.T output = {"title": T("De-duplicate Records")} url = r.url(representation="aadata") #url = "/%s/%s/%s/deduplicate.aadata" % (r.application, #r.controller, #r.function) items = dt.html(totalrows, displayrows, datatable_id, dt_ajax_url=url, dt_displayLength=display_length, dt_bulk_actions=[(T("Merge"), "merge", "pair-action")]) output["items"] = items response.s3.actions = [{ "label": str(T("View")), "url": r.url(target="[id]", method="read"), "_class": "action-btn" }] if len(record_ids) < 2: output["add_btn"] = DIV( SPAN(T( "You need to have at least 2 records in this list in order to merge them." ), _style="float:left; padding-right:10px;"), A(T("Find more"), _href=r.url(method="search", id=0, component_id=0, vars={}))) else: output["add_btn"] = DIV( SPAN( T("Select 2 records from this list, then click 'Merge'." )), ) response.s3.dataTableID = [datatable_id] response.view = self._view(r, "list.html") else: r.error(501, r.ERROR.BAD_FORMAT) return output
def create_event_frame(self, event_start, event_end, start=None, end=None, slots=None): """ Create an event frame for the current resource @param event_start: the event start field (S3ResourceField) @param event_end: the event end field (S3ResourceField) @param start: the start date/time (string) @param end: the end date/time (string) @param slots: the slot length (string) @return: the event frame """ resource = self.resource now = datetime.datetime.utcnow() dtparse = self.dtparse start_dt = end_dt = None STANDARD_SLOT = "1 day" # Parse start and end time if start: start_dt = dtparse(start, start=now) if end: relative_to = start_dt if start_dt else now end_dt = dtparse(end, start=relative_to) # Fall back to now if internval end is not specified if not end_dt: end_dt = now if not start_dt and event_start and event_start.field: # No interval start => fall back to first event start query = S3FieldSelector(event_start.selector) != None resource.add_filter(query) rows = resource.select([event_start.selector], limit=1, orderby=event_start.field, as_rows=True) # Remove the filter we just added resource.rfilter.filters.pop() resource.rfilter.query = None if rows: first_event = rows.first()[event_start.colname] if isinstance(first_event, datetime.date): first_event = datetime.datetime.fromordinal( first_event.toordinal()) start_dt = first_event if not start_dt and event_end and event_end.field: # No interval start => fall back to first event end minus # one standard slot length: query = S3FieldSelector(event_end.selector) != None resource.add_filter(query) rows = resource.select([event_end.selector], limit=1, orderby=event_end.field, as_rows=True) # Remove the filter we just added resource.rfilter.filters.pop() resource.rfilter.query = None if rows: last_event = rows.first()[event_end.colname] if isinstance(last_event, datetime.date): last_event = datetime.datetime.fromordinal( last_event.toordinal()) start_dt = dtparse("-%s" % STANDARD_SLOT, start=last_event) if not start_dt: # No interval start => fall back to interval end minus # one slot length: if not slots: slots = STANDARD_SLOT try: start_dt = dtparse("-%s" % slots, start=end_dt) except (SyntaxError, ValueError): slots = STANDARD_SLOT start_dt = dtparse("-%s" % slots, start=end_dt) if not slots: # No slot length => # Determine optimum slot length automatically # @todo: determine from density of events rather than # total interval length? seconds = abs(end_dt - start_dt).total_seconds() day = 86400 if seconds < day: slots = "hours" elif seconds < 3 * day: slots = "6 hours" elif seconds < 28 * day: slots = "days" elif seconds < 90 * day: slots = "weeks" elif seconds < 730 * day: slots = "months" elif seconds < 2190 * day: slots = "3 months" else: slots = "years" return S3TimePlotEventFrame(start_dt, end_dt, slots)
def add_event_data(self, event_frame, resource, event_start, event_end, facts): """ Extract event data from resource and add them to the event frame @param event_frame: the event frame @param resource: the resource @param event_start: the event start field (S3ResourceField) @param event_end: the event_end field (S3ResourceField) @param fact: list of fact fields (S3ResourceField) @return: the extracted data (dict from S3Resource.select) """ # Fields to extract fields = set(fact.selector for fact in facts) fields.add(event_start.selector) fields.add(event_end.selector) fields.add(resource._id.name) # Filter by event frame start: # End date of events must be after the event frame start date if event_end: end_selector = S3FieldSelector(event_end.selector) start = event_frame.start query = (end_selector == None) | (end_selector >= start) else: # No point if events have no end date query = None # Filter by event frame end: # Start date of events must be before event frame end date start_selector = S3FieldSelector(event_start.selector) end = event_frame.end q = (start_selector == None) | (start_selector <= end) query = query & q if query is not None else q # Add as temporary filter resource.add_filter(query) # Extract the records data = resource.select(fields) # Remove the filter we just added resource.rfilter.filters.pop() resource.rfilter.query = None # Do we need to convert dates into datetimes? convert_start = True if event_start.ftype == "date" else False convert_end = True if event_start.ftype == "date" else False fromordinal = datetime.datetime.fromordinal convert_date = lambda d: fromordinal(d.toordinal()) # Column names for extractions pkey = str(resource._id) start_colname = event_start.colname end_colname = event_end.colname # Use table name as event type tablename = resource.tablename # Create the events events = [] add_event = events.append for row in data["rows"]: values = dict((fact.colname, row[fact.colname]) for fact in facts) start = row[start_colname] if convert_start: start = convert_date(start) end = row[end_colname] if convert_end: end = convert_date(end) event = S3TimePlotEvent(row[pkey], start=start, end=end, values=values, event_type=tablename) add_event(event) # Extend the event frame with these events if events: event_frame.extend(events) return data
def _datalist(self, r, widget, **attr): """ Generate a dataList @param r: the S3Request instance @param widget: the widget as a tuple: (label, tablename, icon, filter) @param attr: controller attributes for the request """ T = current.T s3db = current.s3db id = r.id context = widget.get("context", None) if context: context = self._resolve_context(context, id) s3db.context = context tablename = widget.get("tablename", None) resource = s3db.resource(tablename, context=True) table = resource.table # Config Options: # 1st choice: Widget # 2nd choice: get_config # 3rd choice: Default config = resource.get_config list_fields = widget.get("list_fields", config("list_fields", None)) list_layout = widget.get("list_layout", config("list_layout", None)) orderby = widget.get( "orderby", config("list_orderby", ~resource.table.created_on)) filter = widget.get("filter", None) if filter: resource.add_filter(filter) # Use the widget-index to create a unique ID listid = "profile-list-%s-%s" % (tablename, widget["index"]) # Page size pagesize = 4 representation = r.representation if representation == "dl": # Ajax-update get_vars = r.get_vars record_id = get_vars.get("record", None) if record_id is not None: # Ajax-update of a single record resource.add_filter(S3FieldSelector("id") == record_id) start, limit = 0, 1 else: # Ajax-update of full page start = get_vars.get("start", None) limit = get_vars.get("limit", None) if limit is not None: try: start = int(start) limit = int(limit) except ValueError: start, limit = 0, pagesize else: start = None else: # Page-load start, limit = 0, pagesize # Ajax-delete items? if representation == "dl" and r.http in ("DELETE", "POST"): if "delete" in r.get_vars: return self._dl_ajax_delete(r, resource) else: r.error(405, r.ERROR.BAD_METHOD) # dataList datalist, numrows, ids = resource.datalist(fields=list_fields, start=start, limit=limit, listid=listid, orderby=orderby, layout=list_layout) # Render the list ajaxurl = r.url(vars={"update": widget["index"]}, representation="dl") data = datalist.html(ajaxurl=ajaxurl, pagesize=pagesize) if numrows == 0: msg = P(I(_class="icon-folder-open-alt"), BR(), S3CRUD.crud_string(tablename, "msg_no_match"), _class="empty_card-holder") data.insert(1, msg) if representation == "dl": # This is an Ajax-request, so we don't need the wrapper current.response.view = "plain.html" return data # Interactive only below here label = widget.get("label", "") if label: label = T(label) icon = widget.get("icon", "") if icon: icon = TAG[""](I(_class=icon), " ") # Permission to create new items? insert = widget.get("insert", True) if insert and current.auth.s3_has_permission("create", table): #if r.tablename = "org_organisation": # @ToDo: Special check for creating resources on Organisation profile if filter: vars = filter.serialize_url(filter) else: vars = Storage() vars.refresh = listid if context: filters = context.serialize_url(resource) for f in filters: vars[f] = filters[f] default = widget.get("default", None) if default: k, v = default.split("=", 1) vars[k] = v title_create = widget.get("title_create", None) if title_create: title_create = T(title_create) else: title_create = S3CRUD.crud_string(tablename, "title_create") c, f = tablename.split("_", 1) c = widget.get("create_controller", c) f = widget.get("create_function", f) create = A( I(_class="icon icon-plus-sign small-add"), _href=URL(c=c, f=f, args=["create.popup"], vars=vars), _class="s3_modal", _title=title_create, ) else: create = "" if numrows > pagesize: # Button to display the rest of the records in a Modal more = numrows - pagesize vars = {} if context: filters = context.serialize_url(resource) for f in filters: vars[f] = filters[f] if filter: filters = filter.serialize_url(resource) for f in filters: vars[f] = filters[f] c, f = tablename.split("_", 1) url = URL(c=c, f=f, args=["datalist.popup"], vars=vars) more = DIV(A( BUTTON( "%s (%s)" % (T("see more"), more), _class="btn btn-mini", _type="button", ), _class="s3_modal", _href=url, _title=label, ), _class="more_profile") else: more = "" # Render the widget output = DIV(create, H4(icon, label, _class="profile-sub-header"), DIV(data, more, _class="card-holder"), _class="span6") return output
def _datalist(self, r, widget, **attr): """ Generate a data list @param r: the S3Request instance @param widget: the widget definition as dict @param attr: controller attributes for the request """ T = current.T s3db = current.s3db context = widget.get("context", None) tablename = widget.get("tablename", None) resource, context = self._resolve_context(r, tablename, context) # Config Options: # 1st choice: Widget # 2nd choice: get_config # 3rd choice: Default config = resource.get_config list_fields = widget.get("list_fields", config("list_fields", None)) list_layout = widget.get("list_layout", config("list_layout", None)) orderby = widget.get("orderby", config("list_orderby", ~resource.table.created_on)) filter = widget.get("filter", None) if filter: resource.add_filter(filter) # Use the widget-index to create a unique ID listid = "profile-list-%s-%s" % (tablename, widget["index"]) # Page size pagesize = widget.get("pagesize", 4) representation = r.representation if representation == "dl": # Ajax-update get_vars = r.get_vars record_id = get_vars.get("record", None) if record_id is not None: # Ajax-update of a single record resource.add_filter(S3FieldSelector("id") == record_id) start, limit = 0, 1 else: # Ajax-update of full page start = get_vars.get("start", None) limit = get_vars.get("limit", None) if limit is not None: try: start = int(start) limit = int(limit) except ValueError: start, limit = 0, pagesize else: start = None else: # Page-load start, limit = 0, pagesize # Ajax-delete items? if representation == "dl" and r.http in ("DELETE", "POST"): if "delete" in r.get_vars: return self._dl_ajax_delete(r, resource) else: r.error(405, r.ERROR.BAD_METHOD) # dataList datalist, numrows, ids = resource.datalist(fields=list_fields, start=start, limit=limit, listid=listid, orderby=orderby, layout=list_layout) # Render the list ajaxurl = r.url(vars={"update": widget["index"]}, representation="dl") data = datalist.html(ajaxurl=ajaxurl, pagesize=pagesize, empty = P(I(_class="icon-folder-open-alt"), BR(), S3CRUD.crud_string(tablename, "msg_no_match"), _class="empty_card-holder" ), ) if representation == "dl": # This is an Ajax-request, so we don't need the wrapper current.response.view = "plain.html" return data # Interactive only below here label = widget.get("label", "") if label: label = T(label) icon = widget.get("icon", "") if icon: icon = TAG[""](I(_class=icon), " ") s3 = current.response.s3 if pagesize and numrows > pagesize: # Button to display the rest of the records in a Modal more = numrows - pagesize vars = {} if context: filters = context.serialize_url(resource) for f in filters: vars[f] = filters[f] if filter: filters = filter.serialize_url(resource) for f in filters: vars[f] = filters[f] c, f = tablename.split("_", 1) url = URL(c=c, f=f, args=["datalist.popup"], vars=vars) more = DIV(A(BUTTON("%s (%s)" % (T("see more"), more), _class="btn btn-mini", _type="button", ), _class="s3_modal", _href=url, _title=label, ), _class="more_profile") else: more = "" # Link for create-popup create_popup = self._create_popup(r, widget, listid, resource, context, numrows) colspan = widget.get("colspan", 1) if colspan == 1: _class = "span6" elif colspan == 2: _class = "span12" else: # Unsupported raise # Render the widget output = DIV(create_popup, H4(icon, label, _class="profile-sub-header"), DIV(data, more, _class="card-holder"), _class=_class) return output