diff options
-rw-r--r-- | lib/toaster/toastergui/templates/basetable_top.html | 2 | ||||
-rw-r--r-- | lib/toaster/toastergui/templates/basetable_top_buildprojects.html | 16 | ||||
-rw-r--r-- | lib/toaster/toastergui/templates/basetable_top_projectbuilds.html | 16 | ||||
-rw-r--r-- | lib/toaster/toastergui/templates/build.html | 78 | ||||
-rw-r--r-- | lib/toaster/toastergui/templates/landing.html | 66 | ||||
-rw-r--r-- | lib/toaster/toastergui/templates/mrb_section.html | 102 | ||||
-rw-r--r-- | lib/toaster/toastergui/templates/projects.html | 36 | ||||
-rw-r--r-- | lib/toaster/toastergui/templatetags/projecttags.py | 11 | ||||
-rw-r--r-- | lib/toaster/toastergui/urls.py | 5 | ||||
-rwxr-xr-x | lib/toaster/toastergui/views.py | 130 |
10 files changed, 384 insertions, 78 deletions
diff --git a/lib/toaster/toastergui/templates/basetable_top.html b/lib/toaster/toastergui/templates/basetable_top.html index e3f6a4ee2..92a3b5080 100644 --- a/lib/toaster/toastergui/templates/basetable_top.html +++ b/lib/toaster/toastergui/templates/basetable_top.html @@ -175,8 +175,8 @@ <button class="btn" type="submit" value="Search">Search</button> </form> <div class="pull-right"> + {% block custombuttons%} {% endblock %} {% if tablecols %} - {% block custombuttons%} {% endblock %} <div class="btn-group"> <button class="btn dropdown-toggle" data-toggle="dropdown">Edit columns <span class="caret"></span> diff --git a/lib/toaster/toastergui/templates/basetable_top_buildprojects.html b/lib/toaster/toastergui/templates/basetable_top_buildprojects.html new file mode 100644 index 000000000..d51717959 --- /dev/null +++ b/lib/toaster/toastergui/templates/basetable_top_buildprojects.html @@ -0,0 +1,16 @@ +{% extends "basetable_top.html" %} + +{%block custombuttons %} +{% if MANAGED %} + <div class="btn-group builds-projects"> + <button class="btn dropdown-toggle" data-toggle="dropdown"> + <span class="selection">Show all builds</span> + <i class="icon-caret-down"></i> + </button> + <ul class="dropdown-menu"> + <li><a href="{% url 'all-builds'%}">Show all builds</a></li> + <li><a href="{% url 'all-projects'%}">Show all projects</a></li> + </ul> + </div> +{% endif %} +{%endblock%} diff --git a/lib/toaster/toastergui/templates/basetable_top_projectbuilds.html b/lib/toaster/toastergui/templates/basetable_top_projectbuilds.html new file mode 100644 index 000000000..bfefff5e3 --- /dev/null +++ b/lib/toaster/toastergui/templates/basetable_top_projectbuilds.html @@ -0,0 +1,16 @@ +{% extends "basetable_top.html" %} + +{%block custombuttons %} +{% if MANAGED %} + <div class="btn-group builds-projects"> + <button class="btn dropdown-toggle" data-toggle="dropdown"> + <span class="selection">Show all projects</span> + <i class="icon-caret-down"></i> + </button> + <ul class="dropdown-menu"> + <li><a href="{% url 'all-builds'%}">Show all builds</a></li> + <li><a href="{% url 'all-projects'%}">Show all projects</a></li> + </ul> + </div> +{% endif %} +{%endblock%} diff --git a/lib/toaster/toastergui/templates/build.html b/lib/toaster/toastergui/templates/build.html index bef1f1539..f20f04174 100644 --- a/lib/toaster/toastergui/templates/build.html +++ b/lib/toaster/toastergui/templates/build.html @@ -6,83 +6,11 @@ {% block pagecontent %} <div class="row-fluid"> - {% if not objects.paginator.count and not request.GET.filter and not request.GET.search %} - <!-- Empty - no data in database --> - <div class="hero-unit span12"> - <button type="button" class="close" data-dismiss="alert">×</button> - <div class="row-fluid"> - <div class="span6"> - <h1>This is Toaster</h1> - <p>A web interface to <a href="http://www.yoctoproject.org/tools-resources/projects/bitbake">BitBake</a>, the <a href="http://www.yoctoproject.org">Yocto Project</a> build system.</p> - <p class="hero-actions"> - <a class="btn btn-primary btn-large" href="https://www.yoctoproject.org/documentation/toaster-manual">Show me the manual</a> - <a class="btn btn-large" href="https://wiki.yoctoproject.org/wiki/Contribute_to_Toaster">I want to contribute</a> - </p> - </div> - <div class="span5"> - <a href="http://www.yoctoproject.org"><img src="{% static 'img/toaster.png' %}" class="thumbnail" alt="Yocto Project"/> </a> - </div> - </div> - </div> - {% endif %} - {%if mru.count > 0%} - <div class="page-header top-air"> - <h1> - Recent Builds - </h1> - </div> - {% for build in mru %} - <div class="alert {%if build.outcome == build.SUCCEEDED%}alert-success{%elif build.outcome == build.FAILED%}alert-error{%else%}alert-info{%endif%}"> - <div class="row-fluid"> - <div class="lead span5"> - {%if build.outcome == build.SUCCEEDED%}<i class="icon-ok-sign success"></i>{%elif build.outcome == build.FAILED%}<i class="icon-minus-sign error"></i>{%else%}{%endif%} - {%if build.outcome == build.SUCCEEDED or build.outcome == build.FAILED %} - <a href="{%url 'builddashboard' build.pk%}" class="{%if build.outcome == build.SUCCEEDED %}success{%else%}error{%endif%}"> - {% endif %} - <span data-toggle="tooltip" {%if build.target_set.all.count > 1%}title="Targets: {%for target in build.target_set.all%}{{target.target}} {%endfor%}"{%endif%}>{{build.target_set.all.0.target}} {%if build.target_set.all.count > 1%}(+ {{build.target_set.all.count|add:"-1"}}){%endif%} {{build.machine}} ({{build.completed_on|naturaltime}})</span> - {%if build.outcome == build.SUCCEEDED or build.outcome == build.FAILED %} - </a> - {% endif %} - </div> - {%if build.outcome == build.SUCCEEDED or build.outcome == build.FAILED %} - <div class="span2 lead"> - {% if build.errors_no %} - <i class="icon-minus-sign red"></i> <a href="{%url 'builddashboard' build.pk%}#errors" class="error">{{build.errors_no}} error{{build.errors_no|pluralize}}</a> - {% endif %} - </div> - <div class="span2 lead"> - {% if build.warnings_no %} - <i class="icon-warning-sign yellow"></i> <a href="{%url 'builddashboard' build.pk%}#warnings" class="warning">{{build.warnings_no}} warning{{build.warnings_no|pluralize}}</a> - {% endif %} - </div> - <div class="lead pull-right"> - Build time: <a href="{% url 'buildtime' build.pk %}">{{ build.timespent|sectohms }}</a> - </div> - {%endif%}{%if build.outcome == build.IN_PROGRESS %} - <div class="span4"> - <div class="progress" style="margin-top:5px;" data-toggle="tooltip" title="{{build.completeper}}% of tasks complete"> - <div style="width: {{build.completeper}}%;" class="bar"></div> - </div> - </div> - <div class="lead pull-right">ETA: in {{build.eta|naturaltime}}</div> - {%endif%} - </div> - </div> + {% include "mrb_section.html" %} - {% endfor %}{%endif%} - {% if not objects.paginator.count and not request.GET.filter and not request.GET.search %} - <!-- Empty - no data in database --> - {% if mru.count == 0 %} - <div class="page-header top-air"> - <h1>All builds</h1> - </div> - <div class="alert alert-info lead"> - Toaster has not recorded any builds yet. Go build something with <a href="http://www.yoctoproject.org/docs/current/yocto-project-qs/yocto-project-qs.html#test-run">Knotty</a> or <a href="https://www.yoctoproject.org/documentation/hob-manual">Hob</a> - </div> - {% endif %} - {% else %} + {% if 1 %} <div class="page-header top-air"> <h1> {% if request.GET.filter and objects.paginator.count > 0 or request.GET.search and objects.paginator.count > 0 %} @@ -108,7 +36,7 @@ {% else %} - {% include "basetable_top.html" %} + {% include "basetable_top_buildprojects.html" %} <!-- Table data rows; the order needs to match the order of "tablecols" definitions; and the <td class value needs to match the tablecols clclass value for show/hide buttons to work --> {% for build in objects %} <tr class="data"> diff --git a/lib/toaster/toastergui/templates/landing.html b/lib/toaster/toastergui/templates/landing.html new file mode 100644 index 000000000..071edf86e --- /dev/null +++ b/lib/toaster/toastergui/templates/landing.html @@ -0,0 +1,66 @@ +{% extends "base.html" %} + +{% load static %} +{% load projecttags %} +{% load humanize %} + +{% block pagecontent %} + + <div class="container-fluid"> + <div class="row-fluid"> + <!-- Empty - no data in database --> + <div class="hero-unit span12"> + <button class="close" data-dismiss="alert" type="button"> + × + </button> + <div class="row-fluid"> + <div class="span6"> + <h1> + This is Toaster + </h1> + <p> + A web interface to + <a href="http://www.yoctoproject.org/tools-resources/projects/bitbake"> + BitBake + </a> + , the + <a href="http://www.yoctoproject.org"> + Yocto Project + </a> + build system. + </p> + <p class="hero-actions"> + <a class="btn btn-primary btn-large" href="https://www.yoctoproject.org/documentation/toaster-manual"> + Show me the manual + </a> + <a class="btn btn-large" href="https://wiki.yoctoproject.org/wiki/Contribute_to_Toaster"> + I want to contribute + </a> + </p> + </div> + <div class="span5"> + <a href="http://www.yoctoproject.org"> + <img alt="Yocto Project" class="thumbnail" src="/static/img/toaster.png"/> + </a> + </div> + </div> + </div> + <!-- Empty - no data in database --> + <div class="page-header top-air"> + <h1> + All builds + </h1> + </div> + <div class="alert alert-info lead"> + Toaster has not recorded any builds yet. Go build something with + <a href="http://www.yoctoproject.org/docs/current/yocto-project-qs/yocto-project-qs.html#test-run"> + Knotty + </a> + or + <a href="https://www.yoctoproject.org/documentation/hob-manual"> + Hob + </a> + </div> + </div> + +{% endblock %} diff --git a/lib/toaster/toastergui/templates/mrb_section.html b/lib/toaster/toastergui/templates/mrb_section.html new file mode 100644 index 000000000..5ba0b0849 --- /dev/null +++ b/lib/toaster/toastergui/templates/mrb_section.html @@ -0,0 +1,102 @@ +{% load static %} +{% load projecttags %} +{% load humanize %} + + +{%if mru.count > 0%} + + <div class="page-header top-air"> + <h1> + Latest Builds + </h1> + </div> + <div id="latest-builds"> + {% for build in mru %} + <div class="alert {%if build.outcome == build.SUCCEEDED%}alert-success{%elif build.outcome == build.FAILED%}alert-error{%else%}alert-info{%endif%}" style="padding-top: 0;"> + {% if build.project %} + <span class="label {%if build.outcome == build.SUCCEEDED%}label-success{%elif build.outcome == build.FAILED%}label-danger{%else%}label-info{%endif%}" style="font-weight: normal; margin-bottom: 5px; margin-left:-15px; padding-top:5px;"> {{build.project.name}} </span> + {% endif %} + + <div class="row-fluid"> + <div class="span4 lead"> + {%if build.outcome == build.SUCCEEDED%}<i class="icon-ok-sign success"></i>{%elif build.outcome == build.FAILED%}<i class="icon-minus-sign error"></i>{%else%}{%endif%} + {%if build.outcome == build.SUCCEEDED or build.outcome == build.FAILED %} + <a href="{%url 'builddashboard' build.pk%}" class="{%if build.outcome == build.SUCCEEDED %}success{%else%}error{%endif%}"> + {% endif %} + <span data-toggle="tooltip" {%if build.target_set.all.count > 1%}title="Targets: {%for target in build.target_set.all%}{{target.target}} {%endfor%}"{%endif%}>{{build.target_set.all.0.target}} {%if build.target_set.all.count > 1%}(+ {{build.target_set.all.count|add:"-1"}}){%endif%} {{build.machine}} ({{build.completed_on|naturaltime}})</span> + {%if build.outcome == build.SUCCEEDED or build.outcome == build.FAILED %} + </a> + {% endif %} + </div> + {%if build.outcome == build.SUCCEEDED or build.outcome == build.FAILED %} + <div class="span2 lead"> + {% if build.errors_no %} + <i class="icon-minus-sign red"></i> <a href="{%url 'builddashboard' build.pk%}#errors" class="error">{{build.errors_no}} error{{build.errors_no|pluralize}}</a> + {% endif %} + </div> + <div class="span2 lead"> + {% if build.warnings_no %} + <i class="icon-warning-sign yellow"></i> <a href="{%url 'builddashboard' build.pk%}#warnings" class="warning">{{build.warnings_no}} warning{{build.warnings_no|pluralize}}</a> + {% endif %} + </div> + <div class="lead "> + <span class="lead{%if not build.project%} pull-right{%endif%}"> + Build time: <a href="{% url 'buildtime' build.pk %}">{{ build.timespent|sectohms }}</a> + </span> + {% if build.project %} + <a class="btn {%if build.outcome == build.SUCCEEDED%}btn-success{%elif build.outcome == build.FAILED%}btn-danger{%else%}btn-info{%endif%} pull-right" onclick="scheduleBuild({{build.project.name|json}}, {{build.get_sorted_target_list|mapselect:'target'|json}})">Run again</a> + {% endif %} + </div> + {%endif%} + {%if build.outcome == build.IN_PROGRESS %} + <div class="span4"> + <div class="progress" style="margin-top:5px;" data-toggle="tooltip" title="{{build.completeper}}% of tasks complete"> + <div style="width: {{build.completeper}}%;" class="bar"></div> + </div> + </div> + <div class="lead pull-right">ETA: in {{build.eta|naturaltime}}</div> + {%endif%} + </div> + </div> + + {% endfor %} + </div> + +<script> + +function _makeXHRBuildCall(data, onsuccess, onfail) { + $.ajax( { + type: "POST", + url: "{% url 'xhr_projectbuild' project.id %}", + data: data, + headers: { 'X-CSRFToken' : $.cookie('csrftoken')}, + success: function (_data) { + if (_data.error != "ok") { + alert(_data.error); + } else { + if (onsuccess != undefined) onsuccess(_data); + } + }, + error: function (_data) { + alert("Call failed"); + console.log(_data); + if (onfail) onfail(data); + } + }); +} + + +function scheduleBuild(projectName, buildlist) { + console.log("scheduleBuild"); +// _makeXHRBuildCall({targets: buildlist.join(" ")}, function (_data) { + + $('#latest-builds').prepend('<div class="alert alert-info" style="padding-top:0px">' + '<span class="label label-info" style="font-weight: normal; margin-bottom: 5px; margin-left:-15px; padding-top:5px;">'+projectName+'</span><div class="row-fluid">' + + '<div class="span4 lead">' + buildlist.join(" ") + + '</div><div class="span4 lead pull-right">Build queued. Your build will start shortly.</div></div></div>'); +// } +} + +</script> + +{%endif%} + diff --git a/lib/toaster/toastergui/templates/projects.html b/lib/toaster/toastergui/templates/projects.html new file mode 100644 index 000000000..432f025d3 --- /dev/null +++ b/lib/toaster/toastergui/templates/projects.html @@ -0,0 +1,36 @@ +{% extends "base.html" %} + +{% load static %} +{% load projecttags %} +{% load humanize %} + +{% block pagecontent %} + + + {% include "mrb_section.html" %} + + + <div class="page-header top-air"> + <h1> + All projects + </h1> + </div> + +{% include "basetable_top_projectbuilds.html" %} + {% for o in objects %} + <tr class="data"> + <td><a href="{% url 'project' o.id %}">{{o.name}}</a></td> + <td><a href="{% url 'project' o.id %}">{{o.release.name}}</a></td> + <td>{{o.get_current_machine_name}}</td> + <td>{{o.get_number_of_builds}}</td> + <td>{{o.get_last_outcome}}</td> + <td>{{o.get_last_target}}</td> + <td>{{o.get_last_errors}}</td> + <td>{{o.get_last_warnings}}</td> + <td>{{o.get_last_imgfiles}}</td> + <td>{{o.updated|date:"d/m/y H:i"}}</td> + </tr> + {% endfor %} +{% include "basetable_bottom.html" %} + +{% endblock %} diff --git a/lib/toaster/toastergui/templatetags/projecttags.py b/lib/toaster/toastergui/templatetags/projecttags.py index b953aa158..4a97eb7ac 100644 --- a/lib/toaster/toastergui/templatetags/projecttags.py +++ b/lib/toaster/toastergui/templatetags/projecttags.py @@ -24,6 +24,7 @@ import re from django import template from django.utils import timezone from django.template.defaultfilters import filesizeformat +import json as JsonLib register = template.Library() @@ -40,6 +41,16 @@ def sectohms(time): hours = int(tdsec / 3600) return "%02d:%02d:%02d" % (hours, int((tdsec - (hours * 3600))/ 60), int(tdsec) % 60) + +@register.filter(name = 'mapselect') +def mapselect(value, argument): + return map(lambda x: vars(x)[argument], value) + + +@register.filter(name = "json") +def json(value): + return JsonLib.dumps(value) + @register.assignment_tag def query(qs, **kwargs): """ template tag which allows queryset filtering. Usage: diff --git a/lib/toaster/toastergui/urls.py b/lib/toaster/toastergui/urls.py index e642e3203..07821b603 100644 --- a/lib/toaster/toastergui/urls.py +++ b/lib/toaster/toastergui/urls.py @@ -21,6 +21,8 @@ from django.views.generic import RedirectView urlpatterns = patterns('toastergui.views', # landing page + url(r'^landing/$', 'landing', name='landing'), + url(r'^builds/$', 'builds', name='all-builds'), # build info navigation url(r'^build/(?P<build_id>\d+)$', 'builddashboard', name="builddashboard"), @@ -74,6 +76,7 @@ urlpatterns = patterns('toastergui.views', url(r'^targets/$', 'targets', name='targets'), url(r'^machines/$', 'machines', name='machines'), + url(r'^projects/$', 'projects', name='all-projects'), url(r'^project/(?P<pid>\d+)/$', 'project', name='project'), url(r'^project/(?P<pid>\d+)/configuration$', 'projectconf', name='projectconf'), url(r'^project/(?P<pid>\d+)/builds$', 'projectbuilds', name='projectbuilds'), @@ -85,5 +88,5 @@ urlpatterns = patterns('toastergui.views', # default redirection - url(r'^$', RedirectView.as_view( url= 'builds/')), + url(r'^$', RedirectView.as_view( url= 'landing')), ) diff --git a/lib/toaster/toastergui/views.py b/lib/toaster/toastergui/views.py index 66113cfdf..38d67e378 100755 --- a/lib/toaster/toastergui/views.py +++ b/lib/toaster/toastergui/views.py @@ -38,6 +38,51 @@ from datetime import timedelta from django.utils import formats import json + +# all new sessions should come through the landing page; +# determine in which mode we are running in, and redirect appropriately +def landing(request): + if toastermain.settings.MANAGED and Build.objects.count() == 0 and Project.objects.count() > 0: + return redirect(reverse('all-projects'), permanent = False) + + if Build.objects.all().count() > 0: + return redirect(reverse('all-builds'), permanent = False) + + return render(request, 'landing.html') + +def _project_recent_build_list(prj): + # build requests not yet started + return (map(lambda x: { + "id": x.pk, + "targets" : map(lambda y: {"target": y.target }, x.brtarget_set.all()), + "status": x.get_state_display(), + }, prj.buildrequest_set.filter(state__lt = BuildRequest.REQ_INPROGRESS).order_by("-pk")) + + # build requests started, but with no build yet + map(lambda x: { + "id": x.pk, + "targets" : map(lambda y: {"target": y.target }, x.brtarget_set.all()), + "status": x.get_state_display(), + }, prj.buildrequest_set.filter(state = BuildRequest.REQ_INPROGRESS, build = None).order_by("-pk")) + + # build requests that failed + map(lambda x: { + "id": x.pk, + "targets" : map(lambda y: {"target": y.target }, x.brtarget_set.all()), + "status": x.get_state_display(), + "errors": map(lambda y: {"type": y.errtype, "msg": y.errmsg, "tb": y.traceback}, x.brerror_set.all()), + }, prj.buildrequest_set.filter(state = BuildRequest.REQ_FAILED).order_by("-pk")) + + # and already made builds + map(lambda x: { + "id": x.pk, + "targets": map(lambda y: {"target": y.target }, x.target_set.all()), + "status": x.get_outcome_display(), + "completed_on" : x.completed_on.strftime('%s')+"000", + "build_time" : (x.completed_on - x.started_on).total_seconds(), + "build_page_url" : reverse('builddashboard', args=(x.pk,)), + "completeper": x.completeper(), + "eta": x.eta().ctime(), + }, prj.build_set.all())) + + def _build_page_range(paginator, index = 1): try: page = paginator.page(index) @@ -219,6 +264,8 @@ def _save_parameters_cookies(response, pagesize, orderby, request): response.set_cookie(key='orderby', value=html_parser.unescape(orderby), path=request.path) return response + + # shows the "all builds" page def builds(request): template = 'build.html' @@ -242,7 +289,7 @@ def builds(request): build_info = _build_page_range(Paginator(queryset, pagesize), request.GET.get('page', 1)) # build view-specific information; this is rendered specifically in the builds page, at the top of the page (i.e. Recent builds) - build_mru = Build.objects.filter(completed_on__gte=(timezone.now()-timedelta(hours=24))).order_by("-started_on")[:3] + build_mru = Build.objects.order_by("-started_on")[:3] # set up list of fstypes for each build fstypes_map = {}; @@ -2553,6 +2600,84 @@ if toastermain.settings.MANAGED: } return render(request, template, context) + + + + def projects(request): + template="projects.html" + + (pagesize, orderby) = _get_parameters_values(request, 10, 'updated:-') + mandatory_parameters = { 'count': pagesize, 'page' : 1, 'orderby' : orderby } + retval = _verify_parameters( request.GET, mandatory_parameters ) + if retval: + return _redirect_parameters( 'all-projects', request.GET, mandatory_parameters) + + queryset_all = Project.objects.all() + + # boilerplate code that takes a request for an object type and returns a queryset + # for that object type. copypasta for all needed table searches + (filter_string, search_term, ordering_string) = _search_tuple(request, Project) + queryset_with_search = _get_queryset(Project, queryset_all, None, search_term, ordering_string, 'updated:-') + queryset = _get_queryset(Project, queryset_all, filter_string, search_term, ordering_string, 'updated:-') + + # retrieve the objects that will be displayed in the table; projects a paginator and gets a page range to display + project_info = _build_page_range(Paginator(queryset, pagesize), request.GET.get('page', 1)) + + # build view-specific information; this is rendered specifically in the builds page, at the top of the page (i.e. Recent builds) + build_mru = Build.objects.order_by("-started_on")[:3] + + + + context = { + 'mru' : build_mru, + + 'objects' : project_info, + 'objectname' : "projects", + 'default_orderby' : 'id:-', + 'search_term' : search_term, + 'total_count' : queryset_with_search.count(), + 'tablecols': [ + {'name': 'Project', + 'orderfield': _get_toggle_order(request, "name"), + 'ordericon':_get_toggle_order_icon(request, "name"), + 'orderkey' : 'name', + }, + {'name': 'Release', + 'qhelp' : "The version of the build system used by the project", + 'orderfield': _get_toggle_order(request, "release__name"), + 'ordericon':_get_toggle_order_icon(request, "release__name"), + 'orderkey' : 'release__name', + }, + {'name': 'Machine', + 'qhelp': "The hardware currently selected for the project", + }, + {'name': 'Number of builds', + 'qhelp': "How many builds have been run for the project", + }, + {'name': 'Last outcome', 'clclass': 'loutcome', + 'qhelp': "Tells you if the last project build completed successfully or failed", + }, + {'name': 'Last target', 'clclass': 'ltarget', + 'qhelp': "The last project build target(s): one or more recipes or image recipes", + }, + {'name': 'Last errors', 'clclass': 'lerrors', + 'qhelp': "How many errors were encountered during the last project build (if any)", + }, + {'name': 'Last warnings', 'clclass': 'lwarnings', + 'qhelp': "How many warnigns were encountered during the last project build (if any)", + }, + {'name': 'Last image files', 'clclass': 'limagefiles', 'hidden': 1, + 'qhelp': "The root file system types produced by the last project build", + }, + {'name': 'Last updated', 'clclass': 'updated', + 'orderfield': _get_toggle_order(request, "updated"), + 'ordericon':_get_toggle_order_icon(request, "updated"), + 'orderkey' : 'updated', + } + ] + } + return render(request, template, context) + else: # these are pages that are NOT available in interactive mode def managedcontextprocessor(request): @@ -2599,3 +2724,6 @@ else: def projectbuilds(request): raise Exception("page not available in interactive mode") + + def projects(request): + raise Exception("page not available in interactive mode") |