def _make_collection_root_link(): global _collection_root_link if _collection_root_link is None: _collection_root_link = node_url(get_collections_node().id) return _collection_root_link
def _display(req, show_navbar=True, render_paths=True, params=None): if "jsonrequest" in req.params: return handle_json_request(req) if params is None: params = req.args nid = params.get("id", type=int) if nid is None: node = get_collections_node() else: node = q(Node).prefetch_attrs().prefetch_system_attrs().get(nid) if node is not None and not node.has_read_access(): node = None show_id = [] if req.args.get("disable_content"): content_html = u"" else: content_html = render_content(node, req, render_paths, show_id) if params.get("raw"): req.write(content_html) else: html = render_page(req, node, content_html, show_navbar, show_id) req.write(html)
def handle_json_request(req): s = [] if req.args.get("cmd") == "get_list_smi": searchmaskitem_id = req.params.get("searchmaskitem_id") f = None g = None if searchmaskitem_id and searchmaskitem_id != "full": f = q(Node).get(searchmaskitem_id).getFirstField() if not f: # All Metadata f = g = getMetadataType("text") container_id = req.args.get("container_id") container = q(Container).get(container_id) if container_id else None if container is None or not container.has_read_access(): container = get_collections_node() s = [ f.getSearchHTML( Context(g, value=req.args.get("query_field_value"), width=174, name="query" + str(req.args.get("fieldno")), language=lang(req), container=container, user=current_user, ip=req.ip)) ] req.write( req.params.get("jsoncallback") + "(%s)" % json.dumps(s, indent=4))
def handle_json_request(req): s = [] if req.args.get("cmd") == "get_list_smi": searchmaskitem_id = req.params.get("searchmaskitem_id") f = None g = None if searchmaskitem_id and searchmaskitem_id != "full": f = q(Node).get(searchmaskitem_id).getFirstField() if not f: # All Metadata f = g = getMetadataType("text") container_id = req.args.get("container_id") container = q(Container).get(container_id) if container_id else None if container is None or not container.has_read_access(): container = get_collections_node() s = [ f.getSearchHTML( Context( g, value=req.args.get("query_field_value"), width=174, name="query" + str(req.args.get("fieldno")), language=lang(req), container=container, user=current_user, ip=req.ip))] req.write(req.params.get("jsoncallback") + "(%s)" % json.dumps(s, indent=4))
def make_navtree_entries(language, collection, container): hide_empty = collection.get("style_hide_empty") == "1" opened = {t[0] for t in container.all_parents.with_entities(Node.id)} opened.add(container.id) navtree_entries = [] def make_navtree_entries_rec(navtree_entries, node, indent, hide_empty): small = not isinstance(node, (Collection, Collections)) e = NavTreeEntry(node, indent, small, hide_empty, language) if node.id == collection.id or node.id == container.id: e.active = 1 navtree_entries.append(e) if node.id in opened: e.folded = 0 """ find children ids for a given node id for an additional filter to determine the container_children important: this additional filter is logical not needed - but it speeds up the computation of the container_children especially for guest users this additional filter is only used if the number of children ids is lower than 100 """ children_ids = db.session.execute( "select cid from nodemapping where nid = %d" % node.id) cids = [row['cid'] for row in children_ids] if len(cids) < 100: container_children = node.container_children.filter( Node.id.in_(cids)).filter_read_access().order_by( Node.orderpos).prefetch_attrs() else: container_children = node.container_children.filter_read_access( ).order_by(Node.orderpos).prefetch_attrs() for c in container_children: if hasattr(node, "dont_ask_children_for_hide_empty"): style_hide_empty = hide_empty else: style_hide_empty = c.get("style_hide_empty") == "1" make_navtree_entries_rec(navtree_entries, c, indent + 1, style_hide_empty) collections_root = get_collections_node() if collections_root is not None: time0 = time.time() make_navtree_entries_rec(navtree_entries, collections_root, 0, hide_empty) time1 = time.time() logg.info("make_navtree_entries: %f", time1 - time0) return navtree_entries
def make_navtree_entries(language, collection, container): hide_empty = collection.get("style_hide_empty") == "1" opened = {t[0] for t in container.all_parents.with_entities(Node.id)} opened.add(container.id) navtree_entries = [] def make_navtree_entries_rec(navtree_entries, node, indent, hide_empty): small = not isinstance(node, (Collection, Collections)) e = NavTreeEntry(node, indent, small, hide_empty, language) if node.id == collection.id or node.id == container.id: e.active = 1 navtree_entries.append(e) if node.id in opened: e.folded = 0 """ find children ids for a given node id for an additional filter to determine the container_children important: this additional filter is logical not needed - but it speeds up the computation of the container_children especially for guest users this additional filter is only used if the number of children ids is lower than 100 """ children_ids = db.session.execute("select cid from nodemapping where nid = %d" % node.id) cids = [row['cid'] for row in children_ids] if len(cids) < 100: container_children = node.container_children.filter(Node.id.in_(cids)).filter_read_access().order_by(Node.orderpos).prefetch_attrs() else: container_children = node.container_children.filter_read_access().order_by(Node.orderpos).prefetch_attrs() for c in container_children: if hasattr(node, "dont_ask_children_for_hide_empty"): style_hide_empty = hide_empty else: style_hide_empty = c.get("style_hide_empty") == "1" make_navtree_entries_rec(navtree_entries, c, indent + 1, style_hide_empty) collections_root = get_collections_node() if collections_root is not None: time0 = time.time() make_navtree_entries_rec(navtree_entries, collections_root, 0, hide_empty) time1 = time.time() logg.info("make_navtree_entries: %f", time1 - time0) return navtree_entries
def find_collection_and_container(node_id): node = q(Node).get(node_id) if node_id else None if node is None: collection = get_collections_node() container = collection else: if isinstance(node, Container): container = node if isinstance(node, Collection): # XXX: is Collections also needed here? collection = node else: collection = node.get_collection() else: container = node.get_container() collection = node.get_collection() return collection, container
def getLinks(self): guest_user = get_guest_user() l = [Link("/logout", t(self.language, "sub_header_logout_title"), t(self.language, "sub_header_logout"), icon="/img/logout.gif")] if self.user == guest_user: if config.get("config.ssh") == "yes": host = config.get("host.name") or self.host l = [Link("https://" + host + "/login", t(self.language, "sub_header_login_title"), t(self.language, "sub_header_login"), icon="/img/login.gif")] else: l = [Link("/login", t(self.language, "sub_header_login_title"), t(self.language, "sub_header_login"), icon="/img/login.gif")] if self.is_workflow_area: l += [Link("/", t(self.language, "sub_header_frontend_title"), t(self.language, "sub_header_frontend"), icon="/img/frontend.gif")] if self.user.is_editor: idstr = "" if self.id: idstr = "?id=" + unicode(self.id) # set edit-link to upload_dir if user comes from collections if not self.id or int(self.id) == get_collections_node().id: if self.user.upload_dir: idstr = "?id=" + unicode(self.user.upload_dir.id) l += [Link("/edit" + idstr, t(self.language, "sub_header_edit_title"), t(self.language, "sub_header_edit"), icon="/img/edit.gif")] if self.user.is_admin: l += [Link("/admin", t(self.language, "sub_header_administration_title"), t(self.language, "sub_header_administration"), icon="/img/admin.gif")] if self.user.is_workflow_editor: l += [Link("/publish/", t(self.language, "sub_header_workflow_title"), t(self.language, "sub_header_workflow"), icon="/img/workflow.gif")] if self.user.can_change_password: l += [Link("/pwdchange", t(self.language, "sub_header_changepwd_title"), t(self.language, "sub_header_changepwd"), "_parent", icon="/img/changepwd.gif")] return l
def get_node_data_struct(req, path, params, data, id, debug=True, allchildren=False, singlenode=False, parents=False, send_children=False, fetch_files=False, csv=False): res = _prepare_response() timetable = res["timetable"] # verify signature if a user is given, otherwise use guest user if params.get('user'): user = _handle_oauth(res, req.fullpath, params, timetable) else: user = get_guest_user() res['oauthuser'] = '' # username supplied for authentication (login name) in query parameter user if user is not None: res['username'] = user.login_name res['userid'] = user.id else: res['userid'] = '' # unique id for authenticated user if applicable (node.id for internal, dirid for dynamic users) res['username'] = '' # name of the user, may be the name of the guest user or a personal name result_shortlist = [] # query parameters typefilter = params.get( 'type', '') # return only nodes of given type like dissertation/diss parent_type = params.get( 'parent_type', '' ) # return only nodes that have only parents of given type like folder or collection # XXX: do we want version support? # send_versions = params.get('send_versions', '').lower() # return also nodes that are older versions of other nodes # return only nodes that have an EXIF location that lies between the given lon,lat values exif_location_rect = params.get('exif_location_rect', '') mdt_name = params.get('mdt_name', '') attrreg = params.get('attrreg', '') searchquery = params.get('q', '') # node query sortfield = params.get('sortfield', '') sortformat = params.get('sortformat', '') # 'sissfi' limit = params.get("limit", DEFAULT_NODEQUERY_LIMIT) offset = params.get("start", 0) csv_allchildren = csv and allchildren # check node existence node = q(Node).get(id) if node is None: return _client_error_response(404, u"node not found") home = get_home_root_node() collections = get_collections_node() # check node access if node.has_read_access(user=user) and (node.is_descendant_of(collections) or node.is_descendant_of(home)): pass else: return _client_error_response(403, u"forbidden") if mdt_name: mdt = q(Metadatatype).filter_by(name=mdt_name).count() if not mdt: return _client_error_response( 404, u'no such metadata type: ' + mdt_name) if allchildren: if csv: # fetch only those columns which are needed, this is faster than fetch all columns and need less space nodequery = node.all_children_by_query( q(Node.attrs.label("attributes"), Node.id, Node.name, Node.schema, Node.type)) else: nodequery = node.all_children elif parents: nodequery = node.parents else: nodequery = node.children if searchquery: search_languages = get_service_search_languages() try: searchtree = search.parse_searchquery_old_style(searchquery) except search.SearchQueryException as e: return _client_error_response(400, str(e)) nodequery = apply_searchtree_to_query(nodequery, searchtree, search_languages) if typefilter: nodequery = nodequery.filter( (Node.type + "/" + Node.schema).op("~")(typefilter)) if attrreg: spl = attrreg.split('=') if len(spl) != 2: return _client_error_response(400, "wrong attrreg value: " + attrreg) akey, aval = spl nodequery = nodequery.filter(Node.attrs[akey].astext.op("~")(aval)) sortdirection = u"" if sortfield: sfields = [x.strip() for x in sortfield.split(',')] sfields_without_sign = [] sortformat = sortformat[:len(sfields)] for sfield, sformat in izip_longest(sfields, sortformat, fillvalue="s"): if sformat == "i": astype = Integer elif sformat == "f": astype = Float else: astype = Unicode if sfield[0] == "-": sfield = sfield[1:] desc = True sortdirection += u"d" else: desc = False sortdirection += u"u" sfields_without_sign.append(sfield) if sfield == 'node.id': order_expr = Node.id elif sfield == 'node.name': order_expr = Node.name elif sfield == 'node.type': order_expr = Node.type elif sfield == 'node.orderpos': order_expr = Node.orderpos else: order_expr = Node.attrs[sfield].cast(astype) if desc: order_expr = sql.desc(order_expr) nodequery = nodequery.order_by(order_expr.nullslast()) sfields = sfields_without_sign else: sfields = [] ### TODO: do we need this? if parent_type: raise NotImplementedError("parent_type not supported at the moment") # XXX: do we need this? pass ### actually get the nodes if csv_allchildren: nodequery = nodequery.order_by('attributes').distinct() else: nodequery = nodequery.distinct().options(undefer(Node.attrs)) if fetch_files: nodequery = nodequery.options(joinedload(Node.file_objects)) if singlenode: # we already checked that node can be accessed by the user, just return the node nodelist = [node] node_count = 1 limit = 1 else: if mdt_name: nodequery = nodequery.filter(Node.schema == mdt_name) nodequery = nodequery.filter_read_access(user=user) if offset: nodequery = nodequery.offset(offset) if limit: nodequery = nodequery.limit(limit) atime = time.time() try: nodelist = nodequery.all() except Exception as e: return _client_error_response( 400, "the database failed with the message: {}".format(str(e))) node_count = len(nodelist) timetable.append([ 'fetching nodes from db returned {} results'.format(node_count), time.time() - atime ]) atime = time.time() i0 = int(params.get('i0', '0')) i1 = int(params.get('i1', node_count)) def attr_list(node, sfields): r = [] for sfield in sfields: r.append([sfield, node.get(sfield)]) return r if 'add_shortlist' in params: if sortfield: result_shortlist = [[ i, x.id, x.name, x.type, attr_list(x, sfields) ] for i, x in enumerate(nodelist)][i0:i1] timetable.append([ 'build result_shortlist for %d nodes and %d sortfields' % (len(result_shortlist), len(sfields)), time.time() - atime ]) atime = time.time() else: result_shortlist = [[i, x.id, x.name, x.type] for i, x in enumerate(nodelist)][i0:i1] timetable.append([ 'build result_shortlist for %d nodes (no sortfield)' % len(result_shortlist), time.time() - atime ]) atime = time.time() ### XXX: filtering in python, should be moved to the database if exif_location_rect: raise NotImplementedError("not supported at the moment") components = exif_location_rect.split(',') if len(components) != 4: return _client_error_response( 400, u"exif_location_rect is invalid: {}".format( exif_location_rect)) nodelist = _exif_location_filter(nodelist, components) ### build result res['nodelist'] = nodelist res['sfields'] = sfields res['sortfield'] = sortfield res['sortdirection'] = sortdirection res['result_shortlist'] = result_shortlist res['timetable'] = timetable res['nodelist_start'] = offset res['nodelist_limit'] = limit res['nodelist_count'] = node_count res['path'] = req.path res['status'] = 'ok' res['html_response_code'] = '200' # ok res['build_response_end'] = time.time() dataready = "%.3f" % (res['build_response_end'] - res["build_response_start"]) res['dataready'] = dataready return res
def search(searchtype, searchquery, readable_query, paths, req, container_id=None): from web.frontend.content import ContentList if not container_id: container_id = req.args.get("id", type=int) container = q(Container).get(container_id) if container_id else None # if the current node is not a Container or not accessible by the user, use the collections root instead if container is None or not container.has_read_access(): # XXX: The collections root is always searchable. Could there be situations in which we don't want to allow this? # XXX: We could check the read permission for Collections to decide if search is allowed. container = get_collections_node() try: result = container.search(searchquery).filter_read_access() except SearchQueryException as e: # query parsing went wrong or the search backend complained about something return NoSearchResult(readable_query, container, readable_query, error=True) content_list = ContentList(result, container, paths, words=readable_query, show_sidebar=False) try: content_list.feedback(req) except Exception as e: # that should not happen, but is somewhat likely (db failures, illegal search queries that slipped through...), # just show 0 result view and don't confuse the user with unhelpful error messages ;) logg.exception( "exception executing %(searchtype)s search for query %(readable_query)s", dict(searchtype=searchtype, readable_query=readable_query, error=True)) db.session.rollback() return NoSearchResult(readable_query, container, searchtype, error=True) language = lang(req) content_list.linkname = u"{}: {} \"{}\"".format( container.getLabel(language), translate("search_for", language=language), readable_query) content_list.linktarget = "" if content_list.has_elements: logg.info("%s search with query '%s' on container %s produced results", searchtype, readable_query, container_id) return content_list else: logg.info( "%s search with query '%s' on container %s produced no results", searchtype, readable_query, container_id) return NoSearchResult(readable_query, container, searchtype)
def render_page(req, node, content_html, show_navbar=True, show_id=None): """Renders the navigation frame with the inserted content HTML and returns the whole page. """ user = current_user userlinks = UserLinks(user, req) language = lang(req) rootnode = get_collections_node() if node is None: node = rootnode container = rootnode else: container = node.get_container() theme = webconfig.theme front_lang = {"name": config.languages, "actlang": language} frame_context = { "content": Markup(content_html), "id": node.id, "language": front_lang, "show_navbar": show_navbar, } search_html = u"" navtree_html = u"" if show_navbar and not req.args.get("disable_navbar"): if not req.args.get("disable_search"): search_html = render_search_box(container, language, req) if not req.args.get("disable_navtree"): navtree_html = render_navtree(language, node.id, user) ctx_header = { "user_name": user.getName(), "userlinks": userlinks, "header_items": rootnode.getCustomItems("header"), "language": front_lang, "show_language_switcher": (len(front_lang['name']) > 1) } header_html = theme.render_template("frame_header.j2.jade", ctx_header) ctx_footer = { "footer_left_items": rootnode.getCustomItems("footer_left"), "footer_right_items": rootnode.getCustomItems("footer_right") } footer_html = theme.render_template("frame_footer.j2.jade", ctx_footer) frame_context["search"] = Markup(search_html) frame_context["navtree"] = Markup(navtree_html) frame_context["header"] = Markup(header_html) frame_context["footer"] = Markup(footer_html) if show_id and isinstance(show_id, list): show_id = show_id[0] else: show_id = req.args.get("show_id") if show_id: shown_node = q(Node).get(show_id) else: shown_node = node frame_context["google_scholar"] = google_scholar(shown_node) html = theme.render_template("frame.j2.jade", frame_context) return html
def getLinks(self): guest_user = get_guest_user() l = [ Link("/logout", t(self.language, "sub_header_logout_title"), t(self.language, "sub_header_logout"), icon="/img/logout.gif") ] if self.user == guest_user: if config.get("config.ssh") == "yes": host = config.get("host.name") or self.host l = [ Link("https://" + host + "/login", t(self.language, "sub_header_login_title"), t(self.language, "sub_header_login"), icon="/img/login.gif") ] else: l = [ Link("/login", t(self.language, "sub_header_login_title"), t(self.language, "sub_header_login"), icon="/img/login.gif") ] if self.is_workflow_area: l += [ Link("/", t(self.language, "sub_header_frontend_title"), t(self.language, "sub_header_frontend"), icon="/img/frontend.gif") ] if self.user.is_editor: idstr = "" if self.id: idstr = "?id=" + unicode(self.id) # set edit-link to upload_dir if user comes from collections if not self.id or int(self.id) == get_collections_node().id: if self.user.upload_dir: idstr = "?id=" + unicode(self.user.upload_dir.id) l += [ Link("/edit" + idstr, t(self.language, "sub_header_edit_title"), t(self.language, "sub_header_edit"), icon="/img/edit.gif") ] if self.user.is_admin: l += [ Link("/admin", t(self.language, "sub_header_administration_title"), t(self.language, "sub_header_administration"), icon="/img/admin.gif") ] if self.user.is_workflow_editor: l += [ Link("/publish/", t(self.language, "sub_header_workflow_title"), t(self.language, "sub_header_workflow"), icon="/img/workflow.gif") ] if self.user.can_change_password: l += [ Link("/pwdchange", t(self.language, "sub_header_changepwd_title"), t(self.language, "sub_header_changepwd"), "_parent", icon="/img/changepwd.gif") ] return l
def struct2rss(req, path, params, data, struct, debug=False, singlenode=False, send_children=False): nodelist = struct['nodelist'] language = params.get('lang', 'en') items_list = [] host = u"http://" + unicode(_get_header(req, "HOST") or configured_host) collections = get_collections_node() user = get_guest_user() for n in nodelist: nodename = n.name nodeid = str(n.id) updatetime = utime = try_node_date(n) # categories to be included in all items - mask generated or not default_categories = u'<category>node type: ' + n.type + '/' + n.schema + u'</category>\r\n' # check for export mask for this node try: try: mdt = n.metadatatype except: mdt = None mask = mdt.getMask('rss') if mask.get('masktype') != 'export': mask = None except: mask = None if mask: item_xml = u'<item>\r\n' + mask.getViewHTML( [n], flags=8) + default_categories + u'\r\n</item>\r\n' items_list = items_list + [(updatetime, nodename, nodeid, item_xml) ] continue # no rss export mask: build default item from nodesmall mask item_d = {} browsingPathList = getBrowsingPathList(n) browsingPathList = [ x for x in browsingPathList if x[-1].has_read_access( user=user) and x[-1].is_descendant_of(collections) ] browsingPathList_names = [ map(lambda x: x.name, browsingPath) for browsingPath in browsingPathList ] # assumption: longest path is most detailled and illustrative for being used in the title x = sorted([[len(p), i, p] for i, p in enumerate(browsingPathList_names)]) x.reverse() try: most_detailed_path = x[0][2] except: # browsing path list may be empty (for directories, collections, ...) most_detailed_path = '' item_d['title'] = esc(u"{} ({}, {}/{}) {}".format( nodename or u'-unnamed-node-', nodeid, n.type, n.schema, u"/".join(most_detailed_path))) item_d['item_pubDate'] = utime item_d['guid'] = host + u'/node?id=%s' % nodeid item_d['link'] = host + u'/node?id=%s' % nodeid if mdt: lang_mask = mdt.masks.filter( Node.name.startswith(u"nodesmall")).filter( Node.a.language == language).first() if lang_mask is not None: mask = lang_mask else: mask = mdt.get_mask('nodesmall') else: mask = None if mask is not None: attr_list = mask.getViewHTML( [n], VIEW_DATA_ONLY, language) # [[attr_name, value, label, type], ...] else: attr_list = [ ['', n.id, 'node id', ''], ['', n.name, 'node name', ''], ['', n.type + "/" + n.schema, 'node type', ''], ] description = u'' for x in attr_list: description = description + (u'''<b>%s: </b>%s<br/>\r\n''' % (x[2], x[1])) item_d['description'] = description categories = default_categories for x in browsingPathList_names: categories = categories + u'<category>' + esc( u'/'.join(x)) + u'</category>\r\n' ddcs = n.get('ddc').strip() if ddcs.strip(): ddcs = ddcs.split(';') for ddc in ddcs: categories = categories + u'<category>' + esc( ddc) + u'</category>\r\n' subjects = n.get('subject').strip() if subjects: subjects = subjects.split(';') for subject in subjects: categories = categories + u'<category>' + esc( subject) + u'</category>\r\n' item_d['categories'] = categories for k, v in item_d.items(): item_d[k] = v items_list = items_list + [(updatetime, nodename, nodeid, (template_rss_item % item_d))] if items_list: items_list.sort() items_list.reverse() items = '' for x in items_list: items += (x[3] + u'\r\n') pubDate = lastBuildDate = format_date(format='rfc822') struct['dataready'] = (u"%.3f" % (time.time() - struct['build_response_start'])) fcd = feed_channel_dict.copy() fcd['lang'] = u'de' fcd['pubdate'] = pubDate fcd['lastbuild'] = lastBuildDate fcd['link'] = host fcd['atom_link'] = host + req.fullpath fcd['image_title'] = 'testlogo' fcd['image_link'] = host + u'/img/testlogo.png' fcd['image_url'] = host + u'/img/testlogo.png' if 'feed_info' in params: for k, v in params['feed_info'].items(): fcd[k] = v else: fcd['title'] = host + req.fullpath + req.query fcd['items'] = items s = template_rss_channel % fcd # params['feed_info'] return s.encode("utf8")
def test_get_collections_node(req, collections): cached_collections = get_collections_node() assert cached_collections == collections
def render_page(req, node, content_html, show_navbar=True, show_id=None): """Renders the navigation frame with the inserted content HTML and returns the whole page. """ user = current_user userlinks = UserLinks(user, req) language = lang(req) rootnode = get_collections_node() if node is None: node = rootnode container = rootnode else: container = node.get_container() theme = webconfig.theme front_lang = { "name": config.languages, "actlang": language } frame_context = { "content": Markup(content_html), "id": node.id, "language": front_lang, "show_navbar": show_navbar, } search_html = u"" navtree_html = u"" if show_navbar and not req.args.get("disable_navbar"): if not req.args.get("disable_search"): search_html = render_search_box(container, language, req) if not req.args.get("disable_navtree"): navtree_html = render_navtree(language, node.id, user) ctx_header = { "user_name": user.getName(), "userlinks": userlinks, "header_items": rootnode.getCustomItems("header"), "language": front_lang, "show_language_switcher": (len(front_lang['name']) > 1) } header_html = theme.render_template("frame_header.j2.jade", ctx_header) ctx_footer = { "footer_left_items": rootnode.getCustomItems("footer_left"), "footer_right_items": rootnode.getCustomItems("footer_right") } footer_html = theme.render_template("frame_footer.j2.jade", ctx_footer) frame_context["search"] = Markup(search_html) frame_context["navtree"] = Markup(navtree_html) frame_context["header"] = Markup(header_html) frame_context["footer"] = Markup(footer_html) if show_id and isinstance(show_id, list): show_id = show_id[0] else: show_id = req.args.get("show_id") if show_id: shown_node = q(Node).get(show_id) else: shown_node = node frame_context["google_scholar"] = google_scholar(shown_node) html = theme.render_template("frame.j2.jade", frame_context) return html