def helper(self): if self.accepted: self.clear() if not self.cached_helper: helper = self.param.formstyle( self.table, self.vars, self.errors, self.readonly, self.deletable, self.noncreate, show_id=self.show_id, kwargs=self.kwargs, ) for item in self.param.sidecar: helper["form"][-1][-1].append(item) button_attributes = item.attributes button_attributes["_label"] = item.children[0] helper["json_controls"]["form_buttons"] += [button_attributes] if self.action: helper["form"]["_action"] = self.action if self.param.submit_value: helper["controls"]["submit"][ "_value"] = self.param.submit_value if self.form_name: helper["controls"]["hidden_widgets"]["formname"] = INPUT( _type="hidden", _name="_formname", _value=self.form_name) helper["form"].append( helper["controls"]["hidden_widgets"]["formname"]) helper["json_controls"]["form_values"][ "_formname"] = self.form_name if self.formkey: helper["controls"]["hidden_widgets"]["formkey"] = INPUT( _type="hidden", _name="_formkey", _value=self.formkey) helper["form"].append( helper["controls"]["hidden_widgets"]["formkey"]) helper["json_controls"]["form_values"][ "_formkey"] = self.formkey for key in self.param.hidden or {}: helper["controls"]["hidden_widgets"][key] = INPUT( _type="hidden", _name=key, _value=self.param.hidden[key]) helper["form"].append( helper["controls"]["hidden_widgets"][key]) helper["controls"]["begin"] = XML("".join( str(helper["controls"]["begin"]) + str(helper["controls"]["hidden_widgets"][hidden_field]) for hidden_field in helper["controls"]["hidden_widgets"])) self.cached_helper = helper return self.cached_helper
def __call__(self, id=None, redirect_url=None): """This method returns the element that can be included in the page. @param id: if an id is specified, the form is an update form for the specified record id. @param redirect_url: URL to which to redirect after success.""" return XML(VueForm.FORM.format(url=URL(self.url, id, signer=self.signer), check_url=URL(self.url_check, id, signer=self.signer), redirect_url=redirect_url))
def __call__(self, id=None): return XML( ThumbRater.THUMBRATER.format(url=URL(self.url, id, signer=self.signer), callback_url=URL(self.callback_url, id, signer=self.signer)))
def render_action_button( self, url, button_text, icon, icon_size="small", additional_classes=None, additional_styles=None, override_classes=None, override_styles=None, message=None, onclick=None, row_id=None, name="grid-button", row=None, **attrs, ): separator = "?" if row_id: url += "/%s" % row_id classes = self.param.grid_class_style.classes.get(name, "") styles = self.param.grid_class_style.styles.get(name, "") def join(items): return (" ".join(items) if isinstance(items, (list, tuple)) else " %s" % items) if override_classes: classes = join(override_classes) elif additional_classes: classes += join(additional_classes) if override_styles: styles = join(override_styles) elif additional_styles: styles += join(additional_styles) if callable(url): url = url(row) attrs.update(self.attributes_plugin.link(url=url)) link = A( I(_class="fa %s" % icon), _role="button", _class=classes, _message=message, _title=button_text, _style=styles, **attrs, ) if self.param.include_action_button_text: link.append( XML('<span class="grid-action-button-text"> %s</span>' % button_text)) return link
def __call__(self, redirect_url=None): """This method returns the element that can be included in the page. The *args and **kwargs are used when subclassing, to allow for forms that are 'custom built' for some need.""" return XML( VueForm.FORM.format(url=URL(self.url, signer=self.signer), check_url=URL(self.url_check, signer=self.signer), redirect_url=redirect_url))
def grid(self, table): name = 'vue%s' % str(uuid.uuid4())[:8] return DIV(self.mtable(table), TAG.SCRIPT(_src=URL('static/js/axios.min.js')), TAG.SCRIPT(_src=URL('static/js/vue.min.js')), TAG.SCRIPT(_src=URL('static/js/utils.js')), TAG.SCRIPT(_src=URL('static/components/mtable.js')), TAG.SCRIPT( XML('var app=utils.app("%s"); app.start()' % name)), _id=name)
def get_gantt_data(quests): projxml = "<project>" questlist = [x.question.id for x in quests] intlinks = getlinks(questlist) for i, row in enumerate(quests): projxml += convrow(row, getstrdepend(intlinks, row.question.id), True) projxml += '</project>' return XML(projxml)
def edit_dog(): dog_id = request.query.get('id') form = Form(db.dog, record=dog_id, formstyle=FormStyleBulma) form.param.sidecar = [XML('<button class="button" onclick="close_modal();" ' 'style="margin-left: .5rem;">Cancel</button>')] if form.accepted: url = URL('index') redirect(url) return dict(form=form)
def __call__(self, id=None): """This method returns the element that can be included in the page. @param id: id of the file uploaded. This can be useful if there are multiple instances of this form on the page.""" return XML( ThumbRater.THUMBRATER.format(url=URL(self.url, id, signer=self.signer), callback_url=URL(self.callback_url, id, signer=self.signer)))
def grid(self, table): name = "vue%s" % str(uuid.uuid4())[:8] return DIV( self.mtable(table), TAG.SCRIPT(_src=URL("static/js/axios.min.js")), TAG.SCRIPT(_src=URL("static/js/vue.min.js")), TAG.SCRIPT(_src=URL("static/js/utils.js")), TAG.SCRIPT(_src=URL("static/components/mtable.js")), TAG.SCRIPT(XML('var app={}; app.vue = new Vue({el:"#%s"});' % name)), _id=name, )
def __call__(self, img_id=None): # turn our class into HTML return XML( MainAppComponent.MAINAPP.format( get_posts_url=URL(self.get_posts_url, signer=self.signer), get_profile_url=URL(self.get_profile_url, signer=self.signer), get_user_url=URL(self.get_user_url, signer=self.signer), create_post_url=URL(self.create_post_url, signer=self.signer), create_reply_url=URL(self.create_reply_url, signer=self.signer), get_about_url=URL(self.get_about_url, signer=self.signer), delete_all=URL(self.delete_all_posts_url, signer=self.signer), add_like_url=URL(self.add_like_url, signer=self.signer)))
def __call__(self, id=None, cancel_url=""): """This method returns the element that can be included in the page. The *args and **kwargs are used when subclassing, to allow for forms that are 'custom built' for some need. """ return XML( VueForm.FORM.format( url=self.url(id), check_url=self.check_url(id), cancel_url=cancel_url, ) )
def hcaptcha_form(): form = Form([Field( 'dummy_form', 'string', )]) form.structure.append( XML('<div class="h-captcha" data-sitekey="{}"></div>'.format( HCAPTCHA_SITE_KEY))) if form.accepted: r = hCaptcha(request.forms.get('g-recaptcha-response')) if r == True: #do something with form data form.structure.append( XML('<div style="color:green">Captcha was solved succesfully!</font></div>' )) else: form.structure.append( XML('<div class="py4web-validation-error">invalid captcha</div>' )) return dict(form=form)
def __call__(self, id=None): # a clear cut interface is better than dependence on global variables, but including the same user # every time might make data strained, adding user as a prop may be added later return XML( Comment.COMMENT.format(get_url=URL(self.get_url, id, signer=self.signer), add_url=URL(self.add_url, signer=self.signer), edit_url=URL(self.edit_url, signer=self.signer), delete_url=URL(self.delete_url, signer=self.signer), ticket_id=id))
def test_tags(self): DIV = TAG.div IMG = TAG['img/'] self.assertEqual(DIV().xml(), "<div></div>") self.assertEqual(IMG().xml(), "<img/>") self.assertEqual(DIV(_id="my_id").xml(), "<div id=\"my_id\"></div>") self.assertEqual(IMG(_src="crazy").xml(), "<img src=\"crazy\"/>") self.assertEqual( DIV(_class="my_class", _mytrueattr=True).xml(), "<div class=\"my_class\" mytrueattr=\"mytrueattr\"></div>") self.assertEqual( DIV(_id="my_id", _none=None, _false=False, without_underline="serius?").xml(), "<div id=\"my_id\"></div>") self.assertEqual( DIV("<b>xmlscapedthis</b>").xml(), "<div><b>xmlscapedthis</b></div>") self.assertEqual( DIV(XML("<b>don'txmlscapedthis</b>")).xml(), "<div><b>don'txmlscapedthis</b></div>")
def view_project(pid='0'): projectrow = db(db.project.id == pid).select().first() session['projid'] = pid if projectrow else 0 events = db(db.event.projid == pid).select(orderby=~db.event.startdatetime) actions = get_items(qtype='action', status='In Progress', project=pid) questions = get_items(qtype='quest', status='In Progress', project=pid) issues = get_items(qtype='issue', project=pid) res_actions = get_items(qtype='action', status='Resolved', project=pid, execstatus='Incomplete') comp_actions = get_items(qtype='action', status='Resolved', project=pid, execstatus='Completed') res_questions = get_items(qtype='question', status='Resolved', project=pid) if res_actions: projxml = get_gantt_data(res_actions) else: projxml = "<project></project>" db.comment.auth_userid.default = auth.user_id db.comment.parenttable.default = 'project' db.comment.parentid.default = pid commentform = Form(db.comment, formstyle=FormStyleBulma) return dict(projectid=pid, projectrow=projectrow, qactions=actions, questions=questions, issues=issues, res_actions=res_actions, res_questions=res_questions, comp_actions=comp_actions, events=events, get_class=get_class, get_disabled=get_disabled, myconverter=myconverter, project=XML(projxml), auth=auth, like=like, commentform=commentform)
def helper(self): if self.accepted: self.clear() if not self.cached_helper: helper = self.param.formstyle( self.table, self.vars, self.errors, self.readonly, self.deletable, kwargs=self.kwargs, ) for item in self.param.sidecar: helper["form"][-1][-1].append(item) if self.action: helper["form"]["_action"] = self.action if self.param.submit_value: helper["controls"]["submit"][ "_value"] = self.param.submit_value if self.form_name: helper["controls"]["hidden_widgets"]["formname"] = INPUT( _type="hidden", _name="_formname", _value=self.form_name) helper["form"].append( helper["controls"]["hidden_widgets"]["formname"]) if self.formkey: helper["controls"]["hidden_widgets"]["formkey"] = INPUT( _type="hidden", _name="_formkey", _value=self.formkey) helper["form"].append( helper["controls"]["hidden_widgets"]["formkey"]) for key in self.param.hidden or {}: helper["controls"]["hidden_widgets"][key] = INPUT( _type="hidden", _name=key, _value=self.param.hidden[key]) helper["form"].append( helper["controls"]["hidden_widgets"][key]) helper["controls"]["begin"] = XML("".join( str(helper["controls"]["begin"]) + str(helper["controls"]["hidden_widgets"][hidden_field]) for hidden_field in helper["controls"]["hidden_widgets"])) self.cached_helper = helper return self.cached_helper
def employees(path=None): queries = [(db.employee.id > 0)] orderby = [db.employee.last_name, db.employee.first_name] search_queries = [['Search by Company', lambda val: db.company.id == val, db.employee.company.requires], ['Search by Department', lambda val: db.department.id == val, db.employee.department.requires], ['Search by Name', lambda val: "first_name || ' ' || last_Name LIKE '%%%s%%'" % val]] search = GridSearch(search_queries, queries) fields = [db.employee.id, db.employee.first_name, db.employee.last_name, db.company.name, db.department.name, db.employee.hired, db.employee.supervisor, db.employee.active] grid = Grid(path, search.query, search_form=search.search_form, fields=fields, left=[db.company.on(db.employee.company == db.company.id), db.department.on(db.employee.department == db.department.id)], orderby=orderby, create=True, details=True, editable=True, deletable=True, **GRID_DEFAULTS) grid.formatters_by_type['boolean'] = lambda value: SPAN(I(_class='fas fa-check-circle')) if value else "" grid.formatters_by_type['date'] =lambda value: XML( '<script>document.write((new Date(%s,%s,%s)).toLocaleDateString({month: "2-digit", day: "2-digit", year: "numeric"}).split(",")[0])</script>' % (value.year, value.month, value.day,) ) return dict(grid=grid)
def produce( self, table, vars, errors, readonly, noncreate, deletable, classes=None, class_inner_exceptions=None, kwargs=None, ): self.classes.update(classes or {}) self.class_inner_exceptions.update(class_inner_exceptions or {}) kwargs = kwargs if kwargs else {} form_method = "POST" form_action = request.url form_enctype = "multipart/form-data" form = FORM( _method=form_method, _action=form_action, _enctype=form_enctype, **kwargs ) controls = Param( labels=dict(), widgets=dict(), comments=dict(), hidden_widgets=dict(), placeholders=dict(), titles=dict(), errors=dict(), begin=XML(form.xml().split("</form>")[0]), submit="", delete="", end=XML("</form>"), ) json_controls = dict( form_fields=[], form_values=dict(), form_buttons=[], form_method=form_method, form_action=form_action, form_enctype=form_enctype, **kwargs ) class_label = self.classes["label"] class_outer = self.classes["outer"] class_inner = self.classes["inner"] class_error = self.classes["error"] class_info = self.classes["info"] for field in table: # Reset the json control fields. field_attributes = dict() field_value = None field_name = field.name field_type = field.type field_comment = field.comment if field.comment else "" field_label = field.label input_id = "%s_%s" % (field.tablename, field.name) value = vars.get( field.name, field.default() if callable(field.default) else field.default, ) error = errors.get(field.name) field_class = "type-" + field.type.split()[0].replace(":", "-") placeholder = ( field._placeholder if "_placeholder" in field.__dict__ else None ) title = field._title if "_title" in field.__dict__ else None field_disabled = False # only diplay field if readable or writable if not field.readable and not field.writable: continue # if this is a reaonly field only show readable fields if readonly: if not field.readable: continue # if this is an create form (unkown id) then only show writable fields. Some if an edit form was made from a list of fields and noncreate=True elif not vars.get("id") and noncreate: if not field.writable: continue # ignore blob fields if field.type == "blob": # never display blobs (mistake?) continue # ignore fields of type id its value is equal to None if field.type == "id" and value is None: field.writable = False continue # if the form is readonly or this is an id type field, display it as readonly if readonly or not field.writable or field.type == "id": if field.type == "boolean": field_value = value field_type = "checkbox" control = INPUT( _type=field_type, _id=input_id, _name=field_name, _value="ON", _disabled="", _checked=value, _title=title, ) else: field_value = ( field.represent and field.represent(value) or value or "" ) field_type = "represent" control = DIV(field_value) field_disabled = True # if we have a widget for the field use it elif field.widget: control = field.widget(table, value) # Grab the custom widget attributes. field_attributes = control.attributes field_type = "widget" field_value = value # else pick the proper default widget elif field.type == "text": field_value = value or "" field_type = "text" control = TEXTAREA( field_value, _id=input_id, _name=field_name, _placeholder=placeholder, _title=title, ) elif field.type == "date": field_value = value field_type = "date" control = INPUT( _value=field_value, _type="date", _id=input_id, _name=field_name, _placeholder=placeholder, _title=title, ) elif field.type == "datetime": helpervalue = str(value) helpervalue = helpervalue.replace(" ", "T") field_value = helpervalue field_type = "datetime-local" control = INPUT( _value=helpervalue, _type="datetime-local", _id=input_id, _name=field_name, _placeholder=placeholder, _title=title, ) elif field.type == "time": field_value = value field_type = "time" control = INPUT( _value=value, _type="time", _id=input_id, _name=field_name, _placeholder=placeholder, _title=title, ) elif field.type == "boolean": field_value = value field_type = "checkbox" field_attributes["_value"] = "ON" control = INPUT( _type="checkbox", _id=input_id, _name=field_name, _value=field_attributes["_value"], _checked=value, _title=title, ) elif field.type == "upload": control = DIV() if value and not error: download_div = DIV() download_div.append( LABEL( "Currently: ", ) ) if getattr(field, "download_url", None): url = field.download_url(value) else: url = "#" download_div.append(A(" download ", _href=url)) # Set the download url. field_attributes["_download_url"] = url # Set the flag determining whether the file is an image. field_attributes["_is_image"] = (url != "#") and Form.is_image( value ) delete_checkbox_name = "_delete_" + field_name download_div.append( INPUT( _type="checkbox", _value="ON", _name=delete_checkbox_name, _title=title, ) ) download_div.append(" (check to remove)") delete_field_attributes = dict() delete_field_attributes["_label"] = "Remove" delete_field_attributes["_value"] = "ON" delete_field_attributes["_type"] = "checkbox" delete_field_attributes["_name"] = delete_checkbox_name json_controls["form_fields"] += [delete_field_attributes] json_controls["form_values"][delete_checkbox_name] = None control.append(download_div) control.append(LABEL("Change: ")) control.append(INPUT(_type="file", _id=input_id, _name=field_name)) field_value = None field_type = "file" elif get_options(field.requires) is not None and field.writable == True: multiple = field.type.startswith("list:") value = list(map(str, value if isinstance(value, list) else [value])) field_options = [ [k, v, (not k is None and k in value)] for k, v in get_options(field.requires) ] option_tags = [ OPTION(v, _value=k, _selected=_selected) for (k, v, _selected) in field_options ] control = SELECT( *option_tags, _id=input_id, _name=field_name, _multiple=multiple, _title=title ) field_value = value field_type = "options" field_attributes["_multiple"] = multiple field_attributes["_options"] = field_options else: field_type = "password" if field.type == "password" else "text" if field.type.startswith("list:"): value = json.dumps(value or []) field_value = None if field.type == "password" else value field_autocomplete = "off" if field_type == "password" else "on" control = INPUT( _type=field_type, _id=input_id, _name=field_name, _value=field_value, _class=field_class, _placeholder=placeholder, _title=title, _autocomplete=field_autocomplete, ) field_attributes["_autocomplete"] = field_autocomplete key = control.name.rstrip("/") if key == "input": key += "[type=%s]" % (control["_type"] or "text") control["_class"] = ( control.attributes.get("_class", "") + " " + self.classes.get(key, "") ).strip() # Set the form controls. controls["labels"][field_name] = field_label controls["widgets"][field_name] = control controls["comments"][field_name] = field_comment controls["titles"][field_name] = title controls["placeholders"][field_name] = placeholder # Set the remain json field attributes. field_attributes["_title"] = title field_attributes["_label"] = field_label field_attributes["_comment"] = field_comment field_attributes["_id"] = input_id field_attributes["_class"] = field_class field_attributes["_name"] = field_name field_attributes["_type"] = field_type field_attributes["_placeholder"] = placeholder field_attributes["_error"] = error field_attributes["_disabled"] = field_disabled # Add to the json controls. json_controls["form_fields"] += [field_attributes] json_controls["form_values"][field_name] = field_value if error: controls["errors"][field.name] = error if field.type == "boolean": form.append( DIV( SPAN(control, _class=class_inner), LABEL( " " + field.label, _for=input_id, _class=class_label, _style="display: inline !important", ), P(error, _class=class_error) if error else "", P(field.comment or "", _class=class_info), _class=class_outer, ) ) else: form.append( DIV( LABEL(field.label, _for=input_id, _class=class_label), DIV( control, _class=self.class_inner_exceptions.get( control.name, class_inner ), ), P(error, _class=class_error) if error else "", P(field.comment or "", _class=class_info), _class=class_outer, ) ) if vars.get("id"): form.append(INPUT(_name="id", _value=vars["id"], _hidden=True)) if deletable: deletable_record_attributes = dict() deletable_field_name = "_delete" deletable_field_type = "checkbox" # Set the deletable json field attributes. deletable_record_attributes["_label"] = " check to delete" deletable_record_attributes["_name"] = deletable_field_name deletable_record_attributes["_type"] = deletable_field_type deletable_record_attributes["_class"] = self.classes["input[type=checkbox]"] deletable_record_attributes["_value"] = "ON" # Add to the json controls. json_controls["form_fields"] += [deletable_record_attributes] json_controls["form_values"][deletable_field_name] = None controls["delete"] = INPUT( _type=deletable_field_type, _value=deletable_record_attributes["_value"], _name=deletable_field_name, _class=self.classes["input[type=checkbox]"], ) form.append( DIV( SPAN( controls["delete"], _class=class_inner, _stye="vertical-align: middle;", ), P( deletable_record_attributes["_label"], _class="help", _style="display: inline !important", ), _class=class_outer, ) ) submit_button_attributes = dict() submit_button_field_type = "submit" # Set the deletable json field attributes. submit_button_attributes["_label"] = "Submit" submit_button_attributes["_type"] = submit_button_field_type submit_button_attributes["_class"] = self.classes["input[type=submit]"] # Add to the json controls. json_controls["form_buttons"] += [submit_button_attributes] controls["submit"] = INPUT( _type=submit_button_field_type, _value="Submit", _class=self.classes["input[type=submit]"], ) submit = DIV( DIV( controls["submit"], _class=class_inner, ), _class=class_outer, ) form.append(submit) return dict(form=form, controls=controls, json_controls=json_controls)
def produce(self, table, vars, errors, readonly, deletable, classes=None): self.classes.update(classes or {}) form = FORM( _method="POST", _action=request.url, _enctype="multipart/form-data" ) controls = dict( widgets=dict(), hidden_widgets=dict(), errors=dict(), begin=XML(form.xml().split("</form>")[0]), end=XML("</form>"), ) class_label = self.classes["label"] class_outer = self.classes["outer"] class_inner = self.classes["inner"] class_error = self.classes["error"] class_info = self.classes["info"] for field in table: input_id = "%s_%s" % (field.tablename, field.name) value = vars.get(field.name, field.default) error = errors.get(field.name) field_class = field.type.split()[0].replace(":", "-") if not field.readable and not field.writable: continue if not readonly and not field.writable: continue if field.type == "blob": # never display blobs (mistake?) continue if field.type == "id" and value is None: field.writable = False continue if readonly or field.type == "id": control = DIV(field.represent and field.represent(value) or value or "") elif field.widget: control = field.widget(table, value) elif field.type == "text": control = TEXTAREA(value or "", _id=input_id, _name=field.name) elif field.type == "date": control = INPUT( _value=value, _type="date", _id=input_id, _name=field.name ) elif field.type == "datetime": if isinstance(value, str): value = value.replace(" ", "T") control = INPUT( _value=value, _type="datetime-local", _id=input_id, _name=field.name ) elif field.type == "time": control = INPUT( _value=value, _type="time", _id=input_id, _name=field.name ) elif field.type == "boolean": control = INPUT( _type="checkbox", _id=input_id, _name=field.name, _value="ON", _checked=value, ) elif field.type == "upload": control = DIV(INPUT(_type="file", _id=input_id, _name=field.name)) if value: control.append(A("download", _href=field.download_url(value))) control.append( INPUT( _type="checkbox", _value="ON", _name="_delete_" + field.name ) ) control.append("(check to remove)") elif get_options(field.requires) is not None: multiple = field.type.startswith("list:") value = list(map(str, value if isinstance(value, list) else [value])) option_tags = [ OPTION(v, _value=k, _selected=(not k is None and k in value)) for k, v in get_options(field.requires) ] control = SELECT( *option_tags, _id=input_id, _name=field.name, _multiple=multiple ) else: field_type = "password" if field.type == "password" else "text" control = INPUT( _type=field_type, _id=input_id, _name=field.name, _value=value, _class=field_class, ) key = control.name.rstrip("/") if key == "input": key += "[type=%s]" % (control["_type"] or "text") control["_class"] = self.classes.get(key, "") controls["widgets"][field.name] = control if error: controls["errors"][field.name] = error form.append( DIV( LABEL(field.label, _for=input_id, _class=class_label), DIV(control, _class=class_inner), P(error, _class=class_error) if error else "", P(field.comment or "", _class=class_info), _class=class_outer, ) ) if 'id' in vars: form.append(INPUT(_name='id',_value=vars['id'],_hidden=True)) if deletable: controls["delete"] = INPUT( _type="checkbox", _value="ON", _name="_delete", _class=self.classes["input[type=checkbox]"], ) form.append( DIV( DIV(controls["delete"], _class=class_inner,), P("check to delete", _class="help"), _class=class_outer, ) ) controls["submit"] = INPUT( _type="submit", _value="Submit", _class=self.classes["input[type=submit]"], ) submit = DIV(DIV(controls["submit"], _class=class_inner,), _class=class_outer,) form.append(submit) return dict(form=form, controls=controls)
def __call__(self, id=None): """This method returns the element that can be included in the page. @param id: if an id is specified, the form is an update form for the specified record id.""" return XML(VueForm.FORM.format(url=self.url(id), check_url=self.check_url(id)))
def __call__(self, bindCb="uploadedimage" , emitid=None): """This method returns the element that can be included in the page. @param id: id of the file uploaded. This can be useful if there are multiple instances of this form on the page.""" return XML(FileUpload.FILE_UPLOAD.format(url=URL(self.url, 1, signer=self.signer), bindCb=bindCb, emitid=emitid))
def __call__( self, table, vars, errors, readonly, deletable, noncreate, show_id, kwargs=None, ): kwargs = kwargs if kwargs else {} form_method = "POST" form_action = request.url.split(":", 1)[1] form_enctype = "multipart/form-data" form = FORM(_method=form_method, _action=form_action, _enctype=form_enctype, **kwargs) controls = Param( labels=dict(), widgets=dict(), wrappers=dict(), comments=dict(), hidden_widgets=dict(), placeholders=dict(), titles=dict(), errors=dict(), begin=XML(form.xml().split("</form>")[0]), submit="", delete="", end=XML("</form>"), ) json_controls = dict(form_fields=[], form_values=dict(), form_buttons=[], form_method=form_method, form_action=form_action, form_enctype=form_enctype, **kwargs) class_label = self.classes["label"] class_outer = self.classes["outer"] class_inner = self.classes["inner"] class_error = self.classes["error"] class_info = self.classes["info"] all_fields = [x for x in table] if "_virtual_fields" in dir(table): all_fields += table._virtual_fields for field in all_fields: # Reset the json control fields. field_attributes = dict() field_value = None field_name = field.name field_type = field.type field_comment = field.comment if field.comment else "" field_label = field.label input_id = "%s_%s" % (field.tablename, field.name) if isinstance(field, FieldVirtual): value = None else: value = vars.get( field.name, field.default() if callable(field.default) else field.default, ) error = errors.get(field.name) field_class = "type-" + field.type.split()[0].replace(":", "-") placeholder = (field._placeholder if "_placeholder" in field.__dict__ else None) title = field._title if "_title" in field.__dict__ else None field_disabled = False # only display field if readable or writable if not field.readable and not field.writable: continue # if this is a readonly field only show readable fields if readonly: if not field.readable: continue # do not show the id if not desired if field.type == "id" and not show_id: continue # if this is an create form (unkown id) then only show writable fields. # Some if an edit form was made from a list of fields and noncreate=True elif not vars.get("id") and noncreate: if not field.writable: continue # ignore blob fields if field.type == "blob": # never display blobs (mistake?) continue # ignore fields of type id its value is equal to None if field.type == "id" and value is None: field.writable = False continue # if the form is readonly or this is an id type field, display it as readonly if (readonly or not field.writable or field.type == "id" or isinstance(field, FieldVirtual)): # for boolean readonly we use a readonly checbox if field.type == "boolean": control = CheckboxWidget().make(field, value, error, title, readonly=True) # for all othe readonly fields we use represent or a string else: if isinstance(field, FieldVirtual): field_value = field.f(vars) else: field_value = (field.represent and field.represent(value) or value or "") field_type = "represent" control = DIV(field_value) field_disabled = True # if we have a field.widget for the field use it but this logic is deprecated elif field.widget: control = field.widget(table, vars) # else we pick the right widget else: if field.name in self.widgets: widget = self.widgets[field.name] elif field.type == "text": widget = TextareaWidget() elif field.type == "datetime": widget = DateTimeWidget() elif field.type == "boolean": widget = CheckboxWidget() elif field.type == "upload": widget = FileUploadWidget() url = getattr(field, "download_url", lambda value: "#")(value) # Set the download url. field_attributes["_download_url"] = url # Set the flag determining whether the file is an image. field_attributes["_is_image"] = ( url != "#") and Form.is_image(value) # do we need the variables below? delete_field_attributes = dict() delete_field_attributes["_label"] = "Remove" delete_field_attributes["_value"] = "ON" delete_field_attributes["_type"] = "checkbox" delete_field_attributes["_name"] = "_delete_" + field.name json_controls["form_fields"] += [delete_field_attributes] json_controls["form_values"]["_delete_" + field.name] = None elif get_options(field.requires) is not None: widget = SelectWidget() elif field.type == "password": widget = PasswordWidget() elif field.type.startswith("list:"): widget = ListWidget() else: widget = Widget() control = widget.make(field, value, error, title, placeholder) key = control.name.rstrip("/") if key == "input": key += "[type=%s]" % (control["_type"] or "text") control["_class"] = join_classes(control.attributes.get("_class"), self.classes.get(key)) # Set the form controls. controls["labels"][field_name] = field_label controls["widgets"][field_name] = control controls["comments"][field_name] = field_comment controls["titles"][field_name] = title controls["placeholders"][field_name] = placeholder # Set the remain json field attributes. field_attributes["_title"] = title field_attributes["_label"] = field_label field_attributes["_comment"] = field_comment field_attributes["_id"] = to_id(field) field_attributes["_class"] = field_class field_attributes["_name"] = field.name field_attributes["_type"] = field.type field_attributes["_placeholder"] = placeholder field_attributes["_error"] = error field_attributes["_disabled"] = field_disabled # Add to the json controls. json_controls["form_fields"] += [field_attributes] json_controls["form_values"][field_name] = field_value if error: controls["errors"][field.name] = error if field.type == "boolean": controls.wrappers[field.name] = wrapped = SPAN( control, _class=class_inner) form.append( DIV( wrapped, LABEL( " " + field.label, _for=input_id, _class=class_label, _style="display: inline !important", ), P(error, _class=class_error) if error else "", P(field.comment or "", _class=class_info), _class=class_outer, )) else: controls.wrappers[field.name] = wrapped = DIV( control, _class=self.class_inner_exceptions.get( control.name, class_inner), ) form.append( DIV( LABEL(field.label, _for=input_id, _class=class_label), wrapped, P(error, _class=class_error) if error else "", P(field.comment or "", _class=class_info), _class=class_outer, )) if vars.get("id"): form.append(INPUT(_name="id", _value=vars["id"], _hidden=True)) if deletable: deletable_record_attributes = dict() deletable_field_name = "_delete" deletable_field_type = "checkbox" # Set the deletable json field attributes. deletable_record_attributes["_label"] = " check to delete" deletable_record_attributes["_name"] = deletable_field_name deletable_record_attributes["_type"] = deletable_field_type deletable_record_attributes["_class"] = self.classes[ "input[type=checkbox]"] deletable_record_attributes["_value"] = "ON" # Add to the json controls. json_controls["form_fields"] += [deletable_record_attributes] json_controls["form_values"][deletable_field_name] = None controls["delete"] = INPUT( _type=deletable_field_type, _value=deletable_record_attributes["_value"], _name=deletable_field_name, _class=self.classes["input[type=checkbox]"], ) form.append( DIV( SPAN( controls["delete"], _class=class_inner, _stye="vertical-align: middle;", ), P( deletable_record_attributes["_label"], _class="help", _style="display: inline !important", ), _class=class_outer, )) submit_button_attributes = dict() submit_button_field_type = "submit" # Set the deletable json field attributes. submit_button_attributes["_label"] = "Submit" submit_button_attributes["_type"] = submit_button_field_type submit_button_attributes["_class"] = self.classes["input[type=submit]"] # Add to the json controls. json_controls["form_buttons"] += [submit_button_attributes] controls["submit"] = INPUT( _type=submit_button_field_type, _value="Submit", _class=self.classes["input[type=submit]"], ) submit = DIV( DIV( controls["submit"], _class=class_inner, ), _class=class_outer, ) form.append(submit) return dict(form=form, controls=controls, json_controls=json_controls)
def produce(self, table, vars, errors, readonly, deletable, classes=None): self.classes.update(classes or {}) form = FORM(_method="POST", _action=request.url, _enctype="multipart/form-data") controls = Param( labels=dict(), widgets=dict(), comments=dict(), hidden_widgets=dict(), placeholders=dict(), titles=dict(), errors=dict(), begin=XML(form.xml().split("</form>")[0]), submit="", delete="", end=XML("</form>"), ) class_label = self.classes["label"] class_outer = self.classes["outer"] class_inner = self.classes["inner"] class_error = self.classes["error"] class_info = self.classes["info"] for field in table: input_id = "%s_%s" % (field.tablename, field.name) value = vars.get(field.name, field.default) error = errors.get(field.name) field_class = "type-" + field.type.split()[0].replace(":", "-") placeholder = ( field._placeholder if "_placeholder" in field.__dict__ else None ) title = field._title if "_title" in field.__dict__ else None # only diplay field if readable or writable if not field.readable and not field.writable: continue # if this is a reaonly field only show readable fields if readonly: if not field.readable: continue # if this is an create form (unkown id) then only show writable fields elif not vars.get("id"): if not field.writable: continue # ignore blob fields if field.type == "blob": # never display blobs (mistake?) continue # ignore fields of type id its value is equal to None if field.type == "id" and value is None: field.writable = False continue # if the form is readonly or this is an id type field, display it as readonly if readonly or not field.writable or field.type == "id": if field.type == "boolean": control = INPUT( _type="checkbox", _id=input_id, _name=field.name, _value="ON", _disabled="", _checked=value, _title=title, ) else: control = DIV( field.represent and field.represent(value) or value or "" ) # if we have a widget for the field use it elif field.widget: control = field.widget(table, value) # else pick the proper default widget elif field.type == "text": control = TEXTAREA( value or "", _id=input_id, _name=field.name, _placeholder=placeholder, _title=title, ) elif field.type == "date": control = INPUT( _value=value, _type="date", _id=input_id, _name=field.name, _placeholder=placeholder, _title=title, ) elif field.type == "datetime": if isinstance(value, str): value = value.replace(" ", "T") control = INPUT( _value=value, _type="datetime-local", _id=input_id, _name=field.name, _placeholder=placeholder, _title=title, ) elif field.type == "time": control = INPUT( _value=value, _type="time", _id=input_id, _name=field.name, _placeholder=placeholder, _title=title, ) elif field.type == "boolean": control = INPUT( _type="checkbox", _id=input_id, _name=field.name, _value="ON", _checked=value, _title=title, ) elif field.type == "upload": control = DIV() if value and field.download_url is not None and not error: download_div = DIV() download_div.append( LABEL( "Currently: ", ) ) download_div.append( A(" download ", _href=field.download_url(value)) ) download_div.append( INPUT( _type="checkbox", _value="ON", _name="_delete_" + field.name, _title=title, ) ) download_div.append(" (check to remove)") control.append(download_div) control.append(LABEL("Change: ")) control.append(INPUT(_type="file", _id=input_id, _name=field.name)) elif get_options(field.requires) is not None and field.writable == True: multiple = field.type.startswith("list:") value = list(map(str, value if isinstance(value, list) else [value])) option_tags = [ OPTION(v, _value=k, _selected=(not k is None and k in value)) for k, v in get_options(field.requires) ] control = SELECT( *option_tags, _id=input_id, _name=field.name, _multiple=multiple, _title=title ) else: field_type = "password" if field.type == "password" else "text" if field.type.startswith("list:"): value = json.dumps(value or []) control = INPUT( _type=field_type, _id=input_id, _name=field.name, _value=None if field.type == "password" else value, _class=field_class, _placeholder=placeholder, _title=title, _autocomplete="off" if field_type == "password" else "on", ) key = control.name.rstrip("/") if key == "input": key += "[type=%s]" % (control["_type"] or "text") control["_class"] = ( control.attributes.get("_class", "") + " " + self.classes.get(key, "") ).strip() controls["labels"][field.name] = field.label controls["widgets"][field.name] = control controls["comments"][field.name] = field.comment if field.comment else "" controls["titles"][field.name] = title controls["placeholders"][field.name] = placeholder if error: controls["errors"][field.name] = error if field.type == "boolean": form.append( DIV( SPAN(control, _class=class_inner), LABEL( " " + field.label, _for=input_id, _class=class_label, _style="display: inline !important", ), P(error, _class=class_error) if error else "", P(field.comment or "", _class=class_info), _class=class_outer, ) ) else: form.append( DIV( LABEL(field.label, _for=input_id, _class=class_label), DIV(control, _class=class_inner), P(error, _class=class_error) if error else "", P(field.comment or "", _class=class_info), _class=class_outer, ) ) if vars.get("id"): form.append(INPUT(_name="id", _value=vars["id"], _hidden=True)) if deletable: controls["delete"] = INPUT( _type="checkbox", _value="ON", _name="_delete", _class=self.classes["input[type=checkbox]"], ) form.append( DIV( SPAN( controls["delete"], _class=class_inner, _stye="vertical-align: middle;", ), P( " check to delete", _class="help", _style="display: inline !important", ), _class=class_outer, ) ) controls["submit"] = INPUT( _type="submit", _value="Submit", _class=self.classes["input[type=submit]"], ) submit = DIV( DIV( controls["submit"], _class=class_inner, ), _class=class_outer, ) form.append(submit) return dict(form=form, controls=controls)
def __call__(self): """This method returns the element that can be included in the page.""" return XML(Grid.GRID.format(url=self.url()))
def render_table(self): html = DIV(**self.param.grid_class_style.get("grid-wrapper")) grid_header = DIV(**self.param.grid_class_style.get("grid-header")) # build the New button if needed if self.param.create and self.param.create != "": if isinstance(self.param.create, str): create_url = self.param.create else: create_url = self.endpoint + "/new" create_url += "?%s" % self.referrer grid_header.append( self.render_action_button( create_url, self.param.new_action_button_text, "fa-plus", icon_size="normal", override_classes=self.param.grid_class_style.classes.get( "grid-new-button", ""), override_styles=self.param.grid_class_style.get( "new_button"), )) # build the search form if provided if self.param.search_form: grid_header.append(self.render_search_form()) elif self.param.search_queries and len(self.param.search_queries) > 0: grid_header.append(self.render_default_form()) html.append(grid_header) table = TABLE(**self.param.grid_class_style.get("grid-table")) # build the header table.append(self.render_table_header()) # build the rows table.append(self.render_table_body()) # add the table to the html html.append( DIV(table, **self.param.grid_class_style.get("grid-table-wrapper"))) # add the row counter information footer = DIV(**self.param.grid_class_style.get("grid-footer")) row_count = DIV(**self.param.grid_class_style.get("grid-info")) row_count.append("Displaying rows %s thru %s of %s" % ( self.page_start + 1 if self.number_of_pages > 1 else 1, self.page_end if self.page_end < self.total_number_of_rows else self.total_number_of_rows, self.total_number_of_rows, )) if self.number_of_pages > 0 else row_count.append( "No rows to display") footer.append(row_count) # build the pager if self.number_of_pages > 1: footer.append(self.render_table_pager()) html.append(footer) return XML(html)
class Grid: FORMATTERS_BY_TYPE = { "boolean": lambda value: INPUT(_type="checkbox", _checked=value) if value else "", "datetime": lambda value: XML("<script>document.write((new Date(%s,%s,%s,%s,%s,%s)).toLocaleString())</script>" % ( value.year, value.month - 1, value.day, value.hour, value.minute, value.second, )) if value else "", "time": lambda value: XML( "<script>document.write((new Date(0, 0, 0,%s,%s,%s)).toLocaleString().split(', ')[1])</script>" % (value.hour, value.minute, value.second)) if value else "", "date": lambda value: XML( '<script>document.write((new Date(%s,%s,%s)).toLocaleString().split(",")[0])</script>' % ( value.year, value.month - 1, value.day, )) if value else "", "list:string": lambda value: ", ".join(str(x) for x in value) if value else "", "list:integer": lambda value: ", ".join(x for x in value) if value else "", "default": lambda value: str(value) if value is not None else "", } def __init__( self, path, query, search_form=None, search_queries=None, fields=None, show_id=False, orderby=None, left=None, headings=None, create=True, details=True, editable=True, deletable=True, pre_action_buttons=None, post_action_buttons=None, auto_process=True, rows_per_page=15, include_action_button_text=True, search_button_text="Filter", formstyle=FormStyleDefault, grid_class_style=GridClassStyle, ): """ Grid is a searchable/sortable/pageable grid :param path: The part of the URL to be parsed by this grid :param query: the query used to filter the data :param search_form: py4web FORM to be included as the search form :param search_queries: future use - pass a dict of name and a search query :param fields: list of fields to display on the list page, if blank, glean tablename from first query : and use all fields of that table :param show_id: show the record id field on list page - default = False :param orderby: pydal orderby field or list of fields :param left: if joining other tables, specify the pydal left expression here :param headings: list of headings to be used for list page - if not provided use the field label :param create: URL to redirect to for creating records - set to False to not display the button :param editable: URL to redirect to for editing records - set to False to not display the button :param deletable: URL to redirect to for deleting records - set to False to not display the button :param pre_action_buttons: list of action_button instances to include before the standard action buttons :param post_action_buttons: list of action_button instances to include after the standard action buttons :param auto_process: True/False - automatically process the sql for the form - if False, user is responsible for calling process(). """ # in case the query is a Table instead if isinstance(query, query._db.Table): query = query._id != None self.path = path or "" self.db = query._db self.param = Param( query=query, fields=fields, show_id=show_id, orderby=orderby, left=left, search_form=search_form, search_queries=search_queries, headings=headings or [], create=create, details=details, editable=editable, deletable=deletable, pre_action_buttons=pre_action_buttons, post_action_buttons=post_action_buttons, rows_per_page=rows_per_page, include_action_button_text=include_action_button_text, search_button_text=search_button_text, formstyle=formstyle, grid_class_style=grid_class_style, new_sidecar=None, new_submit_value=None, new_action_button_text="New", details_sidecar=None, details_submit_value=None, details_action_button_text="Details", edit_sidecar=None, edit_submit_value=None, edit_action_button_text="Edit", delete_action_button_text="Delete", htmx_target=None, ) # instance variables that will be computed self.action = None self.current_page_number = None self.endpoint = request.fullpath if self.path: self.endpoint = self.endpoint[:-len(self.path)].rstrip("/") self.hidden_fields = None self.form = None self.number_of_pages = None self.page_end = None self.page_start = None self.query_parms = request.params self.readonly_fields = None self.record_id = None self.rows = None self.tablename = None self.total_number_of_rows = None self.use_tablename = self.is_join() self.formatters = {} self.formatters_by_type = copy.copy(Grid.FORMATTERS_BY_TYPE) if auto_process: self.process() def process(self): query = None db = self.db if not self.param.search_form and self.param.search_queries: search_type = safe_int(request.query.get("search_type", 0), default=0) search_string = request.query.get("search_string") if search_type < len(self.param.search_queries) and search_string: query_lambda = self.param.search_queries[search_type][1] try: query = query_lambda(search_string) except: pass # flash a message here if not query: query = self.param.query else: query &= self.param.query parts = self.path.split("/") self.action = parts[0] or "select" self.tablename = self.get_tablenames( self.param.query)[0] # what if there ar 2? self.record_id = safe_int(parts[1] if len(parts) > 1 else None, default=None) if self.param.fields: if not isinstance(self.param.fields, list): self.param.fields = [self.param.fields] else: table = db[self.tablename] self.param.fields = [field for field in table if field.readable] self.readonly_fields = [ field for field in self.param.fields if not field.writable ] self.referrer = None if not self.tablename: raise HTTP(400) if self.action in ["new", "details", "edit"]: # SECURITY: if the record does not exist or does not match query, than we are not allowed if self.record_id: if (db((db[self.tablename]._id == self.record_id) & self.param.query).count() == 0): redirect(self.endpoint) ## maybe flash readonly = self.action == "details" for field in self.readonly_fields: db[field.tablename][field.name].writable = False if not self.param.show_id: # if not show id, find the "id" field and set readable/writable to False for field in db[self.tablename]: if field.type == "id": db[self.tablename][field.name].readable = False db[self.tablename][field.name].writable = False attrs = ({ "_hx-post": request.url, "_hx-target": self.param.htmx_target, "_hx-swap": "innertHTML", } if self.param.htmx_target else {}) self.form = Form( db[self.tablename], record=self.record_id, readonly=readonly, formstyle=self.param.formstyle, **attrs, ) if self.action == "new": if self.param.new_sidecar: self.form.param.sidecar.append(self.param.new_sidecar) if self.param.new_submit_value: self.form.param.submit_value = self.param.new_submit_value if self.action == "details": if self.param.details_sidecar: self.form.param.sidecar.append(self.param.details_sidecar) if self.param.details_submit_value: self.form.param.submit_value = self.param.details_submit_value if self.action == "edit": if self.param.edit_sidecar: self.form.param.sidecar.append(self.param.edit_sidecar) if self.param.edit_submit_value: self.form.param.submit_value = self.param.edit_submit_value # SECURITY: if the new record was created but does not match filter, delete it if self.form.accepted and not self.record_id: new_record = db[self.tablename]._id == self.form.vars["id"] if db(new_record & self.param.query).count() == 0: db(new_record).delete() # TODO: SHOULD FLASH SOME MESSAGE # redirect to the referrer if self.form.accepted or (readonly and request.method == "POST"): referrer = request.query.get("_referrer") if referrer: redirect( base64.b16decode( referrer.encode("utf8")).decode("utf8")) else: redirect(self.endpoint) elif self.action == "delete": db(db[self.tablename].id == self.record_id).delete() url = parse_referer() if url and url.query: self.endpoint += "?%s" % url.query redirect(self.endpoint) elif self.action == "select": self.referrer = "_referrer=%s" % base64.b16encode( request.url.encode("utf8")).decode("utf8") # find the primary key of the primary table pt = db[self.tablename] key_is_missing = True for field in self.param.fields: if (field.table._tablename == pt._tablename and field.name == pt._id.name): key_is_missing = False if key_is_missing: # primary key wasn't included, add it and set show_id to False so it doesn't display self.param.fields.append(pt._id) self.param.show_id = False self.current_page_number = safe_int(request.query.get("page"), default=1) select_params = dict() # try getting sort order from the request sort_order = request.query.get("orderby", "") try: parts = sort_order.lstrip("~").split(".") orderby = db[parts[0]][parts[1]] if sort_order.startswith("~"): orderby = ~orderby select_params["orderby"] = orderby except (IndexError, KeyError, TypeError, AttributeError): select_params["orderby"] = self.param.orderby if self.param.left: select_params["left"] = self.param.left if self.param.left: # TODO: maybe this can be made more efficient self.total_number_of_rows = len( db(query).select(db[self.tablename].id, **select_params)) else: self.total_number_of_rows = db(query).count() # if at a high page number and then filter causes less records to be displayed, reset to page 1 if (self.current_page_number - 1) * self.param.rows_per_page > self.total_number_of_rows: self.current_page_number = 1 if self.total_number_of_rows > self.param.rows_per_page: self.page_start = self.param.rows_per_page * ( self.current_page_number - 1) self.page_end = self.page_start + self.param.rows_per_page select_params["limitby"] = (self.page_start, self.page_end) else: self.page_start = 0 if self.total_number_of_rows > 1: self.page_start = 1 self.page_end = self.total_number_of_rows if self.param.fields: self.rows = db(query).select(*self.param.fields, **select_params) else: self.rows = db(query).select(**select_params) self.number_of_pages = self.total_number_of_rows // self.param.rows_per_page if self.total_number_of_rows % self.param.rows_per_page > 0: self.number_of_pages += 1 else: redirect(self.endpoint) def iter_pages( self, current_page, num_pages, left_current=1, right_current=1, left_edge=1, right_edge=1, ): """ generator used to determine which page numbers should be shown on the Grid pager :param left_current: # of pages to add to the left of current page :param right_current: # of fpages to add to the right of current page :param left_edge: # of pages to add to the left end :param right_edge: # of fpages to add to the right end """ left_range = set(range(1, min(2 + left_edge, num_pages + 1))) mid_range = set( range( max(1, current_page - left_current), min(current_page + right_current + 1, num_pages + 1), )) right_range = set(range(max(1, num_pages - right_edge), num_pages + 1)) return list(sorted(left_range | mid_range | right_range)) def render_action_button( self, url, button_text, icon, icon_size="small", additional_classes=None, additional_styles=None, override_classes=None, override_styles=None, message=None, onclick=None, row_id=None, name="grid-button", row=None, **attr, ): separator = "?" if row_id: url += "/%s" % row_id classes = self.param.grid_class_style.classes.get(name, "") styles = self.param.grid_class_style.styles.get(name, "") def join(items): return (" ".join(items) if isinstance(items, (list, tuple)) else " %s" % items) if override_classes: classes = join(override_classes) elif additional_classes: classes += join(additional_classes) if override_styles: styles = join(override_styles) elif additional_styles: styles += join(additional_styles) if callable(url): url = url(row) if self.param.htmx_target: attr["_hx-get"] = url attr["_hx-target"] = self.param.htmx_target attr["_hx-swap"] = "innertHTML" else: attr["_href"] = url link = A( I(_class="fa %s" % icon), _role="button", _class=classes, _message=message, _title=button_text, _style=styles, **attr, ) if self.param.include_action_button_text: link.append( XML('<span class="grid-action-button-text"> %s</span>' % button_text)) return link def render_default_form(self): search_type = safe_int(request.query.get("search_type", 0), default=0) search_string = request.query.get("search_string") options = [ OPTION(items[0], _value=k, _selected=(k == search_type)) for k, items in enumerate(self.param.search_queries) ] hidden_fields = [ INPUT(_name=key, _value=request.query.get(key), _type="hidden") for key in request.query if not key in ("search_type", "search_string") ] if self.param.htmx_target: attrs = { "_hx-post": self.endpoint, "_hx-target": self.param.htmx_target, "_hx-swap": "innertHTML", } else: attrs = {"_method": "GET", "_action": self.endpoint} form = FORM(*hidden_fields, **attrs) select = SELECT( *options, **dict(_name="search_type", ), ) input = INPUT( _type="text", _name="search_string", _value=search_string, ) submit = INPUT(_type="submit", _value="Search") clear_script = "document.querySelector('[name=search_string]').value='';" clear = INPUT(_type="submit", _value="Clear", _onclick=clear_script) div = DIV(_id="grid-search", **self.param.grid_class_style.get("grid-search")) # we do not need classes for these elements tr = TR() if len(options) > 1: tr.append(TD(select)) tr.append(TD(input)) tr.append(TD(submit, clear)) form.append(TABLE(tr)) div.append(form) return div def render_search_form(self): # TODO: Do we need this? div = DIV(_id="grid-search", **self.param.grid_class_style.get("grid-search")) div.append(self.param.search_form.custom["begin"]) tr = TR(**self.param.grid_class_style.get("search_form_tr")) for field in self.param.search_form.table: td = TD(**self.param.grid_class_style.get("search_form_td")) if field.type == "boolean": sb = DIV(**self.param.grid_class_style.get("search_boolean")) sb.append(self.param.search_form.custom["widgets"][field.name]) sb.append(field.label) td.append(sb) else: td.append(self.param.search_form.custom["widgets"][field.name]) if (field.name in self.param.search_form.custom["errors"] and self.param.search_form.custom["errors"][field.name]): td.append( DIV( self.param.search_form.custom["errors"][field.name], _style="color:#ff0000", )) tr.append(td) if self.param.search_button_text: tr.append( TD( INPUT( _class="button", _type="submit", _value=self.param.search_button_text, ), **self.param.grid_class_style.get("search_form_td"), )) else: tr.append( TD( self.param.search_form.custom["submit"], **self.param.grid_class_style.get("search_form_td"), )) div.append( TABLE(tr, **self.param.grid_class_style.get("search_form_table"))) for hidden_widget in self.param.search_form.custom[ "hidden_widgets"].keys(): div.append( self.param.search_form.custom["hidden_widgets"][hidden_widget]) div.append(self.param.search_form.custom["end"]) return div def render_table_header(self): up = I(**self.param.grid_class_style.get("grid-sorter-icon-up")) dw = I(**self.param.grid_class_style.get("grid-sorter-icon-down")) columns = [] sort_order = request.query.get("orderby", "") for index, field in enumerate(self.param.fields): if field.readable and (field.type != "id" or self.param.show_id): key = "%s.%s" % (field.tablename, field.name) heading = (self.param.headings[index] if index < len(self.param.headings) else field.label) heading = title(heading) # add the sort order query parm sort_query_parms = dict(self.query_parms) if key == sort_order: sort_query_parms["orderby"] = "~" + key href = URL(self.endpoint, vars=sort_query_parms) col = A(heading, up, _href=href) else: sort_query_parms["orderby"] = key href = URL(self.endpoint, vars=sort_query_parms) col = A(heading, dw if "~" + key == sort_order else "", _href=href) columns.append((key, col)) thead = THEAD( _class=self.param.grid_class_style.classes.get("grid-thead", "")) for key, col in columns: col_class = " grid-col-%s" % key thead.append( TH( col, _class=self.param.grid_class_style.classes.get( "grid-th", "") + col_class, _style=self.param.grid_class_style.styles.get("grid-th"), )) if (self.param.details or self.param.editable or self.param.deletable or self.param.pre_action_buttons or self.param.post_action_buttons): thead.append( TH("", **self.param.grid_class_style.get("grid-th-action-button"))) return thead def render_field(self, row, field, field_index): """ Render a field if only 1 table in the query, the no table name needed when getting the row value - however, if there are multiple tables in the query (self.use_tablename == True) then we need to use the tablename as well when accessing the value in the row object the row object sent in can take :param row: :param field: :return: """ if self.use_tablename: field_value = row[field.tablename][field.name] else: field_value = row[field.name] key = "%s.%s" % (field.tablename, field.name) formatter = (self.formatters.get(key) or self.formatters_by_type.get(field.type) or self.formatters_by_type.get("default")) class_type = "grid-cell-type-%s" % str( field.type).split(":")[0].split("(")[0] class_col = " grid-col-%s" % key td = TD( formatter(field_value), _class=(self.param.grid_class_style.classes.get("grid-td", "") + " " + class_type if class_type not in self.param.grid_class_style.classes.get( class_type, "").split(" ") else "" + " " + self.param.grid_class_style.classes.get(class_type, "") + " " + class_col).strip(), _style=(self.param.grid_class_style.styles.get(class_type) or self.param.grid_class_style.styles.get("grid-td")), ) return td def render_table_body(self): tbody = TBODY() for row in self.rows: # find the row id - there may be nested tables.... if self.use_tablename and self.tablename in row and "id" not in row: row_id = row[self.tablename]["id"] else: row_id = row["id"] self.use_tablename = False tr = TR( _role="row", _class=self.param.grid_class_style.classes.get("grid-tr"), _style=self.param.grid_class_style.styles.get("grid-tr"), ) # add all the fields to the row for index, field in enumerate(self.param.fields): if field.readable and (field.type != "id" or self.param.show_id): tr.append(self.render_field(row, field, index)) td = None # add the action buttons if ((self.param.details and self.param.details != "") or (self.param.editable and self.param.editable != "") or (self.param.deletable and self.param.deletable != "") or (self.param.post_action_buttons or self.param.pre_action_buttons)): classes = ( self.param.grid_class_style.classes.get("grid-td", "") + " " + self.param.grid_class_style.classes.get( "grid-td-action-button")).strip() styles = ( self.param.grid_class_style.styles.get("grid-td", "") + " " + self.param.grid_class_style.styles.get( "grid-td-action-button")).strip() td = TD(_class=classes, _style=styles) if self.param.pre_action_buttons: for btn in self.param.pre_action_buttons: if btn.onclick: btn.url = None td.append( self.render_action_button( btn.url, btn.text, btn.icon, _onclick=btn.onclick, additional_classes=btn.additional_classes, message=btn.message, row_id=row_id if btn.append_id else None, row=row, )) if self.param.details and self.param.details != "": if isinstance(self.param.details, str): details_url = self.param.details else: details_url = self.endpoint + "/details" details_url += "/%s?%s" % (row_id, self.referrer) td.append( self.render_action_button( details_url, self.param.details_action_button_text, "fa-id-card", name="grid-details-button", )) if self.param.editable and self.param.editable != "": if isinstance(self.param.editable, str): edit_url = self.param.editable else: edit_url = self.endpoint + "/edit" edit_url += "/%s?%s" % (row_id, self.referrer) td.append( self.render_action_button( edit_url, self.param.edit_action_button_text, "fa-edit", name="grid-edit-button", )) if self.param.deletable and self.param.deletable != "": if isinstance(self.param.deletable, str): delete_url = self.param.deletable else: delete_url = self.endpoint + "/delete" delete_url += "/%s?%s" % (row_id, self.referrer) td.append( self.render_action_button( delete_url, self.param.delete_action_button_text, "fa-trash", additional_classes="confirmation", message="Delete record", name="grid-delete-button", _onclick= "if(!confirm('sure you want to delete')) return false;", )) if self.param.post_action_buttons: for btn in self.param.post_action_buttons: if btn.onclick: btn.url = None td.append( self.render_action_button( btn.url, btn.text, btn.icon, _onclick=btn.onclick, additional_classes=btn.additional_classes, message=btn.message, row_id=row_id if btn.append_id else None, row=row, )) tr.append(td) tbody.append(tr) return tbody def render_table_pager(self): pager = DIV(**self.param.grid_class_style.get("grid-pagination")) previous_page_number = None for page_number in self.iter_pages(self.current_page_number, self.number_of_pages): pager_query_parms = dict(self.query_parms) pager_query_parms["page"] = page_number # if there is a gat add a spacer if previous_page_number and page_number - previous_page_number > 1: pager.append(SPAN("...", _style="margin:0 10px;")) is_current = self.current_page_number == page_number page_name = ("grid-pagination-button-current" if is_current else "grid-pagination-button") attrs = ({ "_hx-get": URL(self.endpoint, vars=pager_query_parms), "_hx-target": self.param.htmx_target, "_hx-swap": "innertHTML", } if self.param.htmx_target else { "_href": URL(self.endpoint, vars=pager_query_parms) }) pager.append( A( page_number, **self.param.grid_class_style.get(page_name), _role="button", **attrs, )) previous_page_number = page_number return pager def render_table(self): html = DIV(**self.param.grid_class_style.get("grid-wrapper")) grid_header = DIV(**self.param.grid_class_style.get("grid-header")) # build the New button if needed if self.param.create and self.param.create != "": if isinstance(self.param.create, str): create_url = self.param.create else: create_url = self.endpoint + "/new" create_url += "?%s" % self.referrer grid_header.append( self.render_action_button( create_url, self.param.new_action_button_text, "fa-plus", icon_size="normal", override_classes=self.param.grid_class_style.classes.get( "grid-new-button", ""), override_styles=self.param.grid_class_style.get( "new_button"), )) # build the search form if provided if self.param.search_form: grid_header.append(self.render_search_form()) elif self.param.search_queries and len(self.param.search_queries) > 0: grid_header.append(self.render_default_form()) html.append(grid_header) table = TABLE(**self.param.grid_class_style.get("grid-table")) # build the header table.append(self.render_table_header()) # build the rows table.append(self.render_table_body()) # add the table to the html html.append( DIV(table, **self.param.grid_class_style.get("grid-table-wrapper"))) # add the row counter information footer = DIV(**self.param.grid_class_style.get("grid-footer")) row_count = DIV(**self.param.grid_class_style.get("grid-info")) row_count.append("Displaying rows %s thru %s of %s" % ( self.page_start + 1 if self.number_of_pages > 1 else 1, self.page_end if self.page_end < self.total_number_of_rows else self.total_number_of_rows, self.total_number_of_rows, )) if self.number_of_pages > 0 else row_count.append( "No rows to display") footer.append(row_count) # build the pager if self.number_of_pages > 1: footer.append(self.render_table_pager()) html.append(footer) return XML(html) def render(self): """ build the query table :return: html representation of the table or the py4web Form object """ if self.action == "select": return self.render_table() elif self.action in ["new", "details", "edit"]: return self.form def data(self): """ get the record that is being edited / displayed :return: DAL record of the record being edited """ return (self.db[self.tablename](self.record_id) if self.tablename and self.record_id else None) def add_search_query(self, name, query, requires): if self.param.search_form: raise ValueError( "Cannot add search queries if a you provide a search_form to the grid call " "or if auto_process is set to True. Ensure no search_form is set, set " "auto_process to False, add your search query and then call grid.process()." ) if self.param.search_queries is None: self.param.search_queries = [] self.param.search_queries.append([name, query, requires]) def get_tablenames(self, *args): """Returns the tablenames used by this grid""" return list(self.db._adapter.tables(*args).keys()) def is_join(self): items = [self.param.query] if self.param.left is not None: if isinstance(self.param.left, (list, tuple)): items += [item for item in self.param.left] else: items += [self.param.left] return len(self.get_tablenames(*items)) > 1
def __call__(self, id=None): """This method returns the element that can be included in the page. @param id: id of the file uploaded. This can be useful if there are multiple instances of this form on the page.""" return XML(FileUpload.FILE_UPLOAD.format(url=self.url(id=id)))
def test_sanitize(self): permitted_tags = [ 'div', 'td', 'b', 'br/', 'strong', 'span', 'img/', 'a', ] allowed_attributes = { 'a': ['href', 'title'], 'img': ['src', 'alt'], 'blockquote': ['type'], 'td': ['colspan'], } # test permitted for x in permitted_tags: T = TAG[x] s_tag = T().xml() if x == "img/": # alt or src attribute is required. src has to have a valid href s_tag = T(_alt="empty").xml() self.assertEqual( XML(s_tag, sanitize=True, permitted_tags=['img/'], allowed_attributes={ 'img': ['src', 'alt'] }).xml(), "<img alt=\"empty\"/>") s_tag = T(_src="/image.png").xml() self.assertEqual( XML(s_tag, sanitize=True, permitted_tags=['img/'], allowed_attributes={ 'img': ['src', 'alt'] }).xml(), "<img src=\"/image.png\"/>") elif x == "a": # It has to have a valid href or title or not tag empty s_tag = T("this is a link", _href="http://web2py.com/").xml() self.assertEqual( XML(s_tag, sanitize=True, permitted_tags=['a'], allowed_attributes={ 'a': ['href', 'title'] }).xml(), "<a href=\"http://web2py.com/\">this is a link</a>") s_tag = T("without href", _title="this is a link?").xml() self.assertEqual( XML(s_tag, sanitize=True, permitted_tags=['a'], allowed_attributes={ 'a': ['href', 'title'] }).xml(), '<a title="this is a link?">without href</a>') s_tag = T(_title="empty_tag").xml() self.assertEqual( XML(s_tag, sanitize=True, permitted_tags=['a'], allowed_attributes={ 'a': ['href', 'title'] }).xml(), '<a title="empty_tag"></a>') else: self.assertEqual( XML(s_tag, sanitize=True, permitted_tags=permitted_tags, allowed_attributes=allowed_attributes).xml(), "<%s></%s>" % (x, x) if not x[-1] == "/" else "<%s>" % x) # test tag out of list out_of_list = [ 'blockquote', 'i', 'li', 'ol', 'ul', 'p', 'cite', 'code', 'pre', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'table', 'tbody', 'thead', 'tfoot', 'tr' 'strong' ] for x in out_of_list: T = TAG[x] self.assertEqual( XML(T().xml(), sanitize=True, permitted_tags=permitted_tags, allowed_attributes=allowed_attributes).xml(), "<%s></%s>" % (x, x)) # test unusual tags for x in ["evil", "n0c1v3"]: T = TAG[x] self.assertEqual( XML(T().xml(), sanitize=True, permitted_tags=permitted_tags, allowed_attributes=allowed_attributes).xml(), "<%s></%s>" % (x, x)) # test allowed_attributes s_tag = TAG['td']("content_td", _colspan="2", _extra_attr="invalid").xml() self.assertEqual( XML(s_tag, sanitize=True, permitted_tags=['td'], allowed_attributes={ 'td': ['colspan'] }).xml(), '<td colspan="2">content_td</td>') s_tag = TAG['a']("link", _href="http://web2py.com/", _title="my_title").xml() self.assertEqual( XML(s_tag, sanitize=True, permitted_tags=['a'], allowed_attributes={ 'a': ['href', 'title'] }).xml(), '<a href="http://web2py.com/" title="my_title">link</a>') s_tag = TAG['img/'](_alt="empty", _src="/images/logo.png").xml() self.assertEqual( XML(s_tag, sanitize=True, permitted_tags=['img/'], allowed_attributes={ 'img': ['src', 'alt'] }).xml(), '<img src="/images/logo.png" alt="empty"/>') s_tag = TAG['div']("content", _style="{backgrond-color: red;}").xml() self.assertEqual( XML(s_tag, sanitize=True, permitted_tags=['div'], allowed_attributes={ 'div': ['style'] }).xml(), '<div style="{backgrond-color: red;}">content</div>') self.assertEqual( XML(TAG['a']("oh no!", _href="invalid_link").xml(), sanitize=True, permitted_tags=['a']).xml(), 'oh no!') self.assertEqual( XML(TAG['div']("", _onclick="evil()").xml(), sanitize=True, permitted_tags=['div']).xml(), '<div></div>') # valid inside invalid s_tag = TAG['evil'](TAG['div']('valid'), _style="{backgrond-color: red;}").xml() self.assertEqual( XML(s_tag, sanitize=True, permitted_tags=['div'], allowed_attributes={ 'div': ['style'] }).xml(), '<evil><div>valid</div></evil>') self.assertEqual( XML(TAG['a'](TAG['img/'](_src="/index.html"), _class="teste").xml(), sanitize=True, permitted_tags=['a', 'img/']).xml(), '<img src="/index.html"/>') # tags deleted even allowed self.assertEqual( XML(TAG['img/']().xml(), sanitize=True, permitted_tags=['img']).xml(), "") self.assertEqual( XML(TAG['img/'](_src="invalid_url").xml(), sanitize=True, permitted_tags=['img']).xml(), "") self.assertEqual( XML(TAG['img/'](_class="teste").xml(), sanitize=True, permitted_tags=['img']).xml(), "") self.assertEqual( XML(TAG['a'](_href="invalid_link").xml(), sanitize=True, permitted_tags=['a']).xml(), "")
def mtable(self, table): path = self.path.replace("<tablename>", table._tablename) return XML(MTABLE.format(url=URL(path), render={}))