Toaster refactoring and bugfixing patchset
cover Toaster refactoring and bugfixing patchset
From: Alexandru DAMIAN <[email protected]>
This is a Toaster patchest that brings in refactoring in order to eliminate duplicated code paths, and bring in improvements in the communication between the browser and back-end.
The changes have been tested and reviewed on the Toaster mailing list.
Can you please pull when possible ?
Cheers,
Alex
The following changes since commit 430f7a288b4446600b3a943c51f6711ffcf9e619:
bitbake: Bump version to 1.27.1 (2015-06-09 13:29:19 +0100)
are available in the git repository at:
git://git.yoctoproject.org/poky-contrib adamian/20150610-submission-bb
http://git.yoctoproject.org/cgit.cgi/poky-contrib/log/?h=adamian/20150610-submission-bb
Alexandru DAMIAN (14):
toaster: move project data typeahead to the REST API convert all project-based view to JSON APIs
toaster: toastertables REST refactoring
toastergui: use defaults on xhr_datatypeahead parameters
toastergui: remove xhr_projectedit and xhr_projectinfo URLs
toaster: toaster table add raw data
toaster: ToasterTables add computational fields
toaster: toastertables raise errors
toaster: add class template view for single-object pages
toaster: eliminate duplicate querysets
toastergui: remove xhr_datatypeahead and xhr_XXXbuild
toastergui: remove xhr_datatypeahaed layerdeps call
toaster: fixes after refactoring
toastergui: prevent contamination of libtoaster context
Ed Bartosh (2):
toaster: Code cleanup: bashisms
toaster: Make toaster script to work in dash
Michael Wood (2):
toaster: Add ajax loading spinner
toaster: Add global ajaxError handler
Vlad Fulgeanu (1):
toaster: tables: Remove obsolete field_name for select column
bin/toaster | 82 ++-- lib/toaster/contrib/tts/urllist.py | 5 - lib/toaster/orm/models.py | 44 +- lib/toaster/toastergui/static/css/default.css | 49 +++ lib/toaster/toastergui/static/js/base.js | 140 +++--- lib/toaster/toastergui/static/js/importlayer.js | 14 +- lib/toaster/toastergui/static/js/layerdetails.js | 6 +- lib/toaster/toastergui/static/js/libtoaster.js | 60 ++- lib/toaster/toastergui/static/js/projectapp.js | 146 ++++-- lib/toaster/toastergui/static/js/table.js | 37 +- lib/toaster/toastergui/tables.py | 120 +++-- lib/toaster/toastergui/templates/base.html | 29 +- .../toastergui/templates/basetable_bottom.html | 2 +- .../templates/detail_pagination_bottom.html | 2 +- lib/toaster/toastergui/templates/importlayer.html | 1 - lib/toaster/toastergui/templates/layerdetails.html | 5 +- .../toastergui/templates/managed_mrb_section.html | 2 +- lib/toaster/toastergui/templates/project.html | 56 ++- lib/toaster/toastergui/templates/runagain.html | 2 +- .../toastergui/templates/toastertable-simple.html | 2 +- lib/toaster/toastergui/templates/toastertable.html | 2 +- lib/toaster/toastergui/templatetags/projecttags.py | 4 +- lib/toaster/toastergui/tests.py | 7 +- lib/toaster/toastergui/urls.py | 52 ++- lib/toaster/toastergui/views.py | 489 ++++++++++----------- lib/toaster/toastergui/widgets.py | 122 +++-- lib/toaster/toastermain/settings.py | 2 +- 27 files changed, 881 insertions(+), 601 deletions(-)
Signature · 1 line
1.9.1
PATCH 1/19 toaster: move project data typeahead to the REST API
From: Alexandru DAMIAN <[email protected]>
This patch enables JSON requests on the project REST endpoint, and replaces the universal queries "xhr_datatypeahead" with the project type to the REST project endpoint.
The patch adds a decorator that takes a context returned by a view and either renders the template specified as the decorator argument, or converts the context to JSON.
Normal "search", "filter" and "order" options for view work as normal on the JSON API format. To enable the JSON return, set the "format" GET parameter to "json".
In order to demonstrate the functionality, the "New build" button is switched from using the xhr_datatypeahead to the project REST API with JSON formatting. Additionally, the XHR APIs that perform actions with the project id passed as parameter are removed, and the needed URLs are populated from the project JSON API returns after the project has been selected.
--- lib/toaster/toastergui/static/js/base.js | 51 ++++++++----- lib/toaster/toastergui/static/js/importlayer.js | 6 +- lib/toaster/toastergui/static/js/layerdetails.js | 2 +- lib/toaster/toastergui/static/js/libtoaster.js | 12 +-- lib/toaster/toastergui/templates/base.html | 24 +++--- .../toastergui/templates/basetable_bottom.html | 2 +- .../templates/detail_pagination_bottom.html | 2 +- lib/toaster/toastergui/templatetags/projecttags.py | 4 +- lib/toaster/toastergui/urls.py | 3 - lib/toaster/toastergui/views.py | 89 ++++++++++++++-------- 10 files changed, 119 insertions(+), 76 deletions(-)
lib/toaster/toastergui/static/js/base.js +34 -17
@@ -15,20 +15,21 @@ function basePageInit (ctx) { /* Hide the change project icon when there is only one project */ if (ctx.numProjects == 1){ - $('#project .icon-pencil').hide(); + $('#project .icon-pencil').hide(); } newBuildButton.show().removeAttr("disabled"); + _checkProjectBuildable() _setupNewBuildButton(); function _checkProjectBuildable(){ - if (currentProjectId == undefined) + if (libtoaster.ctx.projectId == undefined) return; - libtoaster.getProjectInfo(ctx.projectInfoUrl, currentProjectId, + libtoaster.getProjectInfo(ctx.projectInfoUrl, libtoaster.ctx.projectId, function(data){ if (data.machine.name == undefined || data.layers.length == 0) { /* we can't build anything with out a machine and some layers */
@@ -53,18 +54,13 @@ function basePageInit (ctx) { /* If we don't have a current project then present the set project * form. */ - if (currentProjectId == undefined) { + if (libtoaster.ctx.projectId == undefined) { $('#change-project-form').show(); $('#project .icon-pencil').hide(); } - libtoaster.makeTypeahead(newBuildTargetInput, { type : "targets", project_id: currentProjectId }, function(item){ - /* successfully selected a target */ - selectedTarget = item; - }); - - libtoaster.makeTypeahead(newBuildProjectInput, { type : "projects" }, function(item){ + libtoaster.makeTypeahead(newBuildProjectInput, libtoaster.ctx.projectsUrl, { format : "json" }, function(item){ /* successfully selected a project */ newBuildProjectSaveBtn.removeAttr("disabled"); selectedProject = item;
@@ -93,20 +89,40 @@ function basePageInit (ctx) { if (!selectedTarget) selectedTarget = { name: newBuildTargetInput.val() }; /* fire and forget */ - libtoaster.startABuild(ctx.projectBuildUrl, currentProjectId, selectedTarget.name, null, null); - window.location.replace(ctx.projectBasePageUrl+currentProjectId); + libtoaster.startABuild(ctx.projectBuildUrl, libtoaster.ctx.projectId, selectedTarget.name, null, null); + window.location.replace(libtoaster.ctx.projectPageUrl); }); newBuildProjectSaveBtn.click(function() { - currentProjectId = selectedProject.id + libtoaster.ctx.projectId = selectedProject.pk /* Update the typeahead project_id paramater */ _checkProjectBuildable(); - newBuildTargetInput.data('typeahead').options.xhrParams.project_id = currentProjectId; - newBuildTargetInput.val(""); - $("#new-build-button #project a").text(selectedProject.name).attr('href', ctx.projectBasePageUrl+currentProjectId); - $("#new-build-button .alert a").attr('href', ctx.projectBasePageUrl+currentProjectId); + /* we set the effective context of the page to the currently selected project */ + /* TBD: do we override even if we already have a context project ?? */ + /* TODO: replace global library context with references to the "selected" project */ + libtoaster.ctx.projectPageUrl = selectedProject.projectPageUrl; + libtoaster.ctx.xhrProjectEditUrl = selectedProject.xhrProjectEditUrl; + libtoaster.ctx.projectName = selectedProject.name; + libtoaster.ctx.projectId = selectedProject.id; + + ctx.projectBuildUrl = selectedProject.projectBuildUrl; + + /* we can create a target typeahead only after we have a project selected */ + newBuildTargetInput.prop("disabled", false); + newBuildTargetBuildBtn.prop("disabled", false); + + libtoaster.makeTypeahead(newBuildTargetInput, selectedProject.xhrProjectDataTypeaheadUrl, { type : "targets" }, function(item){ + /* successfully selected a target */ + selectedTarget = item; + }); + + newBuildTargetInput.val(""); + /* set up new form aspect */ + $("#new-build-button #project a").text(selectedProject.name).attr('href', libtoaster.ctx.projectPageUrl); + $("#new-build-button .alert a").attr('href', libtoaster.ctx.projectPageUrl); + $("#project .icon-pencil").show(); $("#change-project-form").slideUp({ 'complete' : function() { $("#new-build-button #project").show();
@@ -116,6 +132,7 @@ function basePageInit (ctx) { $('#new-build-button #project .icon-pencil').click(function() { newBuildProjectSaveBtn.attr("disabled", "disabled"); newBuildProjectInput.val($("#new-build-button #project a").text()); + $("#cancel-change-project").show(); $(this).parent().hide(); $("#change-project-form").slideDown(); });
lib/toaster/toastergui/static/js/importlayer.js +3 -3
@@ -18,7 +18,7 @@ function importLayerPageInit (ctx) { $("#new-project-button").hide(); - libtoaster.makeTypeahead(layerDepInput, { type : "layers", project_id: libtoaster.ctx.projectId, include_added: "true" }, function(item){ + libtoaster.makeTypeahead(layerDepInput, libtoaster.ctx.xhrProjectDataTypeaheadUrl, { type : "layers", project_id: libtoaster.ctx.projectId, include_added: "true" }, function(item){ currentLayerDepSelection = item; layerDepBtn.removeAttr("disabled");
@@ -28,7 +28,7 @@ function importLayerPageInit (ctx) { /* We automatically add "openembedded-core" layer for convenience as a * dependency as pretty much all layers depend on this one */ - $.getJSON(libtoaster.ctx.xhrDataTypeaheadUrl, { type : "layers", project_id: libtoaster.ctx.projectId, include_added: "true" , value: "openembedded-core" }, function(layer) { + $.getJSON(libtoaster.ctx.xhrProjectDataTypeaheadUrl, { type : "layers", project_id: libtoaster.ctx.projectId, include_added: "true" , value: "openembedded-core" }, function(layer) { if (layer.list.length == 1) { currentLayerDepSelection = layer.list[0]; layerDepBtn.click();
@@ -211,7 +211,7 @@ function importLayerPageInit (ctx) { var name = $(this).val(); /* Check if the layer name exists */ - $.getJSON(libtoaster.ctx.xhrDataTypeaheadUrl, { type : "layers", project_id: libtoaster.ctx.projectId, include_added: "true" , value: name }, function(layer) { + $.getJSON(libtoaster.ctx.xhrProjectDataTypeaheadUrl, { type : "layers", project_id: libtoaster.ctx.projectId, include_added: "true" , value: name }, function(layer) { if (layer.list.length > 0) { for (var i in layer.list){ if (layer.list[i].name == name) {
lib/toaster/toastergui/static/js/layerdetails.js +1 -1
@@ -9,7 +9,7 @@ function layerDetailsPageInit (ctx) { var addRmLayerBtn = $("#add-remove-layer-btn"); /* setup the dependencies typeahead */ - libtoaster.makeTypeahead(layerDepInput, { type : "layers", project_id: libtoaster.ctx.projectId, include_added: "true" }, function(item){ + libtoaster.makeTypeahead(layerDepInput, libtoaster.ctx.xhrProjectDataTypeaheadUrl, { type : "layers", project_id: libtoaster.ctx.projectId, include_added: "true" }, function(item){ currentLayerDepSelection = item; layerDepBtn.removeAttr("disabled");
lib/toaster/toastergui/static/js/libtoaster.js +6 -6
@@ -10,16 +10,16 @@ var libtoaster = (function (){ * xhrUrl: the url to get the JSON from expects JSON in the form: * { "list": [ { "name": "test", "detail" : "a test thing" }, .... ] } * xhrParams: the data/parameters to pass to the getJSON url e.g. - * { 'type' : 'projects' } the text typed will be passed as 'value'. + * { 'type' : 'projects' } the text typed will be passed as 'search'. * selectedCB: function to call once an item has been selected one * arg of the item. */ - function _makeTypeahead (jQElement, xhrParams, selectedCB) { + function _makeTypeahead (jQElement, xhrUrl, xhrParams, selectedCB) { jQElement.typeahead({ source: function(query, process){ - xhrParams.value = query; - $.getJSON(libtoaster.ctx.xhrDataTypeaheadUrl, this.options.xhrParams, function(data){ + xhrParams.search = query; + $.getJSON(xhrUrl, this.options.xhrParams, function(data){ if (data.error !== "ok") { console.log("Error getting data from server "+data.error); return;
@@ -41,7 +41,7 @@ var libtoaster = (function (){ return $('<span></span>').text(item.name).get(0); }, sorter: function (items) { return items; }, - xhrUrl: libtoaster.ctx.xhrDataTypeaheadUrl, + xhrUrl: xhrUrl, xhrParams: xhrParams, });
@@ -172,7 +172,7 @@ var libtoaster = (function (){ function _getLayerDepsForProject(projectId, layerId, onSuccess, onFail){ /* Check for dependencies not in the current project */ - $.getJSON(libtoaster.ctx.xhrDataTypeaheadUrl, + $.getJSON(libtoaster.ctx.xhrProjectDataTypeaheadUrl, { type: 'layerdeps', 'value': layerId , project_id: projectId }, function(data) { if (data.error != "ok") {
lib/toaster/toastergui/templates/base.html +15 -9
@@ -31,11 +31,19 @@ projectId : {{project.id|default:'undefined'}}, jsUrl : "{% static 'js/' %}", htmlUrl : "{% static 'html/' %}", + projectsUrl : "{% url 'all-projects' %}", {% if project.id %} - xhrDataTypeaheadUrl : "{% url 'xhr_datatypeahead' project.id %}", - xhrProjectEditUrl : "{% url 'xhr_projectedit' project.id %}", - projectPageUrl : "{% url 'project' project.id %}", - projectName : "{{project.name}}", + xhrProjectDataTypeaheadUrl : "{% url 'xhr_datatypeahead' project.id %}", + xhrProjectEditUrl : "{% url 'xhr_projectedit' project.id %}", + projectPageUrl : "{% url 'project' project.id %}", + projectName : "{{project.name}}", + projectId : {{project.id}}, + {% else %} + xhrProjectDataTypeaheadUrl : undefined, + xhrProjectEditUrl : undefined, + projectPageUrl : undefined, + projectName : undefined, + projectId : undefined, {% endif %} }; </script>
@@ -45,8 +53,6 @@ $(document).ready(function () { /* Vars needed for base.js */ var ctx = {}; - ctx.projectBuildUrl = "{% url 'xhr_build' %}"; - ctx.projectBasePageUrl = "{% url 'base_project' %}"; ctx.projectInfoUrl = "{% url 'xhr_projectinfo' %}"; ctx.numProjects = {{projects|length}}; ctx.currentUrl = "{{request.path|escapejs}}";
@@ -99,7 +105,7 @@ <div class="input-append"> <input type="text" class="input-medium" id="project-name-input" placeholder="Type a project name" autocomplete="off" data-minLength="1" data-autocomplete="off" data-provide="typeahead"/> <button id="save-project-button" class="btn" type="button">Save</button> - <a href="#" id="cancel-change-project" class="btn btn-link">Cancel</a> + <a href="#" id="cancel-change-project" class="btn btn-link" style="display: none">Cancel</a> </div> <p><a id="view-all-projects" href="{% url 'all-projects' %}">View all projects</a></p> </form>
@@ -111,9 +117,9 @@ <li id="targets-form"> <h6>Recipe(s):</h6> <form> - <input type="text" class="input-xlarge" id="build-target-input" placeholder="Type a recipe name" autocomplete="off" data-minLength="1" data-autocomplete="off" data-provide="typeahead" /> + <input type="text" class="input-xlarge" id="build-target-input" placeholder="Type a recipe name" autocomplete="off" data-minLength="1" data-autocomplete="off" data-provide="typeahead" disabled/> <div> - <a class="btn btn-primary" id="build-button" data-project-id="{{project.id}}">Build</a> + <button class="btn btn-primary" id="build-button" data-project-id="{{project.id}}" disabled>Build</button> </div> </form> </li>
lib/toaster/toastergui/templates/basetable_bottom.html +1 -1
@@ -57,7 +57,7 @@ } } - // load cookie for number of entries to be displayed on page + // load data for number of entries to be displayed on page if ({{request.GET.count}} != "") { pagesize = {{request.GET.count}}; }
lib/toaster/toastergui/templates/detail_pagination_bottom.html +1 -1
@@ -38,7 +38,7 @@ <!-- Update page display settings --> <script> $(document).ready(function() { - // load cookie for number of entries to be displayed on page + // load data for number of entries to be displayed on page if ({{request.GET.count}} != "") { pagesize = {{request.GET.count}}; }
lib/toaster/toastergui/templatetags/projecttags.py +2 -2
@@ -51,11 +51,11 @@ def get_tasks(queryset): @register.filter(name = "json") -def json(value): +def json(value, default = None): # JSON spec says that "\/" is functionally identical to "/" to allow for HTML-tag embedding in JSON strings # unfortunately, I can't find any option in the json module to turn on forward-slash escaping, so we do # it manually here - return mark_safe(JsonLib.dumps(value, ensure_ascii=False).replace('</', '<\\/')) + return mark_safe(JsonLib.dumps(value, default = default, ensure_ascii=False).replace('</', '<\\/')) @register.assignment_tag def query(qs, **kwargs):
lib/toaster/toastergui/urls.py +0 -3
@@ -77,8 +77,6 @@ urlpatterns = patterns('toastergui.views', url(r'^projects/$', 'projects', name='all-projects'), - url(r'^project/$', lambda x: HttpResponseBadRequest(), name='base_project'), - 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'),
@@ -110,7 +108,6 @@ urlpatterns = patterns('toastergui.views', name="all-layers"), - url(r'^xhr_build/$', 'xhr_build', name='xhr_build'), url(r'^xhr_projectbuild/(?P<pid>\d+)$', 'xhr_projectbuild', name='xhr_projectbuild'), url(r'^xhr_projectinfo/$', 'xhr_projectinfo', name='xhr_projectinfo'), url(r'^xhr_projectedit/(?P<pid>\d+)$', 'xhr_projectedit', name='xhr_projectedit'),
lib/toaster/toastergui/views.py +56 -33
175 lines not on this page · raw message
Signature · 1 line
1.9.1
PATCH 2/19 convert all project-based view to JSON APIs
From: Alexandru DAMIAN <[email protected]>
This patch converts all-project views that are REST-style URLs to support JSON encoding if the "format=json" parameter is supplied.
The formatting code is enhanced to prevent following Django foreign keys, to convert enum-values from int to string, and support for timedelta.
--- lib/toaster/toastergui/views.py | 164 ++++++++++++++++++++++++---------------- 1 file changed, 99 insertions(+), 65 deletions(-)
lib/toaster/toastergui/views.py +99 -65
@@ -87,6 +87,55 @@ def _project_recent_build_list(prj): list(prj.buildrequest_set.filter(state__in=[BuildRequest.REQ_COMPLETED, BuildRequest.REQ_FAILED]).order_by("-pk")[:3])) +def _template_renderer(template): + def func_wrapper(view): + def returned_wrapper(request, *args, **kwargs): + try: + context = view(request, *args, **kwargs) + except RedirectException as e: + return e.get_redirect_response() + + if request.GET.get('format', None) == 'json': + # objects is a special keyword - it's a Page, but we need the actual objects here + # in XHR, the objects come in the "list" property + if "objects" in context: + context["list"] = context["objects"].object_list + del context["objects"] + + # we're about to return; to keep up with the XHR API, we set the error to OK + context["error"] = "ok" + def _objtojson(obj): + from django.db.models.query import QuerySet + from django.db.models import Model, IntegerField + if isinstance(obj, datetime): + return obj.isoformat() + elif isinstance(obj, timedelta): + return obj.total_seconds() + elif isinstance(obj, QuerySet) or isinstance(obj, set): + return list(obj) + elif hasattr( obj, '__dict__'): + d = obj.__dict__ + nd = dict(d) + for di in d: + if di.startswith("_"): + del nd[di] + elif isinstance(d[di], Model): + nd[di] = d[di].pk + elif isinstance(d[di], int) and hasattr(obj, "get_%s_display" % di): + nd[di] = getattr(obj, "get_%s_display" % di)() + return nd + else: + raise TypeError("Unserializable object %s of type %s" % ( obj, type(obj))) + + return HttpResponse(jsonfilter(context, default=_objtojson ), + content_type = "application/json; charset=utf-8") + else: + return render(request, template, context) + return returned_wrapper + return func_wrapper + + + def _build_page_range(paginator, index = 1): try: page = paginator.page(index)
@@ -129,7 +178,7 @@ def _redirect_parameters(view, g, mandatory_parameters, *args, **kwargs): if not i in params: params[i] = urllib.unquote(str(mandatory_parameters[i])) - return redirect(url + "?%s" % urllib.urlencode(params), permanent = False, *args, **kwargs) + return redirect(url + "?%s" % urllib.urlencode(params), permanent = False, **kwargs) class RedirectException(Exception): def __init__(self, view, g, mandatory_parameters, *args, **kwargs):
@@ -141,7 +190,7 @@ class RedirectException(Exception): self.okwargs = kwargs def get_redirect_response(self): - return _redirect_parameters(self.view, self.g, self.mandatory_parameters, self.oargs, self.okwargs) + return _redirect_parameters(self.view, self.g, self.mandatory_parameters, self.oargs, **self.okwargs) FIELD_SEPARATOR = ":" AND_VALUE_SEPARATOR = "!"
@@ -1839,8 +1888,8 @@ if toastermain.settings.MANAGED: # shows the "all builds" page for managed mode; it displays build requests (at least started!) instead of actual builds + @_template_renderer("managed_builds.html") def builds(request): - template = 'managed_builds.html' # define here what parameters the view needs in the GET portion in order to # be able to display something. 'count' and 'page' are mandatory for all views # that use paginators.
@@ -1850,12 +1899,10 @@ if toastermain.settings.MANAGED: try: context, pagesize, orderby = _build_list_helper(request, buildrequests, True) except InvalidRequestException as e: - return _redirect_parameters( builds, request.GET, e.response) + raise RedirectException( builds, request.GET, e.response) - response = render(request, template, context) _set_parameters_values(pagesize, orderby, request) - return response - + return context # helper function, to be used on "all builds" and "project builds" pages
@@ -2151,8 +2198,8 @@ if toastermain.settings.MANAGED: # Shows the edit project page + @_template_renderer('project.html') def project(request, pid): - template = "project.html" try: prj = Project.objects.get(id = pid) except Project.DoesNotExist:
@@ -2207,10 +2254,7 @@ if toastermain.settings.MANAGED: except ProjectVariable.DoesNotExist: context["distro"] = "-- not set yet" - response = render(request, template, context) - response['Cache-Control'] = "no-cache, must-revalidate, no-store" - response['Pragma'] = "no-cache" - return response + return context def xhr_projectbuild(request, pid):
@@ -2680,8 +2724,8 @@ if toastermain.settings.MANAGED: return(vars_managed,sorted(vars_fstypes),vars_blacklist) + @_template_renderer("projectconf.html") def projectconf(request, pid): - template = "projectconf.html" try: prj = Project.objects.get(id = pid)
@@ -2730,21 +2774,20 @@ if toastermain.settings.MANAGED: except ProjectVariable.DoesNotExist: pass - return render(request, template, context) + return context + @_template_renderer('projectbuilds.html') def projectbuilds(request, pid): - template = 'projectbuilds.html' buildrequests = BuildRequest.objects.filter(project_id = pid).exclude(state__lte = BuildRequest.REQ_INPROGRESS).exclude(state=BuildRequest.REQ_DELETED) try: context, pagesize, orderby = _build_list_helper(request, buildrequests, False) except InvalidRequestException as e: - return _redirect_parameters(projectbuilds, request.GET, e.response, pid = pid) + raise RedirectException('projectbuilds', request.GET, e.response, pid = pid) - response = render(request, template, context) _set_parameters_values(pagesize, orderby, request) - return response + return context def _file_name_for_artifact(b, artifact_type, artifact_id):
@@ -2861,32 +2904,8 @@ if toastermain.settings.MANAGED: return build_mru - def template_renderer(template): - def func_wrapper(view): - def returned_wrapper(request, *args, **kwargs): - try: - context = view(request, *args, **kwargs) - except RedirectException as e: - return e.get_redirect_response() - - if request.GET.get('format', None) == 'json': - # objects is a special keyword - it's a Page, but we need the actual objects here - # in XHR, the objects come in the "list" property - if "objects" in context: - context["list"] = context["objects"].object_list - del context["objects"] - - # we're about to return; to keep up with the XHR API, we set the error to OK - context["error"] = "ok" - - return HttpResponse(jsonfilter(context, default=lambda obj: obj.isoformat() if isinstance(obj, datetime) else obj.__dict__ ), - content_type = "application/json; charset=utf-8") - else: - return render(request, template, context) - return returned_wrapper - return func_wrapper - @template_renderer("projects.html") + @_template_renderer("projects.html") def projects(request): (pagesize, orderby) = _get_parameters_values(request, 10, 'updated:-') mandatory_parameters = { 'count': pagesize, 'page' : 1, 'orderby' : orderby }
@@ -2995,18 +3014,19 @@ if toastermain.settings.MANAGED: _set_parameters_values(pagesize, orderby, request) return context + @_template_renderer("buildrequestdetails.html") def buildrequestdetails(request, pid, brid): - template = "buildrequestdetails.html" context = { 'buildrequest' : BuildRequest.objects.get(pk = brid, project_id = pid) } - return render(request, template, context) + return context else: # shows the "all builds" page for interactive mode; this is the old code, simply moved + + @_template_renderer('build.html') def builds(request): - template = 'build.html' # define here what parameters the view needs in the GET portion in order to # be able to display something. 'count' and 'page' are mandatory for all views # that use paginators.
@@ -3014,7 +3034,7 @@ else: mandatory_parameters = { 'count': pagesize, 'page' : 1, 'orderby' : orderby } retval = _verify_parameters( request.GET, mandatory_parameters ) if retval: - return _redirect_parameters( 'all-builds', request.GET, mandatory_parameters) + raise RedirectException( 'all-builds', request.GET, mandatory_parameters) # boilerplate code that takes a request for an object type and returns a queryset # for that object type. copypasta for all needed table searches
@@ -3194,55 +3214,69 @@ else: # merge daterange values context.update(context_date) - - response = render(request, template, context) _set_parameters_values(pagesize, orderby, request) - return response + + return context + @_template_renderer('landing_not_managed.html') def newproject(request): - return render(request, 'landing_not_managed.html') + return {} + @_template_renderer('landing_not_managed.html') def project(request, pid): - return render(request, 'landing_not_managed.html') + return {} + @_template_renderer('landing_not_managed.html') def xhr_projectbuild(request, pid): - return render(request, 'landing_not_managed.html') + return {} + @_template_renderer('landing_not_managed.html') def xhr_projectinfo(request): - return render(request, 'landing_not_managed.html') + return {} + @_template_renderer('landing_not_managed.html') def xhr_projectedit(request, pid): - return render(request, 'landing_not_managed.html') + return {} + @_template_renderer('landing_not_managed.html') def xhr_datatypeahead(request): - return render(request, 'landing_not_managed.html') + return {} + @_template_renderer('landing_not_managed.html') def xhr_configvaredit(request, pid): - return render(request, 'landing_not_managed.html') + return {} + @_template_renderer('landing_not_managed.html') def importlayer(request): - return render(request, 'landing_not_managed.html') + return {} + @_template_renderer('landing_not_managed.html') def projectconf(request, pid): - return render(request, 'landing_not_managed.html') + return {} + @_template_renderer('landing_not_managed.html') def projectbuilds(request, pid): - return render(request, 'landing_not_managed.html') + return {} + @_template_renderer('landing_not_managed.html') def build_artifact(request, build_id, artifact_type, artifact_id): - return render(request, 'landing_not_managed.html') + return {} + @_template_renderer('landing_not_managed.html') def projects(request): - return render(request, 'landing_not_managed.html') + return {} + @_template_renderer('landing_not_managed.html') def xhr_importlayer(request): - return render(request, 'landing_not_managed.html') + return {} + @_template_renderer('landing_not_managed.html') def xhr_updatelayer(request): - return render(request, 'landing_not_managed.html') + return {} + @_template_renderer('landing_not_managed.html') def buildrequestdetails(request, pid, brid): - return render(request, 'landing_not_managed.html') + return {}
Signature · 1 line
1.9.1
PATCH 3/19 toaster: toastertables REST refactoring
In reply to Alex DAMIANFrom: Alexandru DAMIAN <[email protected]>
This patch refactors the ToasterTables to bring them in line with REST principles -
- all table pages now support the "format=json" GET parameter that returns the data in JSON format
- the tables themselves
This cleans up the URL namespace by aleviating the need to have two URLS for each table (one for the template and one for the data loading), and fixes minor things in the ToasterTable implementation.
--- lib/toaster/toastergui/static/js/table.js | 5 +- lib/toaster/toastergui/tables.py | 84 ++++++++++++++-------- lib/toaster/toastergui/templates/layerdetails.html | 2 +- .../toastergui/templates/toastertable-simple.html | 2 +- lib/toaster/toastergui/templates/toastertable.html | 2 +- lib/toaster/toastergui/urls.py | 30 +++++--- lib/toaster/toastergui/widgets.py | 47 +++++------- 7 files changed, 102 insertions(+), 70 deletions(-)
lib/toaster/toastergui/static/js/table.js +3 -2
@@ -383,12 +383,13 @@ function tableInit(ctx){ */ var params = { 'name' : filterName, - 'search': tableParams.search + 'search': tableParams.search, + 'cmd': 'filterinfo', }; $.ajax({ type: "GET", - url: ctx.url + 'filterinfo', + url: ctx.url, data: params, headers: { 'X-CSRFToken' : $.cookie('csrftoken')}, success: function (filterData) {
lib/toaster/toastergui/tables.py +56 -28
137 lines not on this page · raw message
lib/toaster/toastergui/templates/layerdetails.html +1 -1
@@ -33,7 +33,7 @@ $(document).ready(function (){ var ctx = { - projectBuildUrl : "{% url 'xhr_build' %}", + projectBuildUrl : "{% url 'xhr_projectbuild' project.id %}", layerDetailsUrl : "{% url 'base_layerdetails' project.id %}", xhrUpdateLayerUrl : "{% url 'xhr_updatelayer' %}", layerVersion : {
lib/toaster/toastergui/templates/toastertable-simple.html +1 -1
@@ -10,7 +10,7 @@ var ctx = { tableName : "{{table_name}}", - url : "{{ xhr_table_url }}", + url : "{{ xhr_table_url }}?format=json", title : "{{title}}", projectLayers : {{projectlayers|json}}, };
lib/toaster/toastergui/templates/toastertable.html +1 -1
@@ -10,7 +10,7 @@ var ctx = { tableName : "{{table_name}}", - url : "{{ xhr_table_url }}", + url : "{{ xhr_table_url }}?format=json", title : "{{title}}", projectLayers : {{projectlayers|json}}, };
lib/toaster/toastergui/urls.py +21 -9
@@ -20,8 +20,7 @@ from django.conf.urls import patterns, include, url from django.views.generic import RedirectView, TemplateView from django.http import HttpResponseBadRequest -import tables -from widgets import ToasterTemplateView +from toastergui import tables urlpatterns = patterns('toastergui.views', # landing page
@@ -81,32 +80,46 @@ urlpatterns = patterns('toastergui.views', url(r'^project/(?P<pid>\d+)/configuration$', 'projectconf', name='projectconf'), url(r'^project/(?P<pid>\d+)/builds/$', 'projectbuilds', name='projectbuilds'), - url(r'^project/(?P<pid>\d+)/layer/(?P<layerid>\d+)$', - ToasterTemplateView.as_view(template_name='layerdetails.html'), - name='layerdetails'), url(r'^project/(?P<pid>\d+)/layer/$', lambda x,pid: HttpResponseBadRequest(), name='base_layerdetails'), # the import layer is a project-specific functionality; url(r'^project/(?P<pid>\d+)/importlayer$', 'importlayer', name='importlayer'), + + # the table pages that have been converted to ToasterTable widget url(r'^project/(?P<pid>\d+)/machines/$', - ToasterTemplateView.as_view(template_name="generic-toastertable-page.html"), + tables.MachinesTable.as_view(template_name="generic-toastertable-page.html"), { 'table_name': tables.MachinesTable.__name__.lower(), 'title' : 'All compatible machines' }, name="all-machines"), url(r'^project/(?P<pid>\d+)/recipes/$', - ToasterTemplateView.as_view(template_name="generic-toastertable-page.html"), + tables.RecipesTable.as_view(template_name="generic-toastertable-page.html"), { 'table_name': tables.RecipesTable.__name__.lower(), 'title' : 'All compatible recipes' }, name="all-targets"), url(r'^project/(?P<pid>\d+)/layers/$', - ToasterTemplateView.as_view(template_name="generic-toastertable-page.html"), + tables.LayersTable.as_view(template_name="generic-toastertable-page.html"), { 'table_name': tables.LayersTable.__name__.lower(), 'title' : 'All compatible layers' }, name="all-layers"), + url(r'^project/(?P<pid>\d+)/layer/(?P<layerid>\d+)$', + tables.LayerDetails.as_view(template_name='layerdetails.html'), + name='layerdetails'), + + url(r'^project/(?P<pid>\d+)/layer/(?P<layerid>\d+)/recipes/$', + tables.LayerRecipesTable.as_view(template_name="generic-toastertable-page.html"), + { 'table_name': tables.LayerRecipesTable.__name__.lower(), + 'title' : 'All recipes in layer' }, + name=tables.LayerRecipesTable.__name__.lower()), + + url(r'^project/(?P<pid>\d+)/layer/(?P<layerid>\d+)/machines/$', + tables.LayerMachinesTable.as_view(template_name="generic-toastertable-page.html"), + { 'table_name': tables.LayerMachinesTable.__name__.lower(), + 'title' : 'All machines in layer' }, + name=tables.LayerMachinesTable.__name__.lower()), url(r'^xhr_projectbuild/(?P<pid>\d+)$', 'xhr_projectbuild', name='xhr_projectbuild'), url(r'^xhr_projectinfo/$', 'xhr_projectinfo', name='xhr_projectinfo'),
@@ -116,7 +129,6 @@ urlpatterns = patterns('toastergui.views', url(r'^xhr_datatypeahead/(?P<pid>\d+)$', 'xhr_datatypeahead', name='xhr_datatypeahead'), url(r'^xhr_importlayer/$', 'xhr_importlayer', name='xhr_importlayer'), url(r'^xhr_updatelayer/$', 'xhr_updatelayer', name='xhr_updatelayer'), - url(r'^xhr_tables/project/(?P<pid>\d+)/', include('toastergui.tables')), # dashboard for failed build requests url(r'^project/(?P<pid>\d+)/buildrequest/(?P<brid>\d+)$', 'buildrequestdetails', name='buildrequestdetails'),
lib/toaster/toastergui/widgets.py +19 -28
68 lines not on this page · raw message
Signature · 1 line
1.9.1
PATCH 4/19 toastergui: use defaults on xhr_datatypeahead parameters
In reply to Alex DAMIANFrom: Alexandru DAMIAN <[email protected]>
This patch prevents crashes when xhr_datatypeahead is called without proper parameters.
--- lib/toaster/toastergui/views.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-)
lib/toaster/toastergui/views.py +10 -10
@@ -2379,7 +2379,7 @@ if toastermain.settings.MANAGED: # returns layers for current project release that are not in the project set, matching the name - if request.GET['type'] == "layers": + if request.GET.get('type', None) == "layers": # all layers for the current project queryset_all = prj.compatible_layerversions().filter(layer__name__icontains=request.GET.get('search',''))
@@ -2394,9 +2394,9 @@ if toastermain.settings.MANAGED: # returns layer dependencies for a layer, excluding current project layers - if request.GET['type'] == "layerdeps": + if request.GET.get('type', None) == "layerdeps": queryset = prj.compatible_layerversions().exclude(pk__in = [x.id for x in prj.projectlayer_equivalent_set()]).filter( - layer__name__in = [ x.depends_on.layer.name for x in LayerVersionDependency.objects.filter(layer_version_id = request.GET['search'])]) + layer__name__in = [ x.depends_on.layer.name for x in LayerVersionDependency.objects.filter(layer_version_id = request.GET.get('search', None))]) final_list = set([x.get_equivalents_wpriority(prj)[0] for x in queryset])
@@ -2405,11 +2405,11 @@ if toastermain.settings.MANAGED: # returns layer versions that would be deleted on the new release__pk - if request.GET['type'] == "versionlayers": + if request.GET.get('type', None) == "versionlayers": retval = [] for i in prj.projectlayer_set.all(): - lv = prj.compatible_layerversions(release = Release.objects.get(pk=request.GET['search'])).filter(layer__name = i.layercommit.layer.name) + lv = prj.compatible_layerversions(release = Release.objects.get(pk=request.GET.get('search', None))).filter(layer__name = i.layercommit.layer.name) # there is no layer_version with the new release id, and the same name if lv.count() < 1: retval.append(i)
@@ -2420,12 +2420,12 @@ if toastermain.settings.MANAGED: # returns layer versions that provide the named targets - if request.GET['type'] == "layers4target": + if request.GET.get('type', None) == "layers4target": # we return data only if the recipe can't be provided by the current project layer set - if reduce(lambda x, y: x + y, [x.recipe_layer_version.filter(name=request.GET['search']).count() for x in prj.projectlayer_equivalent_set()], 0): + if reduce(lambda x, y: x + y, [x.recipe_layer_version.filter(name=request.GET.get('search', None)).count() for x in prj.projectlayer_equivalent_set()], 0): final_list = [] else: - queryset_all = prj.compatible_layerversions().filter(recipe_layer_version__name = request.GET['search']) + queryset_all = prj.compatible_layerversions().filter(recipe_layer_version__name = request.GET.get('search', None)) # exclude layers in the project queryset_all = queryset_all.exclude(pk__in = [x.id for x in prj.projectlayer_equivalent_set()])
@@ -2436,7 +2436,7 @@ if toastermain.settings.MANAGED: return HttpResponse(jsonfilter( { "error":"ok", "list" : map( _lv_to_dict, final_list) }), content_type = "application/json") # returns targets provided by current project layers - if request.GET['type'] == "targets": + if request.GET.get('type', None) == "targets": search_token = request.GET.get('search','') queryset_all = Recipe.objects.filter(layer_version__layer__name__in = [x.layercommit.layer.name for x in prj.projectlayer_set.all().select_related("layercommit__layer")]).filter(Q(name__icontains=search_token) | Q(layer_version__layer__name__icontains=search_token))
@@ -2463,7 +2463,7 @@ if toastermain.settings.MANAGED: }), content_type = "application/json") # returns machines provided by the current project layers - if request.GET['type'] == "machines": + if request.GET.get('type', None) == "machines": queryset_all = Machine.objects.all() if 'project_id' in request.session: queryset_all = queryset_all.filter(layer_version__in = prj.projectlayer_equivalent_set()).order_by("name")
Signature · 1 line
1.9.1
PATCH 5/19 toastergui: remove xhr_projectedit and xhr_projectinfo URLs
In reply to Alex DAMIANFrom: Alexandru DAMIAN <[email protected]>
This patch removes the xhr_projectedit and xhr_projectinfo URLs in favour of REST calls to the Project page.
The project page takes now the POST requests to modify project settings. All usages of removed URLs are now changed to point to the project page, using the json format.
The interface call specs have not modified.
--- lib/toaster/contrib/tts/urllist.py | 3 - lib/toaster/toastergui/static/js/base.js | 3 +- lib/toaster/toastergui/static/js/libtoaster.js | 8 +- lib/toaster/toastergui/static/js/projectapp.js | 73 +++++++++++++--- lib/toaster/toastergui/templates/base.html | 3 - lib/toaster/toastergui/templates/project.html | 2 +- lib/toaster/toastergui/tests.py | 1 - lib/toaster/toastergui/urls.py | 2 - lib/toaster/toastergui/views.py | 110 +++++++++---------------- 9 files changed, 108 insertions(+), 97 deletions(-)
lib/toaster/contrib/tts/urllist.py +0 -3
@@ -40,10 +40,7 @@ URLS = [ 'toastergui/project/1/importlayer', 'toastergui/project/1/targets/', 'toastergui/project/1/machines/', -'toastergui/xhr_build/', 'toastergui/xhr_projectbuild/1/', -'toastergui/xhr_projectinfo/', -'toastergui/xhr_projectedit/1', 'toastergui/xhr_configvaredit/1', 'toastergui/xhr_datatypeahead/1', 'toastergui/xhr_importlayer/',
lib/toaster/toastergui/static/js/base.js +1 -2
@@ -29,7 +29,7 @@ function basePageInit (ctx) { if (libtoaster.ctx.projectId == undefined) return; - libtoaster.getProjectInfo(ctx.projectInfoUrl, libtoaster.ctx.projectId, + libtoaster.getProjectInfo(libtoaster.ctx.projectPageUrl, function(data){ if (data.machine.name == undefined || data.layers.length == 0) { /* we can't build anything with out a machine and some layers */
@@ -102,7 +102,6 @@ function basePageInit (ctx) { /* TBD: do we override even if we already have a context project ?? */ /* TODO: replace global library context with references to the "selected" project */ libtoaster.ctx.projectPageUrl = selectedProject.projectPageUrl; - libtoaster.ctx.xhrProjectEditUrl = selectedProject.xhrProjectEditUrl; libtoaster.ctx.projectName = selectedProject.name; libtoaster.ctx.projectId = selectedProject.id;
lib/toaster/toastergui/static/js/libtoaster.js +4 -4
@@ -120,11 +120,11 @@ var libtoaster = (function (){ } /* Get a project's configuration info */ - function _getProjectInfo(url, projectId, onsuccess, onfail){ + function _getProjectInfo(url, onsuccess, onfail){ $.ajax({ - type: "POST", + type: "GET", + data : { format: "json" }, url: url, - data: { project_id : projectId }, headers: { 'X-CSRFToken' : $.cookie('csrftoken')}, success: function (_data) { if (_data.error !== "ok") {
@@ -150,7 +150,7 @@ var libtoaster = (function (){ function _editCurrentProject(data, onSuccess, onFail){ $.ajax({ type: "POST", - url: libtoaster.ctx.xhrProjectEditUrl, + url: libtoaster.ctx.projectPageUrl + "?format=json", data: data, headers: { 'X-CSRFToken' : $.cookie('csrftoken')}, success: function (data) {
lib/toaster/toastergui/static/js/projectapp.js +61 -12
112 lines not on this page · raw message
lib/toaster/toastergui/templates/base.html +0 -3
@@ -34,13 +34,11 @@ projectsUrl : "{% url 'all-projects' %}", {% if project.id %} xhrProjectDataTypeaheadUrl : "{% url 'xhr_datatypeahead' project.id %}", - xhrProjectEditUrl : "{% url 'xhr_projectedit' project.id %}", projectPageUrl : "{% url 'project' project.id %}", projectName : "{{project.name}}", projectId : {{project.id}}, {% else %} xhrProjectDataTypeaheadUrl : undefined, - xhrProjectEditUrl : undefined, projectPageUrl : undefined, projectName : undefined, projectId : undefined,
@@ -53,7 +51,6 @@ $(document).ready(function () { /* Vars needed for base.js */ var ctx = {}; - ctx.projectInfoUrl = "{% url 'xhr_projectinfo' %}"; ctx.numProjects = {{projects|length}}; ctx.currentUrl = "{{request.path|escapejs}}";
lib/toaster/toastergui/templates/project.html +1 -1
@@ -430,7 +430,7 @@ angular.element(document).ready(function() { scope = angular.element("#main").scope(); scope.urls = {}; scope.urls.xhr_build = "{% url 'xhr_projectbuild' project.id %}"; - scope.urls.xhr_edit = "{% url 'xhr_projectedit' project.id %}"; + scope.urls.xhr_edit = "{% url 'project' project.id %}?format=json"; scope.urls.xhr_datatypeahead = "{% url 'xhr_datatypeahead' project.id %}"; scope.urls.layers = "{% url 'all-layers' project.id %}"; scope.urls.targets = "{% url 'all-targets' project.id %}";
lib/toaster/toastergui/tests.py +0 -1
@@ -40,7 +40,6 @@ class AllProjectsViewTestCase(ProvisionedProjectTestCase): self.assertTrue("id" in data["list"][0]) self.assertTrue("xhrProjectDataTypeaheadUrl" in data["list"][0]) self.assertTrue("projectPageUrl" in data["list"][0]) - self.assertTrue("xhrProjectEditUrl" in data["list"][0]) self.assertTrue("projectBuildUrl" in data["list"][0]) class ProvisionedLayersProjectTestCase(ProvisionedProjectTestCase):
lib/toaster/toastergui/urls.py +0 -2
@@ -122,8 +122,6 @@ urlpatterns = patterns('toastergui.views', name=tables.LayerMachinesTable.__name__.lower()), url(r'^xhr_projectbuild/(?P<pid>\d+)$', 'xhr_projectbuild', name='xhr_projectbuild'), - url(r'^xhr_projectinfo/$', 'xhr_projectinfo', name='xhr_projectinfo'), - url(r'^xhr_projectedit/(?P<pid>\d+)$', 'xhr_projectedit', name='xhr_projectedit'), url(r'^xhr_configvaredit/(?P<pid>\d+)$', 'xhr_configvaredit', name='xhr_configvaredit'), url(r'^xhr_datatypeahead/(?P<pid>\d+)$', 'xhr_datatypeahead', name='xhr_datatypeahead'),
lib/toaster/toastergui/views.py +41 -69
144 lines not on this page · raw message
Signature · 1 line
1.9.1
PATCH 6/19 toaster: toaster table add raw data
In reply to Alex DAMIANFrom: Alexandru DAMIAN <[email protected]>
We add in a JSON response both the raw data and the rendered version for display. The rendered fields start with "static:" to mark a different "namespace".
The toaster.js is updated to always display the "static:" version of a field, if it exists (and ignore the raw data unless the static rendering is missing).
--- lib/toaster/toastergui/static/js/table.js | 14 +++++++++++++- lib/toaster/toastergui/views.py | 31 ++++++++++++++++++++++++++++++- lib/toaster/toastergui/widgets.py | 16 +++++++++++----- 3 files changed, 54 insertions(+), 7 deletions(-)
lib/toaster/toastergui/static/js/table.js +13 -1
@@ -113,8 +113,20 @@ function tableInit(ctx){ for (var i in tableData.rows){ var row = $("<tr></tr>"); for (var key_j in tableData.rows[i]){ + /* if we have a static: version of a key, prefer the static: version for rendering */ + var orig_key_j = key_j; + + if (key_j.indexOf("static:") === 0) { + if (key_j.substr("static:".length) in tableData.rows[i]) { + continue; + } + orig_key_j = key_j.substr("static:".length) + } else if (("static:" + key_j) in tableData.rows[i]) { + key_j = "static:" + key_j; + } + var td = $("<td></td>"); - td.prop("class", key_j); + td.prop("class", orig_key_j); if (tableData.rows[i][key_j]){ td.html(tableData.rows[i][key_j]); }
lib/toaster/toastergui/views.py +30 -1
@@ -87,6 +87,35 @@ def _project_recent_build_list(prj): list(prj.buildrequest_set.filter(state__in=[BuildRequest.REQ_COMPLETED, BuildRequest.REQ_FAILED]).order_by("-pk")[:3])) + +def objtojson(obj): + from django.db.models.query import QuerySet + from django.db.models import Model, IntegerField + from django.db.models.fields.related import ForeignKey + + if isinstance(obj, datetime): + return obj.isoformat() + elif isinstance(obj, timedelta): + return obj.total_seconds() + elif isinstance(obj, QuerySet) or isinstance(obj, set): + return list(obj) + elif type(obj).__name__ == "RelatedManager": + return [x.pk for x in obj.all()] + elif hasattr( obj, '__dict__') and isinstance(obj, Model): + d = obj.__dict__ + nd = dict(d) + for di in d.keys(): + if di.startswith("_"): + del nd[di] + elif isinstance(d[di], Model): + nd[di] = d[di].pk + elif isinstance(d[di], int) and hasattr(obj, "get_%s_display" % di): + nd[di] = getattr(obj, "get_%s_display" % di)() + return nd + else: + raise TypeError("Unserializable object %s (%s) of type %s" % ( obj, dir(obj), type(obj))) + + def _template_renderer(template): def func_wrapper(view): def returned_wrapper(request, *args, **kwargs):
@@ -127,7 +156,7 @@ def _template_renderer(template): else: raise TypeError("Unserializable object %s of type %s" % ( obj, type(obj))) - return HttpResponse(jsonfilter(context, default=_objtojson ), + return HttpResponse(jsonfilter(context, default=objtojson ), content_type = "application/json; charset=utf-8") else: return render(request, template, context)
lib/toaster/toastergui/widgets.py +11 -5
@@ -37,6 +37,7 @@ import json import collections import operator +from toastergui.views import objtojson class ToasterTable(TemplateView): def __init__(self, *args, **kwargs):
@@ -275,19 +276,25 @@ class ToasterTable(TemplateView): for col in self.columns: field = col['field_name'] + if not field: + field = col['static_data_name'] + if not field: + raise Exception("Must supply a field_name or static_data_name for column %s.%s" % (self.__class__.__name__,col)) # Check if we need to process some static data if "static_data_name" in col and col['static_data_name']: - required_data[col['static_data_name']] = self.render_static_data(col['static_data_template'], row) + required_data["static:%s" % col['static_data_name']] = self.render_static_data(col['static_data_template'], row) # Overwrite the field_name with static_data_name # so that this can be used as the html class name col['field_name'] = col['static_data_name'] - else: + + if True: # we add the raw model data at all times model_data = row # Traverse to any foriegn key in the object hierachy for subfield in field.split("__"): - model_data = getattr(model_data, subfield) + if hasattr(model_data, subfield): + model_data = getattr(model_data, subfield) # The field could be a function on the model so check # If it is then call it if isinstance(model_data, types.MethodType):
@@ -299,8 +306,7 @@ class ToasterTable(TemplateView): except FieldError: print "Error: Requested field does not exist" - - data = json.dumps(data, indent=2, cls=DjangoJSONEncoder) + data = json.dumps(data, indent=2, default=objtojson) cache.set(cache_name, data, 60*30) return data
Signature · 1 line
1.9.1
PATCH 7/19 toaster: ToasterTables add computational fields
In reply to Alex DAMIANFrom: Alexandru DAMIAN <[email protected]>
This patch adds the ability to pass a function to be computed for generating a field value in setting up a column in ToasterTables.
Also adding "displayable" property that can be turned False for columns that are present in JSON data but are not part of the UI.
Add the "id" column by default for all rows.
--- lib/toaster/toastergui/static/js/table.js | 13 +++++++++++++ lib/toaster/toastergui/tables.py | 8 ++++++-- lib/toaster/toastergui/views.py | 3 +++ lib/toaster/toastergui/widgets.py | 19 ++++++++++++++++--- 4 files changed, 38 insertions(+), 5 deletions(-)
lib/toaster/toastergui/static/js/table.js +13 -0
@@ -110,9 +110,13 @@ function tableInit(ctx){ setupTableChrome(tableData); /* Add table data rows */ + var column_index; for (var i in tableData.rows){ + /* only display if the column is display-able */ var row = $("<tr></tr>"); + column_index = -1; for (var key_j in tableData.rows[i]){ + /* if we have a static: version of a key, prefer the static: version for rendering */ var orig_key_j = key_j;
@@ -125,6 +129,12 @@ function tableInit(ctx){ key_j = "static:" + key_j; } + /* we skip over un-displayable column entries */ + column_index += 1; + if (! tableData.columns[column_index].displayable) { + continue; + } + var td = $("<td></td>"); td.prop("class", orig_key_j); if (tableData.rows[i][key_j]){
@@ -206,6 +216,9 @@ function tableInit(ctx){ /* Add table header and column toggle menu */ for (var i in tableData.columns){ var col = tableData.columns[i]; + if (col.displayable === false) { + continue; + } var header = $("<th></th>"); header.prop("class", col.field_name);
lib/toaster/toastergui/tables.py +6 -2
@@ -79,7 +79,7 @@ class LayersTable(ToasterTable): self.add_column(title="Git repository URL", help_text="The Git repository for the layer source code", hidden=True, - static_data_name="git_url", + static_data_name="layer__vcs_url", static_data_template=git_url_template) git_dir_template = '''
@@ -328,13 +328,17 @@ class RecipesTable(ToasterTable): self.add_column(title="Revision", field_name="layer_version__get_vcs_reference") - self.add_column(title="Build", help_text="Add or delete recipes to and from your project", hideable=False, static_data_name="add-del-layers", static_data_template='{% include "recipe_btn.html" %}') + self.add_column(title="Project compatible Layer ID", + displayable = False, + field_name = "projectcompatible_layer", + computation = lambda x: (x.layer_version.get_equivalents_wpriority(Project.objects.get(pk=kwargs['pid']))[0])) + class LayerRecipesTable(RecipesTable): """ Smaller version of the Recipes table for use in layer details """
lib/toaster/toastergui/views.py +3 -0
@@ -112,6 +112,9 @@ def objtojson(obj): elif isinstance(d[di], int) and hasattr(obj, "get_%s_display" % di): nd[di] = getattr(obj, "get_%s_display" % di)() return nd + elif isinstance( obj, type(lambda x:x)): + import inspect + return inspect.getsourcelines(obj)[0] else: raise TypeError("Unserializable object %s (%s) of type %s" % ( obj, dir(obj), type(obj)))
lib/toaster/toastergui/widgets.py +16 -3
@@ -54,6 +54,13 @@ class ToasterTable(TemplateView): self.empty_state = "Sorry - no data found" self.default_orderby = "" + # add the "id" column, undisplayable, by default + self.add_column(title="Id", + displayable=False, + orderable=True, + field_name="id") + + def get(self, request, *args, **kwargs): if request.GET.get('format', None) == 'json':
@@ -142,6 +149,7 @@ class ToasterTable(TemplateView): def add_column(self, title="", help_text="", orderable=False, hideable=True, hidden=False, field_name="", filter_name=None, static_data_name=None, + displayable=True, computation=None, static_data_template=None): """Add a column to the table.
@@ -168,6 +176,8 @@ class ToasterTable(TemplateView): 'filter_name' : filter_name, 'static_data_name': static_data_name, 'static_data_template': static_data_template, + 'displayable': displayable, + 'computation': computation, }) def render_static_data(self, template, row):
@@ -289,8 +299,11 @@ class ToasterTable(TemplateView): col['field_name'] = col['static_data_name'] - if True: # we add the raw model data at all times - model_data = row + # compute the computation on the raw data if needed + model_data = row + if col['computation']: + model_data = col['computation'](row) + else: # Traverse to any foriegn key in the object hierachy for subfield in field.split("__"): if hasattr(model_data, subfield):
@@ -300,7 +313,7 @@ class ToasterTable(TemplateView): if isinstance(model_data, types.MethodType): model_data = model_data() - required_data[field] = model_data + required_data[col['field_name']] = model_data data['rows'].append(required_data)
Signature · 1 line
1.9.1
PATCH 8/19 toaster: toastertables raise errors
In reply to Alex DAMIANFrom: Alexandru DAMIAN <[email protected]>
Modifies the widget code to raise the Exceptions to the user instead of printing then to stdout - making the programming errors much more visible.
--- lib/toaster/toastergui/widgets.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-)
lib/toaster/toastergui/widgets.py +7 -5
@@ -207,7 +207,8 @@ class ToasterTable(TemplateView): try: self.filter_actions[filter_action]() except KeyError: - print "Filter and Filter action pair not found" + # pass it to the user - programming error here + raise def apply_orderby(self, orderby): # Note that django will execute this when we try to retrieve the data
@@ -217,8 +218,7 @@ class ToasterTable(TemplateView): """Creates a query based on the model's search_allowed_fields""" if not hasattr(self.queryset.model, 'search_allowed_fields'): - print "Err Search fields aren't defined in the model" - return + raise Exception("Err Search fields aren't defined in the model") search_queries = [] for st in search_term.split(" "):
@@ -228,9 +228,10 @@ class ToasterTable(TemplateView): search_queries.append(reduce(operator.or_, q_map)) search_queries = reduce(operator.and_, search_queries) - print "applied the search to the queryset" + self.queryset = self.queryset.filter(search_queries) + def get_data(self, request, **kwargs): """Returns the data for the page requested with the specified parameters applied"""
@@ -318,7 +319,8 @@ class ToasterTable(TemplateView): data['rows'].append(required_data) except FieldError: - print "Error: Requested field does not exist" + # pass it to the user - programming-error here + raise data = json.dumps(data, indent=2, default=objtojson) cache.set(cache_name, data, 60*30)
Signature · 1 line
1.9.1
PATCH 9/19 toaster: add class template view for single-object pages
In reply to Alex DAMIANFrom: Alexandru DAMIAN <[email protected]>
Adding ToasterTemplateView as prototype for all class-based views that display single objects; equivalent to ToasterTable for object lists.
--- lib/toaster/toastergui/tables.py | 12 ++++++++++-- lib/toaster/toastergui/widgets.py | 26 ++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-)
lib/toaster/toastergui/tables.py +10 -2
@@ -19,7 +19,7 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -from toastergui.widgets import ToasterTable +from toastergui.widgets import ToasterTable, ToasterTemplateView from orm.models import Recipe, ProjectLayer, Layer_Version, Machine, Project from django.db.models import Q, Max from django.conf.urls import url
@@ -142,16 +142,24 @@ class LayersTable(ToasterTable): static_data_name="add-del-layers", static_data_template='{% include "layer_btn.html" %}') -class LayerDetails(TemplateView): + + +class LayerDetails(ToasterTemplateView): def get_context_data(self, **kwargs): context = super(LayerDetails, self).get_context_data(**kwargs) + from toastergui.views import _lv_to_dict context['project'] = Project.objects.get(pk=kwargs['pid']) context['layerversion'] = Layer_Version.objects.get(pk=kwargs['layerid']) + context['layerdict'] = _lv_to_dict(context['project'], context['layerversion']) + context['layerdeps'] = {"list": [x.depends_on.get_equivalents_wpriority(context['project'])[0] for x in context['layerversion'].dependencies.all()]} context['projectlayers'] = map(lambda prjlayer: prjlayer.layercommit.id, ProjectLayer.objects.filter(project=context['project'])) + self.context_entries = ['project', 'layerversion', 'projectlayers', 'layerdict', 'layerdeps'] + return context + class MachinesTable(ToasterTable): """Table of Machines in Toaster"""
lib/toaster/toastergui/widgets.py +26 -0
@@ -325,3 +325,29 @@ class ToasterTable(TemplateView): cache.set(cache_name, data, 60*30) return data + + +class ToasterTemplateView(TemplateView): + # renders a instance in a template, or returns the context as json + # the class-equivalent of the _template_renderer decorator for views + + + def get(self, *args, **kwargs): + if self.request.GET.get('format', None) == 'json': + from django.core.urlresolvers import reverse + from django.shortcuts import HttpResponse + from views import objtojson + from toastergui.templatetags.projecttags import json as jsonfilter + + context = self.get_context_data(**kwargs) + + for x in context.keys(): + if x not in self.context_entries: + del context[x] + + context["error"] = "ok" + + return HttpResponse(jsonfilter(context, default=objtojson ), + content_type = "application/json; charset=utf-8") + + return super(ToasterTemplateView, self).get(*args, **kwargs)
Signature · 1 line
1.9.1
PATCH 10/19 toaster: eliminate duplicate querysets
In reply to Alex DAMIANFrom: Alexandru DAMIAN <[email protected]>
The Layer_Version.get_equivalents_wpriority performs the same function as Project.compatible_layerversions, but in memory and with worse performance.
Replace the code in get_equivalents_wpriority with a call to the project compatible_layerversions, which also returns a queryset instead of a list (can be used to further enhance queries)
--- lib/toaster/orm/models.py | 41 +++-------------------------------------- 1 file changed, 3 insertions(+), 38 deletions(-)
lib/toaster/orm/models.py +3 -38
@@ -20,7 +20,7 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. from django.db import models -from django.db.models import F, Q +from django.db.models import F, Q, Avg from django.utils import timezone
@@ -180,7 +180,7 @@ class Project(models.Model): queryset = queryset.filter(layer__name = layer_name) # order by layer version priority - queryset = queryset.filter(layer_source__releaselayersourcepriority__release = release).order_by("-layer_source__releaselayersourcepriority__priority") + queryset = queryset.filter(Q(layer_source=None) | Q(layer_source__releaselayersourcepriority__release = release)).select_related('layer_source', 'layer', 'up_branch').annotate(prio=Avg("layer_source__releaselayersourcepriority__priority")).order_by("-prio") return queryset
@@ -1062,42 +1062,7 @@ class Layer_Version(models.Model): return self._handle_url_path(self.layer.vcs_web_tree_base_url, '') def get_equivalents_wpriority(self, project): - """ Returns an ordered layerversion list that satisfies a LayerVersionDependency using the layer name and the current Project Releases' LayerSource priority """ - - # layers created for this project, or coming from a build inthe project - query = Q(project = project) | Q(build__project = project) - if self.up_branch is not None: - # the same up_branch name - query |= Q(up_branch__name=self.up_branch.name) - else: - # or we have a layer in the project that's similar to mine (See the layer.name constraint below) - query |= Q(projectlayer__project=project) - - candidate_layer_versions = list(Layer_Version.objects.filter(layer__name = self.layer.name).filter(query).select_related('layer_source', 'layer', 'up_branch').order_by("-id")) - - # optimization - if we have only one, we don't need no stinking sort - if len(candidate_layer_versions) == 1: - return candidate_layer_versions - -# raise Exception(candidate_layer_versions) - - release_priorities = {} - - for ls_id, prio in map(lambda x: (x.layer_source_id, x.priority), project.release.releaselayersourcepriority_set.all().order_by("-priority")): - release_priorities[ls_id] = prio - - def _get_ls_priority(ls): - # if there is no layer source, we have minus infinite priority, as we don't want this layer selected - if ls == None: - return -10000 - try: - return release_priorities[ls.id] - except IndexError: - raise Exception("Unknown %d %s" % (ls.id, release_priorities)) - - return sorted( candidate_layer_versions , - key = lambda x: _get_ls_priority(x.layer_source), - reverse = True) + return project.compatible_layerversions(layer_name = self.layer.name) def get_vcs_reference(self): if self.commit is not None and len(self.commit) > 0:
Signature · 1 line
1.9.1
PATCH 11/19 toastergui: remove xhr_datatypeahead and xhr_XXXbuild
In reply to Alex DAMIANFrom: Alexandru DAMIAN <[email protected]>
We remove the endpoints for XHR on the toastergui application.
The endpoints are now replaced with calls to the respective REST endpoints (i.e. projectlayers, projecttargets, projectmachines).
--- lib/toaster/contrib/tts/urllist.py | 2 - lib/toaster/toastergui/static/js/base.js | 6 +- lib/toaster/toastergui/static/js/importlayer.js | 6 +- lib/toaster/toastergui/static/js/layerdetails.js | 4 +- lib/toaster/toastergui/static/js/libtoaster.js | 8 +- lib/toaster/toastergui/templates/base.html | 2 - lib/toaster/toastergui/templates/layerdetails.html | 4 +- .../toastergui/templates/managed_mrb_section.html | 2 +- lib/toaster/toastergui/templates/project.html | 52 ++++-- lib/toaster/toastergui/templates/runagain.html | 2 +- lib/toaster/toastergui/templatetags/projecttags.py | 2 +- lib/toaster/toastergui/tests.py | 6 +- lib/toaster/toastergui/urls.py | 8 +- lib/toaster/toastergui/views.py | 208 ++++++--------------- lib/toaster/toastergui/widgets.py | 6 +- lib/toaster/toastermain/settings.py | 2 +- 16 files changed, 119 insertions(+), 201 deletions(-)
lib/toaster/contrib/tts/urllist.py +0 -2
@@ -40,9 +40,7 @@ URLS = [ 'toastergui/project/1/importlayer', 'toastergui/project/1/targets/', 'toastergui/project/1/machines/', -'toastergui/xhr_projectbuild/1/', 'toastergui/xhr_configvaredit/1', -'toastergui/xhr_datatypeahead/1', 'toastergui/xhr_importlayer/', 'toastergui/xhr_updatelayer/', 'toastergui/project/1/buildrequest/1',
lib/toaster/toastergui/static/js/base.js +3 -3
@@ -89,7 +89,7 @@ function basePageInit (ctx) { if (!selectedTarget) selectedTarget = { name: newBuildTargetInput.val() }; /* fire and forget */ - libtoaster.startABuild(ctx.projectBuildUrl, libtoaster.ctx.projectId, selectedTarget.name, null, null); + libtoaster.startABuild(ctx.projectBuildsUrl, libtoaster.ctx.projectId, selectedTarget.name, null, null); window.location.replace(libtoaster.ctx.projectPageUrl); });
@@ -105,13 +105,13 @@ function basePageInit (ctx) { libtoaster.ctx.projectName = selectedProject.name; libtoaster.ctx.projectId = selectedProject.id; - ctx.projectBuildUrl = selectedProject.projectBuildUrl; + ctx.projectBuildsUrl = selectedProject.projectBuildsUrl; /* we can create a target typeahead only after we have a project selected */ newBuildTargetInput.prop("disabled", false); newBuildTargetBuildBtn.prop("disabled", false); - libtoaster.makeTypeahead(newBuildTargetInput, selectedProject.xhrProjectDataTypeaheadUrl, { type : "targets" }, function(item){ + libtoaster.makeTypeahead(newBuildTargetInput, selectedProject.projectTargetsUrl, { format: "json" }, function(item){ /* successfully selected a target */ selectedTarget = item; });
lib/toaster/toastergui/static/js/importlayer.js +3 -3
@@ -18,7 +18,7 @@ function importLayerPageInit (ctx) { $("#new-project-button").hide(); - libtoaster.makeTypeahead(layerDepInput, libtoaster.ctx.xhrProjectDataTypeaheadUrl, { type : "layers", project_id: libtoaster.ctx.projectId, include_added: "true" }, function(item){ + libtoaster.makeTypeahead(layerDepInput, libtoaster.ctx.projectLayersUrl, { include_added: "true" }, function(item){ currentLayerDepSelection = item; layerDepBtn.removeAttr("disabled");
@@ -28,7 +28,7 @@ function importLayerPageInit (ctx) { /* We automatically add "openembedded-core" layer for convenience as a * dependency as pretty much all layers depend on this one */ - $.getJSON(libtoaster.ctx.xhrProjectDataTypeaheadUrl, { type : "layers", project_id: libtoaster.ctx.projectId, include_added: "true" , value: "openembedded-core" }, function(layer) { + $.getJSON(libtoaster.ctx.projectLayersUrl, { include_added: "true" , search: "openembedded-core" }, function(layer) { if (layer.list.length == 1) { currentLayerDepSelection = layer.list[0]; layerDepBtn.click();
@@ -211,7 +211,7 @@ function importLayerPageInit (ctx) { var name = $(this).val(); /* Check if the layer name exists */ - $.getJSON(libtoaster.ctx.xhrProjectDataTypeaheadUrl, { type : "layers", project_id: libtoaster.ctx.projectId, include_added: "true" , value: name }, function(layer) { + $.getJSON(libtoaster.ctx.projectLayersUrl, { include_added: "true" , search: name }, function(layer) { if (layer.list.length > 0) { for (var i in layer.list){ if (layer.list[i].name == name) {
lib/toaster/toastergui/static/js/layerdetails.js +2 -2
@@ -9,7 +9,7 @@ function layerDetailsPageInit (ctx) { var addRmLayerBtn = $("#add-remove-layer-btn"); /* setup the dependencies typeahead */ - libtoaster.makeTypeahead(layerDepInput, libtoaster.ctx.xhrProjectDataTypeaheadUrl, { type : "layers", project_id: libtoaster.ctx.projectId, include_added: "true" }, function(item){ + libtoaster.makeTypeahead(layerDepInput, libtoaster.ctx.projectLayersUrl, { include_added: "true" }, function(item){ currentLayerDepSelection = item; layerDepBtn.removeAttr("disabled");
@@ -170,7 +170,7 @@ function layerDetailsPageInit (ctx) { $(".build-target-btn").click(function(){ /* fire a build */ var target = $(this).data('target-name'); - libtoaster.startABuild(ctx.projectBuildUrl, libtoaster.ctx.projectId, target, null, null); + libtoaster.startABuild(ctx.projectBuildsUrl, libtoaster.ctx.projectId, target, null, null); window.location.replace(libtoaster.ctx.projectPageUrl); }); });
lib/toaster/toastergui/static/js/libtoaster.js +4 -4
@@ -93,7 +93,7 @@ var libtoaster = (function (){ } /* cancelABuild: - * url: xhr_projectbuild + * url: projectbuilds * builds_ids: space separated list of build request ids * onsuccess: callback for successful execution * onfail: callback for failed execution
@@ -172,15 +172,15 @@ var libtoaster = (function (){ function _getLayerDepsForProject(projectId, layerId, onSuccess, onFail){ /* Check for dependencies not in the current project */ - $.getJSON(libtoaster.ctx.xhrProjectDataTypeaheadUrl, - { type: 'layerdeps', 'value': layerId , project_id: projectId }, + $.getJSON(libtoaster.ctx.projectLayersUrl, + { format: 'json', search: layerId }, function(data) { if (data.error != "ok") { console.log(data.error); if (onFail !== undefined) onFail(data); } else { - onSuccess(data); + onSuccess(data.layerdeps); } }, function() { console.log("E: Failed to make request");
lib/toaster/toastergui/templates/base.html +0 -2
@@ -33,12 +33,10 @@ htmlUrl : "{% static 'html/' %}", projectsUrl : "{% url 'all-projects' %}", {% if project.id %} - xhrProjectDataTypeaheadUrl : "{% url 'xhr_datatypeahead' project.id %}", projectPageUrl : "{% url 'project' project.id %}", projectName : "{{project.name}}", projectId : {{project.id}}, {% else %} - xhrProjectDataTypeaheadUrl : undefined, projectPageUrl : undefined, projectName : undefined, projectId : undefined,
lib/toaster/toastergui/templates/layerdetails.html +2 -2
@@ -3,7 +3,7 @@ {% load humanize %} {% load static %} {% block localbreadcrumb %} -<li><a href="{% url 'all-layers' project.id %}">All compatible layers</a></li> +<li><a href="{% url 'projectlayers' project.id %}">All compatible layers</a></li> <li> {{layerversion.layer.name}} ({{layerversion.get_vcs_reference|truncatechars:13}}) </li>
@@ -33,7 +33,7 @@ $(document).ready(function (){ var ctx = { - projectBuildUrl : "{% url 'xhr_projectbuild' project.id %}", + projectBuildsUrl : "{% url 'projectbuilds' project.id %}", layerDetailsUrl : "{% url 'base_layerdetails' project.id %}", xhrUpdateLayerUrl : "{% url 'xhr_updatelayer' %}", layerVersion : {
lib/toaster/toastergui/templates/managed_mrb_section.html +1 -1
@@ -129,7 +129,7 @@ <div class="span4 lead">Build queued <i title="This build will start as soon as a build server is available" class="icon-question-sign get-help get-help-blue heading-help" data-toggle="tooltip"></i> </div> - <button class="btn btn-info pull-right cancel-build-btn" data-build-id="{{buildrequest.id}}" data-request-url="{% url 'xhr_projectbuild' buildrequest.project.id %}" >Cancel</button> + <button class="btn btn-info pull-right cancel-build-btn" data-build-id="{{buildrequest.id}}" data-request-url="{% url 'projectbuilds' buildrequest.project.id %}" >Cancel</button> {% elif buildrequest.state == buildrequest.REQ_CREATED %}
lib/toaster/toastergui/templates/project.html +36 -16
125 lines not on this page · raw message
lib/toaster/toastergui/templates/runagain.html +1 -1
@@ -1,6 +1,6 @@ {% load projecttags %} onclick='scheduleBuild( - {% url 'xhr_projectbuild' buildrequest.project.id as bpi %}{{bpi|json}}, + {% url 'projectbuilds' buildrequest.project.id as bpi %}{{bpi|json}}, {{buildrequest.project.name|json}}, {% url 'project' buildrequest.project.id as bpurl %}{{bpurl|json}}, {{buildrequest.brtarget_set.all|get_tasks|json}})
lib/toaster/toastergui/templatetags/projecttags.py +1 -1
@@ -55,7 +55,7 @@ def json(value, default = None): # JSON spec says that "\/" is functionally identical to "/" to allow for HTML-tag embedding in JSON strings # unfortunately, I can't find any option in the json module to turn on forward-slash escaping, so we do # it manually here - return mark_safe(JsonLib.dumps(value, default = default, ensure_ascii=False).replace('</', '<\\/')) + return mark_safe(JsonLib.dumps(value, indent=2, default = default, ensure_ascii=False).replace('</', '<\\/')) @register.assignment_tag def query(qs, **kwargs):
lib/toaster/toastergui/tests.py +3 -3
@@ -38,9 +38,9 @@ class AllProjectsViewTestCase(ProvisionedProjectTestCase): self.assertTrue(AllProjectsViewTestCase.TEST_PROJECT_NAME in map(lambda x: x["name"], data["list"])) self.assertTrue("id" in data["list"][0]) - self.assertTrue("xhrProjectDataTypeaheadUrl" in data["list"][0]) + self.assertTrue("projectLayersUrl" in data["list"][0]) self.assertTrue("projectPageUrl" in data["list"][0]) - self.assertTrue("projectBuildUrl" in data["list"][0]) + self.assertTrue("projectBuildsUrl" in data["list"][0]) class ProvisionedLayersProjectTestCase(ProvisionedProjectTestCase): LAYER_NAME = "base-layer"
@@ -59,7 +59,7 @@ class XHRDataTypeAheadTestCase(ProvisionedLayersProjectTestCase): self.assertTrue(self.lv in self.project.compatible_layerversions()) def test_xhr_datatypeahead_layer(self): - response = self.client.get(reverse('xhr_datatypeahead', args=(self.project.id,)), {"type": "layers"}) + response = self.client.get(reverse('xhr_datatypeahead', args=(self.project.id,)), {"type": "layerdeps"}) self.assertEqual(response.status_code, 200) self.assertTrue(response['Content-Type'].startswith('application/json'))
lib/toaster/toastergui/urls.py +3 -5
@@ -91,19 +91,19 @@ urlpatterns = patterns('toastergui.views', tables.MachinesTable.as_view(template_name="generic-toastertable-page.html"), { 'table_name': tables.MachinesTable.__name__.lower(), 'title' : 'All compatible machines' }, - name="all-machines"), + name="projectmachines"), url(r'^project/(?P<pid>\d+)/recipes/$', tables.RecipesTable.as_view(template_name="generic-toastertable-page.html"), { 'table_name': tables.RecipesTable.__name__.lower(), 'title' : 'All compatible recipes' }, - name="all-targets"), + name="projecttargets"), url(r'^project/(?P<pid>\d+)/layers/$', tables.LayersTable.as_view(template_name="generic-toastertable-page.html"), { 'table_name': tables.LayersTable.__name__.lower(), 'title' : 'All compatible layers' }, - name="all-layers"), + name="projectlayers"), url(r'^project/(?P<pid>\d+)/layer/(?P<layerid>\d+)$', tables.LayerDetails.as_view(template_name='layerdetails.html'),
@@ -121,10 +121,8 @@ urlpatterns = patterns('toastergui.views', 'title' : 'All machines in layer' }, name=tables.LayerMachinesTable.__name__.lower()), - url(r'^xhr_projectbuild/(?P<pid>\d+)$', 'xhr_projectbuild', name='xhr_projectbuild'), url(r'^xhr_configvaredit/(?P<pid>\d+)$', 'xhr_configvaredit', name='xhr_configvaredit'), - url(r'^xhr_datatypeahead/(?P<pid>\d+)$', 'xhr_datatypeahead', name='xhr_datatypeahead'), url(r'^xhr_importlayer/$', 'xhr_importlayer', name='xhr_importlayer'), url(r'^xhr_updatelayer/$', 'xhr_updatelayer', name='xhr_updatelayer'),
lib/toaster/toastergui/views.py +56 -152
276 lines not on this page · raw message
lib/toaster/toastergui/widgets.py +3 -3
@@ -253,8 +253,8 @@ class ToasterTable(TemplateView): data = cache.get(cache_name) - if data: - return data + #if data: + # return data self.setup_columns(**kwargs)
@@ -277,9 +277,9 @@ class ToasterTable(TemplateView): 'default_orderby' : self.default_orderby, 'columns' : self.columns, 'rows' : [], + 'error' : "ok", } - try: for row in page.object_list: #Use collection to maintain the order
lib/toaster/toastermain/settings.py +1 -1
@@ -223,7 +223,7 @@ CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache', 'LOCATION': '/tmp/django-default-cache', - 'TIMEOUT': 5, + 'TIMEOUT': 1, } }
Signature · 1 line
1.9.1
PATCH 12/19 toastergui: remove xhr_datatypeahaed layerdeps call
In reply to Alex DAMIANFrom: Alexandru DAMIAN <[email protected]>
This patch removes the url-constructing calls to get the layer details in favor of embedding the look-up URL in the JSON data on the layer list page.
This allows further removal of the XHR-specific code for layer dependencies in favor of REST calls to layer details data.
--- lib/toaster/toastergui/static/js/importlayer.js | 8 +-- lib/toaster/toastergui/static/js/layerdetails.js | 2 +- lib/toaster/toastergui/static/js/libtoaster.js | 9 ++-- lib/toaster/toastergui/static/js/projectapp.js | 61 ++++++++++++++++------ lib/toaster/toastergui/tables.py | 23 +++++--- lib/toaster/toastergui/templates/importlayer.html | 1 - lib/toaster/toastergui/templates/layerdetails.html | 1 - lib/toaster/toastergui/templates/project.html | 1 - lib/toaster/toastergui/urls.py | 3 -- lib/toaster/toastergui/views.py | 11 +--- 10 files changed, 73 insertions(+), 47 deletions(-)
lib/toaster/toastergui/static/js/importlayer.js +4 -4
@@ -48,7 +48,7 @@ function importLayerPageInit (ctx) { newLayerDep.children("span").tooltip(); var link = newLayerDep.children("a"); - link.attr("href", ctx.layerDetailsUrl+String(currentLayerDepSelection.id)); + link.attr("href", currentLayerDepSelection.layerDetailsUrl); link.text(currentLayerDepSelection.name); link.tooltip({title: currentLayerDepSelection.tooltip, placement: "right"});
@@ -63,11 +63,11 @@ function importLayerPageInit (ctx) { $("#layer-deps-list").append(newLayerDep); - libtoaster.getLayerDepsForProject(libtoaster.ctx.projectId, currentLayerDepSelection.id, function (data){ + libtoaster.getLayerDepsForProject(currentLayerDepSelection.layerDetailsUrl, function (data){ /* These are the dependencies of the layer added as a dependency */ if (data.list.length > 0) { - currentLayerDepSelection.url = ctx.layerDetailsUrl+currentLayerDepSelection.id; - layerDeps[currentLayerDepSelection.id].deps = data.list + currentLayerDepSelection.url = currentLayerDepSelection.layerDetailsUrl; + layerDeps[currentLayerDepSelection.id].deps = data.list; } /* Clear the current selection */
lib/toaster/toastergui/static/js/layerdetails.js +1 -1
@@ -65,7 +65,7 @@ function layerDetailsPageInit (ctx) { newLayerDep.children("span").tooltip(); var link = newLayerDep.children("a"); - link.attr("href", ctx.layerDetailsUrl+String(currentLayerDepSelection.id)); + link.attr("href", currentLayerDepSelection.layerDetailsUrl); link.text(currentLayerDepSelection.name); link.tooltip({title: currentLayerDepSelection.tooltip, placement: "right"});
lib/toaster/toastergui/static/js/libtoaster.js +4 -5
@@ -170,10 +170,10 @@ var libtoaster = (function (){ }); } - function _getLayerDepsForProject(projectId, layerId, onSuccess, onFail){ + function _getLayerDepsForProject(url, onSuccess, onFail){ /* Check for dependencies not in the current project */ - $.getJSON(libtoaster.ctx.projectLayersUrl, - { format: 'json', search: layerId }, + $.getJSON(url, + { format: 'json' }, function(data) { if (data.error != "ok") { console.log(data.error);
@@ -225,8 +225,7 @@ var libtoaster = (function (){ function _addRmLayer(layerObj, add, doneCb){ if (add === true) { /* If adding get the deps for this layer */ - libtoaster.getLayerDepsForProject(libtoaster.ctx.projectId, - layerObj.id, + libtoaster.getLayerDepsForProject(layerObj.url, function (layers) { /* got result for dependencies */
lib/toaster/toastergui/static/js/projectapp.js +46 -15
142 lines not on this page · raw message
lib/toaster/toastergui/tables.py +17 -6
@@ -23,6 +23,7 @@ from toastergui.widgets import ToasterTable, ToasterTemplateView from orm.models import Recipe, ProjectLayer, Layer_Version, Machine, Project from django.db.models import Q, Max from django.conf.urls import url +from django.core.urlresolvers import reverse from django.views.generic import TemplateView class LayersTable(ToasterTable):
@@ -35,9 +36,9 @@ class LayersTable(ToasterTable): def get_context_data(self, **kwargs): context = super(LayersTable, self).get_context_data(**kwargs) - context['project'] = Project.objects.get(pk=kwargs['pid']) - - context['projectlayers'] = map(lambda prjlayer: prjlayer.layercommit.id, ProjectLayer.objects.filter(project=context['project'])) + project = Project.objects.get(pk=kwargs['pid']) + context['project'] = project + context['projectlayers'] = map(lambda prjlayer: prjlayer.layercommit.id, ProjectLayer.objects.filter(project=project)) return context
@@ -142,6 +143,13 @@ class LayersTable(ToasterTable): static_data_name="add-del-layers", static_data_template='{% include "layer_btn.html" %}') + project = Project.objects.get(pk=kwargs['pid']) + self.add_column(title="LayerDetailsUrl", + displayable = False, + field_name="layerDetailsUrl", + computation = lambda x: reverse('layerdetails', args=(project.id, x.id))) + + class LayerDetails(ToasterTemplateView):
@@ -152,7 +160,8 @@ class LayerDetails(ToasterTemplateView): context['project'] = Project.objects.get(pk=kwargs['pid']) context['layerversion'] = Layer_Version.objects.get(pk=kwargs['layerid']) context['layerdict'] = _lv_to_dict(context['project'], context['layerversion']) - context['layerdeps'] = {"list": [x.depends_on.get_equivalents_wpriority(context['project'])[0] for x in context['layerversion'].dependencies.all()]} + context['layerdeps'] = {"list": [ + [{"id": y.id, "name": y.layer.name} for y in x.depends_on.get_equivalents_wpriority(context['project'])][0] for x in context['layerversion'].dependencies.all()]} context['projectlayers'] = map(lambda prjlayer: prjlayer.layercommit.id, ProjectLayer.objects.filter(project=context['project'])) self.context_entries = ['project', 'layerversion', 'projectlayers', 'layerdict', 'layerdeps']
@@ -265,9 +274,10 @@ class RecipesTable(ToasterTable): self.default_orderby = "name" def get_context_data(self, **kwargs): + project = Project.objects.get(pk=kwargs['pid']) context = super(RecipesTable, self).get_context_data(**kwargs) - context['project'] = Project.objects.get(pk=kwargs['pid']) + context['project'] = project context['projectlayers'] = map(lambda prjlayer: prjlayer.layercommit.id, ProjectLayer.objects.filter(project=context['project']))
@@ -342,10 +352,11 @@ class RecipesTable(ToasterTable): static_data_name="add-del-layers", static_data_template='{% include "recipe_btn.html" %}') + project = Project.objects.get(pk=kwargs['pid']) self.add_column(title="Project compatible Layer ID", displayable = False, field_name = "projectcompatible_layer", - computation = lambda x: (x.layer_version.get_equivalents_wpriority(Project.objects.get(pk=kwargs['pid']))[0])) + computation = lambda x: (x.layer_version.get_equivalents_wpriority(project)[0])) class LayerRecipesTable(RecipesTable): """ Smaller version of the Recipes table for use in layer details """
lib/toaster/toastergui/templates/importlayer.html +0 -1
@@ -14,7 +14,6 @@ <script> $(document).ready(function (){ var ctx = { - layerDetailsUrl : "{% url 'base_layerdetails' project.id %}", xhrImportLayerUrl : "{% url 'xhr_importlayer' %}", };
lib/toaster/toastergui/templates/layerdetails.html +0 -1
@@ -34,7 +34,6 @@ $(document).ready(function (){ var ctx = { projectBuildsUrl : "{% url 'projectbuilds' project.id %}", - layerDetailsUrl : "{% url 'base_layerdetails' project.id %}", xhrUpdateLayerUrl : "{% url 'xhr_updatelayer' %}", layerVersion : { name : "{{layerversion.layer.name}}",
lib/toaster/toastergui/templates/project.html +0 -1
@@ -455,7 +455,6 @@ angular.element(document).ready(function() { scope.urls.targets = "{% url 'projecttargets' project.id %}"; scope.urls.machines = "{% url 'projectmachines' project.id %}"; scope.urls.importlayer = "{% url 'importlayer' project.id %}"; - scope.urls.layer = "{% url 'base_layerdetails' project.id %}"; scope.project = {{prj|json}}; scope.builds = {{builds|json}}; scope.layers = {{layers|json}};
lib/toaster/toastergui/urls.py +0 -3
@@ -80,12 +80,9 @@ urlpatterns = patterns('toastergui.views', url(r'^project/(?P<pid>\d+)/configuration$', 'projectconf', name='projectconf'), url(r'^project/(?P<pid>\d+)/builds/$', 'projectbuilds', name='projectbuilds'), - url(r'^project/(?P<pid>\d+)/layer/$', lambda x,pid: HttpResponseBadRequest(), name='base_layerdetails'), - # the import layer is a project-specific functionality; url(r'^project/(?P<pid>\d+)/importlayer$', 'importlayer', name='importlayer'), - # the table pages that have been converted to ToasterTable widget url(r'^project/(?P<pid>\d+)/machines/$', tables.MachinesTable.as_view(template_name="generic-toastertable-page.html"),
lib/toaster/toastergui/views.py +1 -10
@@ -2238,7 +2238,7 @@ if toastermain.settings.MANAGED: if request.method == "POST": # add layers if 'layerAdd' in request.POST: - for lc in Layer_Version.objects.filter(pk__in=request.POST['layerAdd'].split(",")): + for lc in Layer_Version.objects.filter(pk__in=[i for i in request.POST['layerAdd'].split(",") if len(i) > 0]): ProjectLayer.objects.get_or_create(project = prj, layercommit = lc) # remove layers
@@ -2328,15 +2328,6 @@ if toastermain.settings.MANAGED: try: prj = Project.objects.get(pk = pid) - # returns layer dependencies for a layer, excluding current project layers - if request.GET.get('type', None) == "layerdeps": - queryset = prj.compatible_layerversions().exclude(pk__in = [x.id for x in prj.projectlayer_equivalent_set()]).filter( - layer__name__in = [ x.depends_on.layer.name for x in LayerVersionDependency.objects.filter(layer_version_id = request.GET.get('search', None))]) - - final_list = set([x.get_equivalents_wpriority(prj)[0] for x in queryset]) - - return HttpResponse(jsonfilter( { "error":"ok", "list" : map( _lv_to_dict(prj), sorted(final_list, key = lambda x: x.layer.name)) }), content_type = "application/json") - # returns layer versions that would be deleted on the new release__pk if request.GET.get('type', None) == "versionlayers":
Signature · 1 line
1.9.1
PATCH 13/19 toaster: fixes after refactoring
In reply to Alex DAMIANFrom: Alexandru DAMIAN <[email protected]>
This patch fixes issues brought in by refactoring:
* the New Build button is working with pre-set projects
* the xhr_datatypeahead is exposed for calls that are not mapable to the REST objects
* a new table returing recipes provided by layers currently selected in the project is used to provide recipe suggestions
* the field names in json are switched from "list" to "rows" as to maintain consistency with the ToasterTables
* the "value" field in xhr_ calls is now named "search" to maintain consistency
--- lib/toaster/orm/models.py | 5 +- lib/toaster/toastergui/static/js/base.js | 104 +++++++++++++++---------- lib/toaster/toastergui/static/js/libtoaster.js | 2 +- lib/toaster/toastergui/static/js/projectapp.js | 24 ++++-- lib/toaster/toastergui/tables.py | 10 ++- lib/toaster/toastergui/templates/base.html | 7 +- lib/toaster/toastergui/templates/project.html | 3 +- lib/toaster/toastergui/urls.py | 8 ++ lib/toaster/toastergui/views.py | 17 ++-- 9 files changed, 116 insertions(+), 64 deletions(-)
lib/toaster/orm/models.py +2 -3
@@ -180,13 +180,12 @@ class Project(models.Model): queryset = queryset.filter(layer__name = layer_name) # order by layer version priority - queryset = queryset.filter(Q(layer_source=None) | Q(layer_source__releaselayersourcepriority__release = release)).select_related('layer_source', 'layer', 'up_branch').annotate(prio=Avg("layer_source__releaselayersourcepriority__priority")).order_by("-prio") + queryset = queryset.filter(Q(layer_source=None) | Q(layer_source__releaselayersourcepriority__release = release)).select_related('layer_source', 'layer', 'up_branch', "layer_source__releaselayersourcepriority__priority").order_by("-layer_source__releaselayersourcepriority__priority") return queryset - # returns a set of layer-equivalent set of layers already in project def projectlayer_equivalent_set(self): - return [j for i in [x.layercommit.get_equivalents_wpriority(self) for x in self.projectlayer_set.all().select_related("up_branch")] for j in i] + return self.compatible_layerversions().filter(layer__name__in = [x.layercommit.layer.name for x in self.projectlayer_set.all()]).select_related("up_branch") def schedule_build(self): from bldcontrol.models import BuildRequest, BRTarget, BRLayer, BRVariable, BRBitbake
lib/toaster/toastergui/static/js/base.js +61 -43
198 lines not on this page · raw message
lib/toaster/toastergui/static/js/libtoaster.js +1 -1
@@ -25,7 +25,7 @@ var libtoaster = (function (){ return; } - return process (data.list); + return process (data.rows); }); }, updater: function(item) {
lib/toaster/toastergui/static/js/projectapp.js +16 -8
@@ -217,13 +217,13 @@ projectApp.controller('prjCtrl', function($scope, $modal, $http, $interval, $loc $scope.getAutocompleteSuggestions = function(type, currentValue) { var deffered = $q.defer(); - $http({method:"GET", url: $scope.urls.xhr_datatypeahead, params : { type: type, value: currentValue}}) + $http({method:"GET", url: $scope.urls.xhr_datatypeahead, params : { type: type, search: currentValue}}) .success(function (_data) { if (_data.error != "ok") { console.warn(_data.error); deffered.reject(_data.error); } - deffered.resolve(_data.list); + deffered.resolve(_data.rows); }); return deffered.promise;
@@ -534,8 +534,17 @@ projectApp.controller('prjCtrl', function($scope, $modal, $http, $interval, $loc if (_data.error != "ok") { console.warn(_data.error); } else { - console.log("got layer deps", _data.layerdeps.list); - if (_data.layerdeps.list.length > 0) { + /* filter out layers that are already in the project */ + var filtered_list = []; + var projectlayers_ids = $scope.layers.map(function (e) { return e.id }); + for (var i = 0; i < _data.layerdeps.list.length; i++) { + if (projectlayers_ids.indexOf(_data.layerdeps.list[i].id) == -1) { + filtered_list.push( _data.layerdeps.list[i]); + } + } + + _data.layerdeps.list = filtered_list; + if (_data.layerdeps.list.length > 0) { // activate modal console.log("listing modals"); var modalInstance = $modal.open({
@@ -575,7 +584,6 @@ projectApp.controller('prjCtrl', function($scope, $modal, $http, $interval, $loc console.log("built modal instance", modalInstance); modalInstance.result.then(function (selectedArray) { - console.log("layer to add", $scope.layerToAdd) selectedArray.push($scope.layerToAdd.id); console.warn("TRC6: selected", selectedArray);
@@ -634,13 +642,13 @@ projectApp.controller('prjCtrl', function($scope, $modal, $http, $interval, $loc $scope.testProjectSettingsChange = function(elementid) { if (elementid != '#change-project-version') throw "Not implemented"; - $http({method:"GET", url: $scope.urls.xhr_datatypeahead, params : { type: "versionlayers", value: $scope.projectVersion }}). + $http({method:"GET", url: $scope.urls.xhr_datatypeahead, params : { type: "versionlayers", search: $scope.projectVersion }}). success(function (_data) { if (_data.error != "ok") { alert (_data.error); } else { - if (_data.list.length > 0) { + if (_data.rows.length > 0) { // activate modal var modalInstance = $modal.open({ templateUrl: 'change_version_modal',
@@ -660,7 +668,7 @@ projectApp.controller('prjCtrl', function($scope, $modal, $http, $interval, $loc }, resolve: { items: function () { - return _data.list; + return _data.rows; }, releaseName: function () { return $scope.releases.filter(function (e) { if (e.id == $scope.projectVersion) return e;})[0].name;
lib/toaster/toastergui/tables.py +9 -1
@@ -287,7 +287,7 @@ class RecipesTable(ToasterTable): def setup_queryset(self, *args, **kwargs): prj = Project.objects.get(pk = kwargs['pid']) - self.queryset = Recipe.objects.filter(Q(layer_version__up_branch__name= prj.release.name) | Q(layer_version__build__in = prj.build_set.all())).filter(name__regex=r'.{1,}.*') + self.queryset = Recipe.objects.filter(layer_version__in = prj.compatible_layerversions()) search_maxids = map(lambda i: i[0], list(self.queryset.values('name').distinct().annotate(max_id=Max('id')).values_list('max_id')))
@@ -392,3 +392,11 @@ class LayerRecipesTable(RecipesTable): self.add_column(title="Build recipe", static_data_name="add-del-layers", static_data_template=build_recipe_template) + +class ProjectLayersRecipesTable(RecipesTable): + """ Table that lists only recipes available for layers added to the project """ + + def setup_queryset(self, *args, **kwargs): + super(ProjectLayersRecipesTable, self).setup_queryset(*args, **kwargs) + prj = Project.objects.get(pk = kwargs['pid']) + self.queryset = self.queryset.filter(layer_version__in = prj.projectlayer_equivalent_set())
lib/toaster/toastergui/templates/base.html +5 -2
@@ -1,5 +1,6 @@ <!DOCTYPE html> {% load static %} +{% load projecttags %} <html lang="en"> <head> <title>{% if objectname %} {{objectname|title}} - {% endif %}Toaster</title>
@@ -33,8 +34,10 @@ htmlUrl : "{% static 'html/' %}", projectsUrl : "{% url 'all-projects' %}", {% if project.id %} - projectPageUrl : "{% url 'project' project.id %}", - projectName : "{{project.name}}", + projectPageUrl : {% url 'project' project.id as purl%}{{purl|json}}, + projectName : {{project.name|json}}, + projectTargetsUrl: {% url 'projectavailabletargets' project.id as paturl%}{{paturl|json}}, + projectBuildsUrl: {% url 'projectbuilds' project.id as pburl %}{{pburl|json}}, projectId : {{project.id}}, {% else %} projectPageUrl : undefined,
lib/toaster/toastergui/templates/project.html +2 -1
@@ -452,9 +452,10 @@ angular.element(document).ready(function() { scope.urls.xhr_build = "{% url 'projectbuilds' project.id %}"; scope.urls.xhr_edit = "{% url 'project' project.id %}?format=json"; scope.urls.layers = "{% url 'projectlayers' project.id %}"; - scope.urls.targets = "{% url 'projecttargets' project.id %}"; + scope.urls.targets = "{% url 'projectavailabletargets' project.id %}"; scope.urls.machines = "{% url 'projectmachines' project.id %}"; scope.urls.importlayer = "{% url 'importlayer' project.id %}"; + scope.urls.xhr_datatypeahead = {% url 'xhr_datatypeahead' project.id as xhrdta %}{{xhrdta|json}}; scope.project = {{prj|json}}; scope.builds = {{builds|json}}; scope.layers = {{layers|json}};
lib/toaster/toastergui/urls.py +8 -0
@@ -96,6 +96,12 @@ urlpatterns = patterns('toastergui.views', 'title' : 'All compatible recipes' }, name="projecttargets"), + url(r'^project/(?P<pid>\d+)/availablerecipes/$', + tables.ProjectLayersRecipesTable.as_view(template_name="generic-toastertable-page.html"), + { 'table_name': tables.ProjectLayersRecipesTable.__name__.lower(), + 'title' : 'Recipes available for layers in the current project' }, + name="projectavailabletargets"), + url(r'^project/(?P<pid>\d+)/layers/$', tables.LayersTable.as_view(template_name="generic-toastertable-page.html"), { 'table_name': tables.LayersTable.__name__.lower(),
@@ -118,6 +124,8 @@ urlpatterns = patterns('toastergui.views', 'title' : 'All machines in layer' }, name=tables.LayerMachinesTable.__name__.lower()), + + url(r'^xhr_datatypeahead/(?P<pid>\d+)$', 'xhr_datatypeahead', name='xhr_datatypeahead'), url(r'^xhr_configvaredit/(?P<pid>\d+)$', 'xhr_configvaredit', name='xhr_configvaredit'), url(r'^xhr_importlayer/$', 'xhr_importlayer', name='xhr_importlayer'),
lib/toaster/toastergui/views.py +12 -5
@@ -129,9 +129,9 @@ def _template_renderer(template): if request.GET.get('format', None) == 'json': # objects is a special keyword - it's a Page, but we need the actual objects here - # in XHR, the objects come in the "list" property + # in XHR, the objects come in the "rows" property if "objects" in context: - context["list"] = context["objects"].object_list + context["rows"] = context["objects"].object_list del context["objects"] # we're about to return; to keep up with the XHR API, we set the error to OK
@@ -2340,7 +2340,7 @@ if toastermain.settings.MANAGED: retval.append(i) return HttpResponse(jsonfilter( {"error":"ok", - "list" : map( _lv_to_dict(prj), map(lambda x: x.layercommit, retval )) + "rows" : map( _lv_to_dict(prj), map(lambda x: x.layercommit, retval )) }), content_type = "application/json")
@@ -2358,7 +2358,7 @@ if toastermain.settings.MANAGED: # and show only the selected layers for this project final_list = set([x.get_equivalents_wpriority(prj)[0] for x in queryset_all]) - return HttpResponse(jsonfilter( { "error":"ok", "list" : map( _lv_to_dict(prj), final_list) }), content_type = "application/json") + return HttpResponse(jsonfilter( { "error":"ok", "rows" : map( _lv_to_dict(prj), final_list) }), content_type = "application/json") raise Exception("Unknown request! " + request.GET.get('type', "No parameter supplied"))
@@ -2845,7 +2845,7 @@ if toastermain.settings.MANAGED: p.projectPageUrl = reverse('project', args=(p.id,)) p.projectLayersUrl = reverse('projectlayers', args=(p.id,)) p.projectBuildsUrl = reverse('projectbuilds', args=(p.id,)) - p.projectTargetsUrl = reverse('projecttargets', args=(p.id,)) + p.projectTargetsUrl = reverse('projectavailabletargets', args=(p.id,)) # build view-specific information; this is rendered specifically in the builds page, at the top of the page (i.e. Recent builds) build_mru = _managed_get_latest_builds()
@@ -3144,6 +3144,13 @@ else: def project(request, pid): return {} + from django.views.decorators.csrf import csrf_exempt + @csrf_exempt + @_template_renderer('landing_not_managed.html') + def xhr_datatypeahead(request, pid): + return {} + + @_template_renderer('landing_not_managed.html') def xhr_configvaredit(request, pid): return {}
Signature · 1 line
1.9.1
PATCH 14/19 toaster: Code cleanup: bashisms
In reply to Alex DAMIANFrom: Ed Bartosh <[email protected]>
Fixed the following bashisms:
replaced echo -e -> printf
removed 'function' from function definitions
replaced $(< ${file}) -> cat ${file}
--- bin/toaster | 55 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 28 insertions(+), 27 deletions(-)
bin/toaster +28 -27
@@ -28,23 +28,24 @@ # Helper function to kill a background toaster development server -function webserverKillAll() +webserverKillAll() { local pidfile for pidfile in ${BUILDDIR}/.toastermain.pid; do if [ -f ${pidfile} ]; then - while kill -0 $(< ${pidfile}) 2>/dev/null; do - kill -SIGTERM -$(< ${pidfile}) 2>/dev/null - sleep 1 - # Kill processes if they are still running - may happen in interactive shells - pkill -U $UID -f "python.*manage.py runserver" - done; - rm ${pidfile} + pid=`cat ${pidfile}` + while kill -0 $pid 2>/dev/null; do + kill -SIGTERM -$pid 2>/dev/null + sleep 1 + # Kill processes if they are still running - may happen in interactive shells + ps fux | grep "python.*manage.py runserver" | awk '{print $2}' | xargs kill + done + rm ${pidfile} fi done } -function webserverStartAll() +webserverStartAll() { # do not start if toastermain points to a valid process if ! cat "${BUILDDIR}/.toastermain.pid" 2>/dev/null | xargs -I{} kill -0 {} ; then
@@ -62,7 +63,7 @@ function webserverStartAll() if [ $retval -eq 1 ]; then echo "Failed db sync, stopping system start" 1>&2 elif [ $retval -eq 2 ]; then - echo -e "\nError on migration, trying to recover... \n" + printf "\nError on migration, trying to recover... \n" python $BBBASEDIR/lib/toaster/manage.py migrate orm 0001_initial --fake retval=0 python $BBBASEDIR/lib/toaster/manage.py migrate orm || retval=1
@@ -87,7 +88,7 @@ function webserverStartAll() # Helper functions to add a special configuration file -function addtoConfiguration() +addtoConfiguration() { file=$1 shift
@@ -98,13 +99,13 @@ function addtoConfiguration() INSTOPSYSTEM=0 # define the stop command -function stop_system() +stop_system() { # prevent reentry if [ $INSTOPSYSTEM -eq 1 ]; then return; fi INSTOPSYSTEM=1 if [ -f ${BUILDDIR}/.toasterui.pid ]; then - kill $(< ${BUILDDIR}/.toasterui.pid ) 2>/dev/null + kill `cat ${BUILDDIR}/.toasterui.pid` 2>/dev/null rm ${BUILDDIR}/.toasterui.pid fi BBSERVER=0.0.0.0:-1 bitbake -m
@@ -117,12 +118,12 @@ function stop_system() INSTOPSYSTEM=0 } -function check_pidbyfile() { - [ -e $1 ] && kill -0 $(< $1) 2>/dev/null +check_pidbyfile() { + [ -e $1 ] && kill -0 `cat $1` 2>/dev/null } -function notify_chldexit() { +notify_chldexit() { if [ $NOTOASTERUI -eq 0 ]; then check_pidbyfile ${BUILDDIR}/.toasterui.pid && return stop_system
@@ -130,16 +131,16 @@ function notify_chldexit() { } -function verify_prereq() { +verify_prereq() { # Verify prerequisites if ! echo "import django; print (1,) == django.VERSION[0:1] and django.VERSION[1:2][0] in (6,)" | python 2>/dev/null | grep True >/dev/null; then - echo -e "This program needs Django 1.6. Please install with\n\npip install django==1.6\n" + printf "This program needs Django 1.6. Please install with\n\npip install django==1.6\n" return 2 fi if ! echo "import south; print reduce(lambda x, y: 2 if x==2 else 0 if x == 0 else y, map(lambda x: 1+cmp(x[1]-x[0],0), zip([0,8,4], map(int,south.__version__.split(\".\"))))) > 0" | python 2>/dev/null | grep True >/dev/null; then - echo -e "This program needs South 0.8.4. Please install with\n\npip install south==0.8.4\n" + printf "This program needs South 0.8.4. Please install with\n\npip install south==0.8.4\n" return 2 fi return 0
@@ -182,42 +183,42 @@ if [ `basename \"$0\"` = `basename \"${SRCFILE}\"` ]; then # Start just the web server, point the web browser to the interface, and start any Django services. if ! verify_prereq; then - echo -e "Error: Could not verify that the needed dependencies are installed. Please use virtualenv and pip to install dependencies listed in toaster-requirements.txt" 1>&2 + echo "Error: Could not verify that the needed dependencies are installed. Please use virtualenv and pip to install dependencies listed in toaster-requirements.txt" 1>&2 exit 1 fi if [ -n "$BUILDDIR" ]; then - echo -e "Error: It looks like you sourced oe-init-build-env. Toaster cannot start in build mode from an oe-core build environment.\n You should be starting Toaster from a new terminal window." 1>&2 + printf "Error: It looks like you sourced oe-init-build-env. Toaster cannot start in build mode from an oe-core build environment.\n You should be starting Toaster from a new terminal window." 1>&2 exit 1 fi # Define a fake builddir where only the pid files are actually created. No real builds will take place here. BUILDDIR=/tmp/toaster_$$ if [ -d "$BUILDDIR" ]; then - echo -e "Previous toaster run directory $BUILDDIR found, cowardly refusing to start. Please remove the directory when that toaster instance is over" 2>&1 + echo "Previous toaster run directory $BUILDDIR found, cowardly refusing to start. Please remove the directory when that toaster instance is over" 2>&1 exit 1 fi mkdir -p "$BUILDDIR" RUNNING=1 - function trap_ctrlc() { + trap_ctrlc() { echo "** Stopping system" webserverKillAll RUNNING=0 } - function do_cleanup() { + do_cleanup() { find "$BUILDDIR" -type f | xargs rm rmdir "$BUILDDIR" } - function cleanup() { + cleanup() { if grep -ir error "$BUILDDIR" >/dev/null; then if grep -irn "That port is already in use" "$BUILDDIR"; then echo "You can use the \"webport=PORTNUMBER\" parameter to start Toaster on a different port (port $WEB_PORT is already in use)" do_cleanup else - echo -e "\nErrors found in the Toaster log files present in '$BUILDDIR'. Directory will not be cleaned.\n Please review the errors and notify [email protected] or submit a bug https://bugzilla.yoctoproject.org/enter_bug.cgi?product=Toaster" + printf "\nErrors found in the Toaster log files present in '$BUILDDIR'. Directory will not be cleaned.\n Please review the errors and notify [email protected] or submit a bug https://bugzilla.yoctoproject.org/enter_bug.cgi?product=Toaster" fi else echo "No errors found, removing the run directory '$BUILDDIR'"
@@ -248,7 +249,7 @@ fi if ! verify_prereq; then - echo -e "Error: Could not verify that the needed dependencies are installed. Please use virtualenv and pip to install dependencies listed in toaster-requirements.txt" 1>&2 + echo "Error: Could not verify that the needed dependencies are installed. Please use virtualenv and pip to install dependencies listed in toaster-requirements.txt" 1>&2 return 1 fi
Signature · 1 line
1.9.1
PATCH 15/19 toaster: Make toaster script to work in dash
In reply to Alex DAMIANFrom: Ed Bartosh <[email protected]>
Made it working in dash. Note, that due to dash limitations script will not work if sourced.
--- bin/toaster | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-)
bin/toaster +18 -9
@@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/sh # (c) 2013 Intel Corp. # This program is free software; you can redistribute it and/or modify
@@ -54,7 +54,7 @@ webserverStartAll() fi retval=0 - if [ "$TOASTER_MANAGED" '==' '1' ]; then + if [ "$TOASTER_MANAGED" '=' '1' ]; then python $BBBASEDIR/lib/toaster/manage.py syncdb || retval=1 else python $BBBASEDIR/lib/toaster/manage.py syncdb --noinput || retval=1
@@ -68,7 +68,7 @@ webserverStartAll() retval=0 python $BBBASEDIR/lib/toaster/manage.py migrate orm || retval=1 fi - if [ "$TOASTER_MANAGED" '==' '1' ]; then + if [ "$TOASTER_MANAGED" = '1' ]; then python $BBBASEDIR/lib/toaster/manage.py migrate bldcontrol || retval=1 python $BBBASEDIR/lib/toaster/manage.py checksettings --traceback || retval=1 fi
@@ -148,7 +148,16 @@ verify_prereq() { # read command line parameters -BBBASEDIR=`dirname ${BASH_SOURCE:-${(%):-%x}}`/.. +if [ -n "$BASH_SOURCE" ] ; then + TOASTER=${BASH_SOURCE} +elif [ -n "$ZSH_NAME" ] ; then + TOASTER=${(%):-%x} +else + TOASTER=$0 +fi + +BBBASEDIR=`dirname $TOASTER`/.. + RUNNING=0 NOTOASTERUI=0
@@ -176,7 +185,7 @@ for param in $*; do esac done -[ -z "$ZSH_NAME" ] && SRCFILE=${BASH_SOURCE} || SRCFILE=$_ +[ -n "${BASH_SOURCE}" ] && SRCFILE=${BASH_SOURCE} || SRCFILE=$_ if [ `basename \"$0\"` = `basename \"${SRCFILE}\"` ]; then # We are called as standalone. We refuse to run in a build environment - we need the interactive mode for that.
@@ -236,7 +245,7 @@ if [ `basename \"$0\"` = `basename \"${SRCFILE}\"` ]; then echo "Starting browser..." xdg-open http://127.0.0.1:$WEB_PORT/ >/dev/null 2>&1 & fi - trap trap_ctrlc SIGINT + trap trap_ctrlc 2 echo "Toaster is now running. You can stop it with Ctrl-C" while [ $RUNNING -gt 0 ]; do python $BBBASEDIR/lib/toaster/manage.py runbuilds 2>&1 | tee -a "$BUILDDIR/toaster.log"
@@ -262,7 +271,7 @@ fi # Determine the action. If specified by arguments, fine, if not, toggle it -if [ "$1" '==' 'start' ] || [ "$1" '==' 'stop' ]; then +if [ "$1" = 'start' ] || [ "$1" = 'stop' ]; then CMD="$1" else if [ -z "$BBSERVER" ]; then
@@ -281,13 +290,13 @@ if [ -e $BUILDDIR/bitbake.lock ]; then python -c "import fcntl; fcntl.flock(open(\"$BUILDDIR/bitbake.lock\"), fcntl.LOCK_EX|fcntl.LOCK_NB)" 2>/dev/null || lock=0 fi -if [ ${CMD} '==' 'start' ] && [ $lock -eq 0 ]; then +if [ ${CMD} = 'start' ] && [ $lock -eq 0 ]; then echo "Error: bitbake lock state error. File locks show that the system is on." 1>&2 echo "Please wait for the current build to finish, stop and then start the system again." 1>&2 return 3 fi -if [ ${CMD} '==' 'start' ] && [ -e $BUILDDIR/.toastermain.pid ] && kill -0 `cat $BUILDDIR/.toastermain.pid`; then +if [ ${CMD} = 'start' ] && [ -e $BUILDDIR/.toastermain.pid ] && kill -0 `cat $BUILDDIR/.toastermain.pid`; then echo "Warning: bitbake appears to be dead, but the Toaster web server is running. Something fishy is going on." 1>&2 echo "Cleaning up the web server to start from a clean slate." webserverKillAll
Signature · 1 line
1.9.1
PATCH 16/19 toaster: tables: Remove obsolete field_name for select column
In reply to Alex DAMIANFrom: Vlad Fulgeanu <[email protected]>
field_name is no longer used for this kind of column. So no need to specify it here.
--- lib/toaster/toastergui/tables.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-)
lib/toaster/toastergui/tables.py +1 -2
@@ -227,8 +227,7 @@ class MachinesTable(ToasterTable): help_text="Sets the selected machine as the project machine. You can only have one machine per project", hideable=False, static_data_name="add-del-layers", - static_data_template='{% include "machine_btn.html" %}', - field_name="layer_version__id") + static_data_template='{% include "machine_btn.html" %}') class LayerMachinesTable(MachinesTable):
Signature · 1 line
1.9.1
PATCH 17/19 toaster: Add ajax loading spinner
In reply to Alex DAMIANFrom: Michael Wood <[email protected]>
This adds an ajax loading spinner to provide feedback when in-page loading is happening. It will show after 1.2 seconds of initial loading.
[YOCTO #7790]
--- lib/toaster/toastergui/static/css/default.css | 49 ++++++++++++++++++++++++++ lib/toaster/toastergui/static/js/libtoaster.js | 22 ++++++++++++ lib/toaster/toastergui/templates/base.html | 5 +++ 3 files changed, 76 insertions(+)
lib/toaster/toastergui/static/css/default.css +49 -0
@@ -259,3 +259,52 @@ div.add-deps { margin-top: 15px; } thead .description, .get_description_or_summary { width: 364px; } thead .add-del-layers { width: 124px; } + +#loading-notification { + position: fixed; + z-index: 101; + top: 3%; + left: 40%; + right: 40%; + #c09853 + -webkit-box-shadow: 0 0 10px #c09853; + -moz-box-shadow: 0 0 10px #c09853; + box-shadow: 0 0 10px #c09853; +} + +/* Copied in from newer version of Font-Awesome 4.3.0 */ +.fa-spin { + -webkit-animation: fa-spin 2s infinite linear; + animation: fa-spin 2s infinite linear; + display: inline-block; +} +.fa-pulse { + -webkit-animation: fa-spin 1s infinite steps(8); + animation: fa-spin 1s infinite steps(8); + display: inline-block; +} + +@-webkit-keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} + +@keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + -moz-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + -moz-transform: rotate(359deg); + transform: rotate(359deg); + } +} +/* End copied in from newer version of Font-Awesome 4.3.0 */
lib/toaster/toastergui/static/js/libtoaster.js +22 -0
@@ -341,6 +341,8 @@ function reload_params(params) { /* Things that happen for all pages */ $(document).ready(function() { + var ajaxLoadingTimer; + /* If we don't have a console object which might be the case in some * browsers, no-op it to avoid undefined errors. */
@@ -482,6 +484,26 @@ $(document).ready(function() { $('#collapse-warnings').addClass('in'); } + /* Show the loading notification if nothing has happend after 1.5 + * seconds + */ + $(document).bind("ajaxStart", function(){ + if (ajaxLoadingTimer) + window.clearTimeout(ajaxLoadingTimer); + + ajaxLoadingTimer = window.setTimeout(function() { + $("#loading-notification").fadeIn(); + }, 1200); + }); + + $(document).bind("ajaxStop", function(){ + if (ajaxLoadingTimer) + window.clearTimeout(ajaxLoadingTimer); + + $("#loading-notification").fadeOut(); + }); + + function check_for_duplicate_ids () { /* warn about duplicate element ids */ var ids = {};
lib/toaster/toastergui/templates/base.html +5 -0
@@ -68,6 +68,11 @@ </head> <body style="height: 100%"> + + <div id="loading-notification" class="alert lead text-center" style="display:none"> + Loading <i class="fa-pulse icon-spinner"></i> + </div> + <div class="navbar navbar-static-top"> <div class="navbar-inner"> <a class="brand logo" href="#"><img src="{% static 'img/logo.png' %}" class="" alt="Yocto logo project"/></a>
Signature · 1 line
1.9.1
PATCH 18/19 toaster: Add global ajaxError handler
In reply to Alex DAMIANFrom: Michael Wood <[email protected]>
If any ajax calls fail and debug is enabled log the error to the console.
--- lib/toaster/toastergui/static/js/libtoaster.js | 5 +++++ lib/toaster/toastergui/static/js/table.js | 5 ----- 2 files changed, 5 insertions(+), 5 deletions(-)
lib/toaster/toastergui/static/js/libtoaster.js +5 -0
@@ -503,6 +503,11 @@ $(document).ready(function() { $("#loading-notification").fadeOut(); }); + $(document).ajaxError(function(event, jqxhr, settings, errMsg){ + console.warn("Problem with xhr call"); + console.warn(errMsg); + console.warn(jqxhr.responseText); + }); function check_for_duplicate_ids () { /* warn about duplicate element ids */
lib/toaster/toastergui/static/js/table.js +0 -5
@@ -53,11 +53,6 @@ function tableInit(ctx){ tableData: tableData, tableParams: tableParams }, null, libtoaster.dumpsUrlParams(tableParams)); - }, - - error: function (_data) { - console.warn("Call failed"); - console.warn(_data); } }); }
Signature · 1 line
1.9.1
PATCH 19/19 toastergui: prevent contamination of libtoaster context
In reply to Alex DAMIANFrom: Alexandru DAMIAN <[email protected]>
This patch modifies the New Build button to use a local copy of the default libtoaster project context in order to prevent page contamination when a different project is selected in the drop-down menu.
--- lib/toaster/toastergui/static/js/base.js | 38 +++++++++++++++----------------- 1 file changed, 18 insertions(+), 20 deletions(-)
lib/toaster/toastergui/static/js/base.js +18 -20
@@ -11,7 +11,8 @@ function basePageInit(ctx) { return; } - var currentProjectId = libtoaster.ctx.projectId; + var selectedProject = libtoaster.ctx; + var selectedTarget; /* Hide the change project icon when there is only one project */ if (ctx.numProjects === 1) {
@@ -27,18 +28,16 @@ function basePageInit(ctx) { var newBuildProjectSaveBtn = $("#new-build-button #save-project-button"); - var selectedTarget; - _checkProjectBuildable(); _setupNewBuildButton(); function _checkProjectBuildable() { - if (libtoaster.ctx.projectId === undefined) { + if (selectedProject.projectId === undefined) { return; } - libtoaster.getProjectInfo(libtoaster.ctx.projectPageUrl, + libtoaster.getProjectInfo(selectedProject.projectPageUrl, function (data) { if (data.machine.name === undefined || data.layers.length === 0) { /* we can't build anything with out a machine and some layers */
@@ -52,9 +51,14 @@ function basePageInit(ctx) { newBuildTargetInput.prop("disabled", false); newBuildTargetBuildBtn.prop("disabled", false); - libtoaster.makeTypeahead(newBuildTargetInput, libtoaster.ctx.projectTargetsUrl, { format: "json" }, function (item) { + libtoaster.makeTypeahead(newBuildTargetInput, selectedProject.projectTargetsUrl, { format: "json" }, function (item) { /* successfully selected a target */ - selectedTarget = item; + selectedProject.projectPageUrl = item.projectPageUrl; + selectedProject.projectName = item.name; + selectedProject.projectId = item.id; + selectedProject.projectBuildsUrl = item.projectBuildsUrl; + + }); }
@@ -63,18 +67,17 @@ function basePageInit(ctx) { function _setupNewBuildButton() { /* Setup New build button */ - var selectedProject; /* If we don't have a current project then present the set project * form. */ - if (libtoaster.ctx.projectId === undefined) { + if (selectedProject.projectId === undefined) { $('#change-project-form').show(); $('#project .icon-pencil').hide(); } - libtoaster.makeTypeahead(newBuildProjectInput, libtoaster.ctx.projectsUrl, { format : "json" }, function (item) { + libtoaster.makeTypeahead(newBuildProjectInput, selectedProject.projectsUrl, { format : "json" }, function (item) { /* successfully selected a project */ newBuildProjectSaveBtn.removeAttr("disabled"); selectedProject = item;
@@ -107,23 +110,18 @@ function basePageInit(ctx) { selectedTarget = { name: newBuildTargetInput.val() }; } /* fire and forget */ - libtoaster.startABuild(libtoaster.ctx.projectBuildsUrl, libtoaster.ctx.projectId, selectedTarget.name, null, null); - window.location.replace(libtoaster.ctx.projectPageUrl); + libtoaster.startABuild(selectedProject.projectBuildsUrl, selectedProject.projectId, selectedTarget.name, null, null); + window.location.replace(selectedProject.projectPageUrl); }); newBuildProjectSaveBtn.click(function () { - libtoaster.ctx.projectId = selectedProject.pk; + selectedProject.projectId = selectedProject.pk; /* Update the typeahead project_id paramater */ _checkProjectBuildable(); /* we set the effective context of the page to the currently selected project */ /* TBD: do we override even if we already have a context project ?? */ /* TODO: replace global library context with references to the "selected" project */ - libtoaster.ctx.projectPageUrl = selectedProject.projectPageUrl; - libtoaster.ctx.projectName = selectedProject.name; - libtoaster.ctx.projectId = selectedProject.id; - - ctx.projectBuildsUrl = selectedProject.projectBuildsUrl; /* we can create a target typeahead only after we have a project selected */ newBuildTargetInput.prop("disabled", false);
@@ -137,8 +135,8 @@ function basePageInit(ctx) { newBuildTargetInput.val(""); /* set up new form aspect */ - $("#new-build-button #project a").text(selectedProject.name).attr('href', libtoaster.ctx.projectPageUrl); - $("#new-build-button .alert a").attr('href', libtoaster.ctx.projectPageUrl); + $("#new-build-button #project a").text(selectedProject.name).attr('href', selectedProject.projectPageUrl); + $("#new-build-button .alert a").attr('href', selectedProject.projectPageUrl); $("#project .icon-pencil").show(); $("#change-project-form").slideUp({ 'complete' : function () {
Signature · 1 line
1.9.1