| Index: master/webstatus/console.py
|
| diff --git a/master/webstatus/console.py b/master/webstatus/console.py
|
| deleted file mode 100644
|
| index 3b920a937832760ab04746c994d35bdb74d3a9a1..0000000000000000000000000000000000000000
|
| --- a/master/webstatus/console.py
|
| +++ /dev/null
|
| @@ -1,658 +0,0 @@
|
| -# Copyright (c) 2013 The Chromium Authors. All rights reserved.
|
| -# Use of this source code is governed by a BSD-style license that can be
|
| -# found in the LICENSE file.
|
| -
|
| -""" Skia's override of buildbot.status.web.console """
|
| -
|
| -
|
| -from buildbot import util
|
| -from buildbot.changes import changes as changes_module
|
| -from buildbot.status import builder as builder_status
|
| -from buildbot.status.web.base import HtmlResource
|
| -from buildbot.status.web.console import ANYBRANCH, \
|
| - CacheStatus, \
|
| - DevBuild, \
|
| - DevRevision, \
|
| - DoesNotPassFilter, \
|
| - getInProgressResults, \
|
| - getResultsClass, \
|
| - TimeRevisionComparator, \
|
| - IntegerRevisionComparator
|
| -from buildbot.status.web.status_json import JsonResource
|
| -from skia_master_scripts import utils
|
| -from twisted.internet import defer
|
| -
|
| -import builder_name_schema
|
| -import re
|
| -import skia_vars
|
| -import time
|
| -import urllib
|
| -
|
| -
|
| -class ConsoleJsonStatusResource(JsonResource):
|
| - """JSON interface for the console page."""
|
| -
|
| - def __init__(self, status, order_by_time=False):
|
| - JsonResource.__init__(self, status)
|
| -
|
| - self.cache = CacheStatus()
|
| -
|
| - if order_by_time:
|
| - self.comparator = TimeRevisionComparator()
|
| - else:
|
| - self.comparator = IntegerRevisionComparator()
|
| -
|
| - def asDict(self, request):
|
| - cxt = {}
|
| - status = request.site.buildbot_service.getStatus()
|
| -
|
| - # get url parameters
|
| - # Categories to show information for.
|
| - categories = request.args.get("category", [])
|
| - # List of all builders to show on the page.
|
| - builders = request.args.get("builder", [])
|
| - # Repo used to filter the changes shown.
|
| - repository = request.args.get("repository", [None])[0]
|
| - # Branch used to filter the changes shown.
|
| - branch = request.args.get("branch", [ANYBRANCH])[0]
|
| - # List of all the committers name to display on the page.
|
| - dev_name = request.args.get("name", [])
|
| -
|
| - # Debug information to display at the end of the page.
|
| - debug_info = cxt['debuginfo'] = dict()
|
| - debug_info["load_time"] = time.time()
|
| -
|
| - # Keep only the revisions we care about.
|
| - # By default we process the last 40 revisions.
|
| - # If a dev name is passed, we look for the changes by this person in the
|
| - # last 160 revisions.
|
| - num_revs = int(request.args.get("revs", [40])[0])
|
| - if dev_name:
|
| - num_revs *= 4
|
| - num_builds = num_revs
|
| -
|
| - # Get all changes we can find. This is a DB operation, so it must use
|
| - # a deferred.
|
| - d = self.getAllChanges(request, status, debug_info)
|
| - def got_changes(all_changes):
|
| - debug_info["source_all"] = len(all_changes)
|
| -
|
| - rev_filter = {}
|
| - if branch != ANYBRANCH:
|
| - rev_filter['branch'] = branch
|
| - if dev_name:
|
| - rev_filter['who'] = dev_name
|
| - rev_filter['repository'] = skia_vars.GetGlobalVariable('skia_git_url')
|
| - revisions = list(self.filterRevisions(all_changes, max_revs=num_revs,
|
| - rev_filter=rev_filter))
|
| - debug_info["revision_final"] = len(revisions)
|
| -
|
| - # Fetch all the builds for all builders until we get the next build
|
| - # after last_revision.
|
| - builder_list = None
|
| - all_builds = None
|
| - if revisions:
|
| - last_revision = revisions[len(revisions) - 1].revision
|
| - debug_info["last_revision"] = last_revision
|
| -
|
| - (builder_list, all_builds) = self.getAllBuildsForRevision(status,
|
| - request,
|
| - last_revision,
|
| - num_builds,
|
| - categories,
|
| - builders,
|
| - debug_info)
|
| -
|
| - debug_info["added_blocks"] = 0
|
| - debug_info["from_cache"] = 0
|
| -
|
| - if request.args.get("display_cache", None):
|
| - data = ""
|
| - data += "\nGlobal Cache\n"
|
| - data += self.cache.display()
|
| - return data
|
| -
|
| - cxt.update(self.displayPage(request, status, builder_list,
|
| - all_builds, revisions, categories,
|
| - repository, branch, debug_info))
|
| - # Clean up the cache.
|
| - if debug_info["added_blocks"]:
|
| - self.cache.trim()
|
| - return {'builders': cxt['builders'],
|
| - 'revisions': cxt['revisions']}
|
| - d.addCallback(got_changes)
|
| - return d
|
| -
|
| - ##
|
| - ## Data gathering functions
|
| - ##
|
| -
|
| - def getHeadBuild(self, builder):
|
| - """Get the most recent build for the given builder.
|
| - """
|
| - build = builder.getBuild(-1)
|
| -
|
| - # HACK: Work around #601, the head build may be None if it is
|
| - # locked.
|
| - if build is None:
|
| - build = builder.getBuild(-2)
|
| -
|
| - return build
|
| -
|
| - def fetchChangesFromHistory(self, status, max_depth, max_builds, debug_info):
|
| - """Look at the history of the builders and try to fetch as many changes
|
| - as possible. We need this when the main source does not contain enough
|
| - sourcestamps.
|
| -
|
| - max_depth defines how many builds we will parse for a given builder.
|
| - max_builds defines how many builds total we want to parse. This is to
|
| - limit the amount of time we spend in this function.
|
| -
|
| - This function is sub-optimal, but the information returned by this
|
| - function is cached, so this function won't be called more than once.
|
| - """
|
| -
|
| - all_changes = list()
|
| - build_count = 0
|
| - for builder_name in status.getBuilderNames()[:]:
|
| - if build_count > max_builds:
|
| - break
|
| -
|
| - builder = status.getBuilder(builder_name)
|
| - build = self.getHeadBuild(builder)
|
| - depth = 0
|
| - while build and depth < max_depth and build_count < max_builds:
|
| - depth += 1
|
| - build_count += 1
|
| - sourcestamp = build.getSourceStamp()
|
| - all_changes.extend(sourcestamp.changes[:])
|
| - build = build.getPreviousBuild()
|
| -
|
| - debug_info["source_fetch_len"] = len(all_changes)
|
| - return all_changes
|
| -
|
| - @defer.deferredGenerator
|
| - def getAllChanges(self, request, status, debug_info):
|
| - master = request.site.buildbot_service.master
|
| - max_rev_limit = skia_vars.GetGlobalVariable('console_max_rev_limit')
|
| - default_rev_limit = skia_vars.GetGlobalVariable('console_default_rev_limit')
|
| - limit = min(max_rev_limit,
|
| - max(1, int(request.args.get('limit', [default_rev_limit])[0])))
|
| - wfd = defer.waitForDeferred(master.db.changes.getRecentChanges(limit))
|
| - yield wfd
|
| - chdicts = wfd.getResult()
|
| -
|
| - # convert those to Change instances
|
| - wfd = defer.waitForDeferred(
|
| - defer.gatherResults([
|
| - changes_module.Change.fromChdict(master, chdict)
|
| - for chdict in chdicts ]))
|
| - yield wfd
|
| - all_changes = wfd.getResult()
|
| -
|
| - all_changes.sort(key=self.comparator.getSortingKey())
|
| -
|
| - # Remove the dups
|
| - prev_change = None
|
| - new_changes = []
|
| - for change in all_changes:
|
| - rev = change.revision
|
| - if not prev_change or rev != prev_change.revision:
|
| - new_changes.append(change)
|
| - prev_change = change
|
| - all_changes = new_changes
|
| -
|
| - debug_info["source_len"] = len(all_changes)
|
| - yield all_changes
|
| -
|
| - def getBuildDetails(self, request, builder_name, build):
|
| - """Returns an HTML list of failures for a given build."""
|
| - details = {}
|
| - if not build.getLogs():
|
| - return details
|
| -
|
| - for step in build.getSteps():
|
| - (result, reason) = step.getResults()
|
| - if result == builder_status.FAILURE:
|
| - name = step.getName()
|
| -
|
| - # Remove html tags from the error text.
|
| - strip_html = re.compile(r'<.*?>')
|
| - stripped_details = strip_html.sub('', ' '.join(step.getText()))
|
| -
|
| - details['buildername'] = builder_name
|
| - details['status'] = stripped_details
|
| - details['reason'] = reason
|
| - logs = details['logs'] = []
|
| -
|
| - if step.getLogs():
|
| - for log in step.getLogs():
|
| - logname = log.getName()
|
| - logurl = request.childLink(
|
| - "../builders/%s/builds/%s/steps/%s/logs/%s" %
|
| - (urllib.quote(builder_name),
|
| - build.getNumber(),
|
| - urllib.quote(name),
|
| - urllib.quote(logname)))
|
| - logs.append(dict(url=logurl, name=logname))
|
| - return details
|
| -
|
| - def getBuildsForRevision(self, request, builder, builder_name, last_revision,
|
| - num_builds, debug_info):
|
| - """Return the list of all the builds for a given builder that we will
|
| - need to be able to display the console page. We start by the most recent
|
| - build, and we go down until we find a build that was built prior to the
|
| - last change we are interested in."""
|
| -
|
| - revision = last_revision
|
| -
|
| - builds = []
|
| - build = self.getHeadBuild(builder)
|
| - number = 0
|
| - while build and number < num_builds:
|
| - debug_info["builds_scanned"] += 1
|
| - number += 1
|
| -
|
| - # Get the last revision in this build.
|
| - # We first try "got_revision", but if it does not work, then
|
| - # we try "revision".
|
| - got_rev = -1
|
| - try:
|
| - got_rev = build.getProperty("got_revision")
|
| - if not self.comparator.isValidRevision(got_rev):
|
| - got_rev = -1
|
| - except KeyError:
|
| - pass
|
| -
|
| - try:
|
| - if got_rev == -1:
|
| - got_rev = build.getProperty("revision")
|
| - if not self.comparator.isValidRevision(got_rev):
|
| - got_rev = -1
|
| - except Exception:
|
| - pass
|
| -
|
| - # We ignore all builds that don't have last revisions.
|
| - # TODO(nsylvain): If the build is over, maybe it was a problem
|
| - # with the update source step. We need to find a way to tell the
|
| - # user that his change might have broken the source update.
|
| - if got_rev and got_rev != -1:
|
| - dev_revision = self.getChangeForBuild(build, got_rev)
|
| - details = self.getBuildDetails(request, builder_name, build)
|
| - dev_build = DevBuild(dev_revision, build, details,
|
| - getInProgressResults(build))
|
| - builds.append(dev_build)
|
| -
|
| - # Now break if we have enough builds.
|
| - current_revision = self.getChangeForBuild(build, revision)
|
| - if self.comparator.isRevisionEarlier(dev_build, current_revision):
|
| - break
|
| -
|
| - build = build.getPreviousBuild()
|
| -
|
| - return builds
|
| -
|
| - def getChangeForBuild(self, build, revision):
|
| - if not build or not build.getChanges(): # Forced build
|
| - return DevBuild(revision, build, None)
|
| -
|
| - for change in build.getChanges():
|
| - if change.revision == revision:
|
| - return change
|
| -
|
| - # No matching change, return the last change in build.
|
| - changes = list(build.getChanges())
|
| - changes.sort(key=self.comparator.getSortingKey())
|
| - return changes[-1]
|
| -
|
| - def getAllBuildsForRevision(self, status, request, last_revision, num_builds,
|
| - categories, builders, debug_info):
|
| - """Returns a dictionary of builds we need to inspect to be able to
|
| - display the console page. The key is the builder name, and the value is
|
| - an array of build we care about. We also returns a dictionary of
|
| - builders we care about. The key is it's category.
|
| -
|
| - last_revision is the last revision we want to display in the page.
|
| - categories is a list of categories to display. It is coming from the
|
| - HTTP GET parameters.
|
| - builders is a list of builders to display. It is coming from the HTTP
|
| - GET parameters.
|
| - """
|
| -
|
| - all_builds = dict()
|
| -
|
| - # List of all builders in the dictionary.
|
| - builder_list = dict()
|
| -
|
| - debug_info["builds_scanned"] = 0
|
| - # Get all the builders.
|
| - builder_names = status.getBuilderNames()[:]
|
| - for builder_name in builder_names:
|
| - builder = status.getBuilder(builder_name)
|
| -
|
| - # Make sure we are interested in this builder.
|
| - if categories and builder.category not in categories:
|
| - continue
|
| - if builders and builder_name not in builders:
|
| - continue
|
| - if builder_name_schema.IsTrybot(builder_name):
|
| - continue
|
| -
|
| - # We want to display this builder.
|
| - category_full = builder.category or 'default'
|
| -
|
| - category_parts = category_full.split('|')
|
| - category = category_parts[0]
|
| - if len(category_parts) > 1:
|
| - subcategory = category_parts[1]
|
| - else:
|
| - subcategory = 'default'
|
| - if not builder_list.get(category):
|
| - builder_list[category] = {}
|
| - if not builder_list[category].get(subcategory):
|
| - builder_list[category][subcategory] = {}
|
| - if not builder_list[category][subcategory].get(category_full):
|
| - builder_list[category][subcategory][category_full] = []
|
| -
|
| - b = {}
|
| - b["color"] = "notstarted"
|
| - b["pageTitle"] = builder_name
|
| - b["url"] = "./builders/%s" % urllib.quote(builder_name, safe='() ')
|
| - b["builderName"] = builder_name
|
| - state, _ = status.getBuilder(builder_name).getState()
|
| - # Check if it's offline, if so, the box is purple.
|
| - if state == "offline":
|
| - b["color"] = "offline"
|
| - else:
|
| - # If not offline, then display the result of the last
|
| - # finished build.
|
| - build = self.getHeadBuild(status.getBuilder(builder_name))
|
| - while build and not build.isFinished():
|
| - build = build.getPreviousBuild()
|
| -
|
| - if build:
|
| - b["color"] = getResultsClass(build.getResults(), None, False)
|
| -
|
| - # Append this builder to the dictionary of builders.
|
| - builder_list[category][subcategory][category_full].append(b)
|
| - # Set the list of builds for this builder.
|
| - all_builds[builder_name] = self.getBuildsForRevision(request,
|
| - builder,
|
| - builder_name,
|
| - last_revision,
|
| - num_builds,
|
| - debug_info)
|
| -
|
| - return (builder_list, all_builds)
|
| -
|
| -
|
| - ##
|
| - ## Display functions
|
| - ##
|
| -
|
| - def displayStatusLine(self, builder_list, all_builds, revision, debug_info):
|
| - """Display the boxes that represent the status of each builder in the
|
| - first build "revision" was in. Returns an HTML list of errors that
|
| - happened during these builds."""
|
| -
|
| - details = []
|
| - builds = {}
|
| -
|
| - # Display the boxes by category group.
|
| - for category in builder_list:
|
| - for subcategory in builder_list[category]:
|
| - for category_full in builder_list[category][subcategory]:
|
| - for builder in builder_list[category][subcategory][category_full]:
|
| - builder_name = builder['builderName']
|
| - builds[builder_name] = []
|
| - introduced_in = None
|
| - first_not_in = None
|
| -
|
| - cached_value = self.cache.get(builder_name, revision.revision)
|
| - if cached_value:
|
| - debug_info["from_cache"] += 1
|
| -
|
| - b = {}
|
| - b["url"] = cached_value.url
|
| - b["pageTitle"] = cached_value.pageTitle
|
| - b["color"] = cached_value.color
|
| - b["tag"] = cached_value.tag
|
| - b["builderName"] = cached_value.builderName
|
| -
|
| - builds[builder_name].append(b)
|
| -
|
| - if cached_value.details and cached_value.color == "failure":
|
| - details.append(cached_value.details)
|
| -
|
| - continue
|
| -
|
| - # Find the first build that does not include the revision.
|
| - for build in all_builds[builder_name]:
|
| - if self.comparator.isRevisionEarlier(build.revision, revision):
|
| - first_not_in = build
|
| - break
|
| - else:
|
| - introduced_in = build
|
| -
|
| - # Get the results of the first build with the revision, and the
|
| - # first build that does not include the revision.
|
| - results = None
|
| - in_progress_results = None
|
| - previous_results = None
|
| - if introduced_in:
|
| - results = introduced_in.results
|
| - in_progress_results = introduced_in.inProgressResults
|
| - if first_not_in:
|
| - previous_results = first_not_in.results
|
| -
|
| - is_running = False
|
| - if introduced_in and not introduced_in.isFinished:
|
| - is_running = True
|
| -
|
| - url = "./waterfall"
|
| - page_title = builder_name
|
| - tag = ""
|
| - current_details = {}
|
| - if introduced_in:
|
| - current_details = introduced_in.details or ""
|
| - url = "./buildstatus?builder=%s&number=%s" % (
|
| - urllib.quote(builder_name), introduced_in.number)
|
| - page_title += " "
|
| - page_title += urllib.quote(' '.join(introduced_in.text),
|
| - ' \n\\/:')
|
| -
|
| - builder_strip = builder_name.replace(' ', '')
|
| - builder_strip = builder_strip.replace('(', '')
|
| - builder_strip = builder_strip.replace(')', '')
|
| - builder_strip = builder_strip.replace('.', '')
|
| - tag = "Tag%s%s" % (builder_strip, introduced_in.number)
|
| -
|
| - if is_running:
|
| - page_title += ' ETA: %ds' % (introduced_in.eta or 0)
|
| -
|
| - results_class = getResultsClass(results, previous_results,
|
| - is_running, in_progress_results)
|
| -
|
| - b = {}
|
| - b["url"] = url
|
| - b["pageTitle"] = page_title
|
| - b["color"] = results_class
|
| - b["tag"] = tag
|
| - b["builderName"] = builder_name
|
| -
|
| - builds[builder_name].append(b)
|
| -
|
| - # If the box is red, we add the explaination in the details
|
| - # section.
|
| - if current_details and results_class == "failure":
|
| - details.append(current_details)
|
| -
|
| - # Add this box to the cache if it's completed so we don't have
|
| - # to compute it again.
|
| - if results_class not in ("running", "running_failure",
|
| - "notstarted"):
|
| - debug_info["added_blocks"] += 1
|
| - self.cache.insert(builder_name, revision.revision, results_class,
|
| - page_title, current_details, url, tag)
|
| -
|
| - return (builds, details)
|
| -
|
| - def filterRevisions(self, revisions, rev_filter=None, max_revs=None):
|
| - """Filter a set of revisions based on any number of filter criteria.
|
| - If specified, rev_filter should be a dict with keys corresponding to
|
| - revision attributes, and values of 1+ strings"""
|
| - if not rev_filter:
|
| - if max_revs is None:
|
| - for rev in reversed(revisions):
|
| - yield DevRevision(rev)
|
| - else:
|
| - for index, rev in enumerate(reversed(revisions)):
|
| - if index >= max_revs:
|
| - break
|
| - yield DevRevision(rev)
|
| - else:
|
| - num_revs = 0
|
| - for rev in reversed(revisions):
|
| - if max_revs and num_revs >= max_revs:
|
| - break
|
| - try:
|
| - for field, acceptable in rev_filter.iteritems():
|
| - if not hasattr(rev, field):
|
| - raise DoesNotPassFilter
|
| - if type(acceptable) in (str, unicode):
|
| - if getattr(rev, field) != acceptable:
|
| - raise DoesNotPassFilter
|
| - elif type(acceptable) in (list, tuple, set):
|
| - if getattr(rev, field) not in acceptable:
|
| - raise DoesNotPassFilter
|
| - num_revs += 1
|
| - yield DevRevision(rev)
|
| - except DoesNotPassFilter:
|
| - pass
|
| -
|
| - def displayPage(self, request, status, builder_list, all_builds, revisions,
|
| - categories, repository, branch, debug_info):
|
| - """Display the console page."""
|
| - # Build the main template directory with all the informations we have.
|
| - subs = dict()
|
| - subs["branch"] = branch or 'trunk'
|
| - subs["repository"] = repository
|
| - if categories:
|
| - subs["categories"] = ' '.join(categories)
|
| - subs["time"] = time.strftime("%a %d %b %Y %H:%M:%S",
|
| - time.localtime(util.now()))
|
| - subs["debugInfo"] = debug_info
|
| - subs["ANYBRANCH"] = ANYBRANCH
|
| -
|
| - if builder_list:
|
| - builders = builder_list
|
| - else:
|
| - builders = {}
|
| - subs['builders'] = builders
|
| - subs['revisions'] = []
|
| -
|
| - # For each revision we show one line
|
| - for revision in revisions:
|
| - r = {}
|
| -
|
| - # Fill the dictionary with this new information
|
| - r['id'] = revision.revision
|
| - r['link'] = revision.revlink
|
| - if (skia_vars.GetGlobalVariable('commit_bot_username') in revision.who
|
| - and 'Author: ' in revision.comments):
|
| - who = revision.comments.split('Author: ')[1].split('\n')[0]
|
| - who += ' (commit-bot)'
|
| - else:
|
| - who = revision.who
|
| - r['who'] = utils.FixGitSvnEmail(who)
|
| - r['date'] = revision.date
|
| - r['comments'] = revision.comments
|
| - r['repository'] = revision.repository
|
| - r['project'] = revision.project
|
| -
|
| - # Display the status for all builders.
|
| - (builds, details) = self.displayStatusLine(builder_list,
|
| - all_builds,
|
| - revision,
|
| - debug_info)
|
| - r['builds'] = builds
|
| - r['details'] = details
|
| -
|
| - # Calculate the td span for the comment and the details.
|
| - r["span"] = sum ([len(builder_list[category]) \
|
| - for category in builder_list]) + 2
|
| -
|
| - subs['revisions'].append(r)
|
| -
|
| - #
|
| - # Display the footer of the page.
|
| - #
|
| - debug_info["load_time"] = time.time() - debug_info["load_time"]
|
| - return subs
|
| -
|
| -
|
| -class ConsoleStatusResource(HtmlResource):
|
| - """Main console class. It displays a user-oriented status page.
|
| - Every change is a line in the page, and it shows the result of the first
|
| - build with this change for each slave."""
|
| -
|
| - def getPageTitle(self, request):
|
| - status = self.getStatus(request)
|
| - title = status.getTitle()
|
| - if title:
|
| - return "BuildBot: %s" % title
|
| - else:
|
| - return "BuildBot"
|
| -
|
| - def getChangeManager(self, request):
|
| - return request.site.buildbot_service.parent.change_svc
|
| -
|
| - def content(self, request, cxt):
|
| - "This method builds the main console view display."
|
| -
|
| - reload_time = None
|
| - # Check if there was an arg. Don't let people reload faster than
|
| - # every 15 seconds. 0 means no reload.
|
| - if "reload" in request.args:
|
| - try:
|
| - reload_time = int(request.args["reload"][0])
|
| - if reload_time != 0:
|
| - reload_time = max(reload_time, 15)
|
| - except ValueError:
|
| - pass
|
| -
|
| - request.setHeader('Cache-Control', 'no-cache')
|
| -
|
| - # Sets the default reload time to 60 seconds.
|
| - if not reload_time:
|
| - reload_time = skia_vars.GetGlobalVariable('default_webstatus_refresh')
|
| -
|
| - # Append the tag to refresh the page.
|
| - if reload_time is not None and reload_time != 0:
|
| - cxt['refresh'] = reload_time
|
| -
|
| - # List of categories for which we load information but hide initially.
|
| - hidden_categories_sets = request.args.get("hideCategories", [])
|
| - hide_categories = []
|
| - for category_set in hidden_categories_sets:
|
| - hide_categories.extend(category_set.split(','))
|
| - cxt['hide_categories'] = hide_categories
|
| -
|
| - # List of subcategories for which we load information but hide initially.
|
| - hidden_subcategories_sets = request.args.get("hideSubcategories", [])
|
| - hide_subcategories = []
|
| - for subcategory_set in hidden_subcategories_sets:
|
| - hide_subcategories.extend(subcategory_set.split(','))
|
| - cxt['hide_subcategories'] = hide_subcategories
|
| -
|
| - # Console event-loading limits.
|
| - cxt['default_console_limit'] = \
|
| - skia_vars.GetGlobalVariable('console_default_rev_limit')
|
| - cxt['max_console_limit'] = \
|
| - skia_vars.GetGlobalVariable('console_max_rev_limit')
|
| -
|
| - templates = request.site.buildbot_service.templates
|
| - template = templates.get_template("console.html")
|
| - data = template.render(cxt)
|
| -
|
| - return data
|
|
|