add link to delete imported layer
cover add link to delete imported layer
On the layer details page, add a link which enables an imported layer to be deleted.
The following changes since commit 7fad9160ca998496af82747fc7fe69d0f5886e94:
buildinfohelper: ensure task datetimes are timezone-aware (2016-07-06 10:51:46 +0100)
are available in the git repository at:
git://git.yoctoproject.org/poky-contrib elliot/submit/michaelw/toaster/layerdetails-save-rev-del
http://git.yoctoproject.org/cgit.cgi/poky-contrib/log/?h=elliot/submit/michaelw/toaster/layerdetails-save-rev-del
Michael Wood (4):
toaster: layerdetails api Fix saving of git revision of a layer
toaster: api Add util function for returning the error response
toaster: add Layer delete front end feature to layerdetails
toaster: tests Add selenium test for layerdetails page
.../tests/browser/test_layerdetails_page.py | 190 +++++++++++++++++++++ lib/toaster/toastergui/api.py | 121 +++++++++++-- lib/toaster/toastergui/static/js/layerdetails.js | 20 +++ lib/toaster/toastergui/static/js/projectpage.js | 18 ++ lib/toaster/toastergui/templates/layerdetails.html | 20 ++- lib/toaster/toastergui/urls.py | 5 +- lib/toaster/toastergui/views.py | 43 ----- 7 files changed, 362 insertions(+), 55 deletions(-) create mode 100644 bitbake/lib/toaster/tests/browser/test_layerdetails_page.py
--
2.7.4
PATCH 1/4 toaster: layerdetails api Fix saving of git revision of a layer
From: Michael Wood <[email protected]>
Update, clean up and move the api for updating a layerversion from the views to api. Also update the layerdetails page to include the layerversion id in the url getter.
[YOCTO #8952]
--- lib/toaster/toastergui/api.py | 99 +++++++++++++++++++++- lib/toaster/toastergui/templates/layerdetails.html | 2 +- lib/toaster/toastergui/urls.py | 5 +- lib/toaster/toastergui/views.py | 43 ---------- 4 files changed, 103 insertions(+), 46 deletions(-)
lib/toaster/toastergui/api.py +98 -1
@@ -20,11 +20,14 @@ # Temporary home for the UI's misc API import re -from orm.models import Project, ProjectTarget, Build +from orm.models import Project, ProjectTarget, Build, Layer_Version +from orm.models import LayerVersionDependency, LayerSource, ProjectLayer from bldcontrol.models import BuildRequest from bldcontrol import bbcontroller from django.http import HttpResponse, JsonResponse from django.views.generic import View +from django.core.urlresolvers import reverse + class XhrBuildRequest(View):
@@ -109,3 +112,97 @@ class XhrBuildRequest(View): response = HttpResponse() response.status_code = 500 return response + + +class XhrLayer(View): + """ Get and Update Layer information """ + + def post(self, request, *args, **kwargs): + """ + Update a layer + + Entry point: /xhr_layer/<layerversion_id> + Method: POST + + Args: + vcs_url, dirpath, commit, up_branch, summary, description + + add_dep = append a layerversion_id as a dependency + rm_dep = remove a layerversion_id as a depedency + Returns: + {"error": "ok"} + or + {"error": <error message>} + """ + + def error_response(error): + return JsonResponse({"error": error}) + + try: + # We currently only allow Imported layers to be edited + layer_version = Layer_Version.objects.get( + id=kwargs['layerversion_id'], + project=kwargs['pid'], + layer_source__sourcetype=LayerSource.TYPE_IMPORTED) + + except Layer_Version.DoesNotExist: + return error_response("Cannot find imported layer to update") + + if "vcs_url" in request.POST: + layer_version.layer.vcs_url = request.POST["vcs_url"] + if "dirpath" in request.POST: + layer_version.dirpath = request.POST["dirpath"] + if "commit" in request.POST: + layer_version.commit = request.POST["commit"] + layer_version.branch = request.POST["commit"] + if "up_branch" in request.POST: + layer_version.up_branch_id = int(request.POST["up_branch"]) + if "summary" in request.POST: + layer_version.layer.summary = request.POST["summary"] + if "description" in request.POST: + layer_version.layer.description = request.POST["description"] + + if "add_dep" in request.POST: + lvd = LayerVersionDependency( + layer_version=layer_version, + depends_on_id=request.POST["add_dep"]) + lvd.save() + + if "rm_dep" in request.POST: + rm_dep = LayerVersionDependency.objects.get( + layer_version=layer_version, + depends_on_id=request.POST["rm_dep"]) + rm_dep.delete() + + try: + layer_version.layer.save() + layer_version.save() + except Exception as e: + return error_response("Could not update layer version entry: %s" + % e) + + return JsonResponse({"error": "ok"}) + + def delete(self, request, *args, **kwargs): + try: + # We currently only allow Imported layers to be deleted + layer_version = Layer_Version.objects.get( + id=kwargs['layerversion_id'], + project=kwargs['pid'], + layer_source__sourcetype=LayerSource.TYPE_IMPORTED) + except Layer_Version.DoesNotExist: + return error_response("Cannot find imported layer to delete") + + try: + ProjectLayer.objects.get(project=kwargs['pid'], + layercommit=layer_version).delete() + except ProjectLayer.DoesNotExist: + pass + + layer_version.layer.delete() + layer_version.delete() + + return JsonResponse({ + "error": "ok", + "redirect": reverse('project', args=(kwargs['pid'],)) + })
lib/toaster/toastergui/templates/layerdetails.html +1 -1
@@ -44,7 +44,7 @@ $(document).ready(function(){ var ctx = { - xhrUpdateLayerUrl : "{% url 'xhr_updatelayer' %}", + xhrUpdateLayerUrl : "{% url 'xhr_layer' layerversion.pk %}", layerVersion : { name : "{{layerversion.layer.name}}", id : {{layerversion.id}},
lib/toaster/toastergui/urls.py +4 -1
@@ -190,7 +190,10 @@ urlpatterns = patterns('toastergui.views', name='xhr_configvaredit'), url(r'^xhr_importlayer/$', 'xhr_importlayer', name='xhr_importlayer'), - url(r'^xhr_updatelayer/$', 'xhr_updatelayer', name='xhr_updatelayer'), + + url(r'^xhr_layer/(?P<layerversion_id>\d+)$', + api.XhrLayer.as_view(), + name='xhr_layer'), # JS Unit tests url(r'^js-unit-tests/$', 'jsunittests', name='js-unit-tests'),
lib/toaster/toastergui/views.py +0 -43
49 lines not on this page · raw message
Signature · 1 line
2.7.4
PATCH 2/4 toaster: api Add util function for returning the error response
From: Michael Wood <[email protected]>
Also clean up flake8 warnings in XhrBuildRequest
--- lib/toaster/toastergui/api.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-)
lib/toaster/toastergui/api.py +16 -12
@@ -29,6 +29,9 @@ from django.views.generic import View from django.core.urlresolvers import reverse +def error_response(error): + return JsonResponse({"error": error}) + class XhrBuildRequest(View):
@@ -81,20 +84,24 @@ class XhrBuildRequest(View): br.save() except BuildRequest.DoesNotExist: - return JsonResponse({'error':'No such build id %s' % i}) + return error_response('No such build id %s' % i) - return JsonResponse({'error': 'ok'}) + return error_response('ok') if 'buildDelete' in request.POST: for i in request.POST['buildDelete'].strip().split(" "): try: - BuildRequest.objects.select_for_update().get(project = project, pk = i, state__lte = BuildRequest.REQ_DELETED).delete() + BuildRequest.objects.select_for_update().get( + project=project, + pk=i, + state__lte=BuildRequest.REQ_DELETED).delete() + except BuildRequest.DoesNotExist: pass - return JsonResponse({'error': 'ok' }) + return error_response("ok") if 'targets' in request.POST: - ProjectTarget.objects.filter(project = project).delete() + ProjectTarget.objects.filter(project=project).delete() s = str(request.POST['targets']) for t in re.sub(r'[;%|"]', '', s).split(" "): if ":" in t:
@@ -102,12 +109,12 @@ class XhrBuildRequest(View): else: target = t task = "" - ProjectTarget.objects.create(project = project, - target = target, - task = task) + ProjectTarget.objects.create(project=project, + target=target, + task=task) project.schedule_build() - return JsonResponse({'error': 'ok' }) + return error_response('ok') response = HttpResponse() response.status_code = 500
@@ -135,9 +142,6 @@ class XhrLayer(View): {"error": <error message>} """ - def error_response(error): - return JsonResponse({"error": error}) - try: # We currently only allow Imported layers to be edited layer_version = Layer_Version.objects.get(
Signature · 1 line
2.7.4
PATCH 3/4 toaster: add Layer delete front end feature to layerdetails
In reply to Elliot SmithFrom: Michael Wood <[email protected]>
Add the front end feature to delete a layer from the layer details page.
[YOCO #9184]
--- lib/toaster/toastergui/static/js/layerdetails.js | 20 ++++++++++++++++++++ lib/toaster/toastergui/static/js/projectpage.js | 18 ++++++++++++++++++ lib/toaster/toastergui/templates/layerdetails.html | 20 +++++++++++++++++++- lib/toaster/toastergui/urls.py | 2 +- 4 files changed, 58 insertions(+), 2 deletions(-)
lib/toaster/toastergui/static/js/layerdetails.js +20 -0
@@ -397,6 +397,26 @@ function layerDetailsPageInit (ctx) { $(this).parents("form").submit(); }); + $("#layer-delete-confirmed").click(function(){ + $.cookie("layer-deleted", ctx.layerVersion.name, { path: '/'}); + + $.ajax({ + type: "DELETE", + url: ctx.xhrUpdateLayerUrl, + headers: { 'X-CSRFToken' : $.cookie('csrftoken')}, + success: function(data) { + if (data.error != "ok") { + console.warn(data.error); + } else { + window.location = data.redirect + "?notify=layer-deleted"; + } + }, + error: function(data) { + console.warn("Call failed"); + console.warn(data); + } + }); + }); layerDepsList.find(".glyphicon-trash").click(layerDepRemoveClick); layerDepsList.find("a").tooltip();
lib/toaster/toastergui/static/js/projectpage.js +18 -0
@@ -58,12 +58,30 @@ function projectPageInit(ctx) { case 'layer-imported': layerImportedNotification(); break; + case 'layer-deleted': + layerDeletedNotification(); default: break; } } })(); + /* Layer deleted notification */ + function layerDeletedNotification(){ + var layer = $.cookie("layer-deleted"); + + if (!layer) + return; + + var message = "You have deleted <strong>1</strong> layer from your "; + message += "project: <strong>" + layer + "</strong>"; + + libtoaster.showChangeNotification(message); + + $.removeCookie("layer-deleted", { path: "/"}); + } + + /* Layer imported notification */ function layerImportedNotification(){ var imported = $.cookie("layer-imported-alert");
lib/toaster/toastergui/templates/layerdetails.html +19 -1
@@ -6,6 +6,20 @@ {% block title %} {{layerversion.layer.name}} - {{project.name}} - Toaster {% endblock %} {% block pagecontent %} +<div id="delete-layer-modal" class="modal fade" tabindex="-1" role="dialog"> + <div class="modal-dialog" role="document"> + <div class="modal-content"> + <div class="modal-body"> + Are you sure you want to delete the <strong>{{layerversion.layer.name}}</strong> layer? + </div> + <div class="modal-footer"> + <button type="button" id="layer-delete-confirmed" class="btn btn-primary">Delete</button> + <button type="button" class="btn btn-default btn-link" data-dismiss="modal">Cancel</button> + </div> + </div> + </div> +</div> + <div class="row"> <div class="col-md-12"> <ul class="breadcrumb">
@@ -44,7 +58,7 @@ $(document).ready(function(){ var ctx = { - xhrUpdateLayerUrl : "{% url 'xhr_layer' layerversion.pk %}", + xhrUpdateLayerUrl : "{% url 'xhr_layer' project.id layerversion.pk %}", layerVersion : { name : "{{layerversion.layer.name}}", id : {{layerversion.id}},
@@ -271,6 +285,10 @@ </dd> {% endif %} </dl> + {% if layerversion.layer_source_id and layerversion.layer_source.sourcetype == layerversion.layer_source.TYPE_IMPORTED %} + <i class="icon-trash text-danger"></i> + <a href="#delete-layer-modal" role="button" class="text-danger" data-toggle="modal" data-target="#delete-layer-modal">Delete {{layerversion.layer.name}}</a> + {% endif %} </div> </div> </div>
lib/toaster/toastergui/urls.py +1 -1
@@ -191,7 +191,7 @@ urlpatterns = patterns('toastergui.views', url(r'^xhr_importlayer/$', 'xhr_importlayer', name='xhr_importlayer'), - url(r'^xhr_layer/(?P<layerversion_id>\d+)$', + url(r'^xhr_layer/(?P<pid>\d+)/(?P<layerversion_id>\d+)$', api.XhrLayer.as_view(), name='xhr_layer'),
Signature · 1 line
2.7.4
PATCH 4/4 toaster: tests Add selenium test for layerdetails page
In reply to Elliot SmithFrom: Michael Wood <[email protected]>
This tests:
- Adding remove layer from project
- Deleting layer
- Editing layer fields
--- .../tests/browser/test_layerdetails_page.py | 190 +++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 bitbake/lib/toaster/tests/browser/test_layerdetails_page.py
lib/toaster/tests/browser/test_layerdetails_page.py new file +190 -0
@@ -0,0 +1,190 @@ +#! /usr/bin/env python +# ex:ts=4:sw=4:sts=4:et +# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- +# +# BitBake Toaster Implementation +# +# Copyright (C) 2013-2016 Intel Corporation +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from django.core.urlresolvers import reverse +from tests.browser.selenium_helpers import SeleniumTestCase + +from orm.models import Layer, Layer_Version, Project, LayerSource, Release +from orm.models import BitbakeVersion + +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.common.by import By + + +class TestLayerDetailsPage(SeleniumTestCase): + """ Test layerdetails page works correctly """ + + def __init__(self, *args, **kwargs): + super(TestLayerDetailsPage, self).__init__(*args, **kwargs) + + self.initial_values = None + self.url = None + self.imported_layer_version = None + + def setUp(self): + release = Release.objects.create( + name='baz', + bitbake_version=BitbakeVersion.objects.create(name='v1') + ) + + # project to add new custom images to + self.project = Project.objects.create(name='foo', release=release) + + layer_source = LayerSource.objects.create( + sourcetype=LayerSource.TYPE_IMPORTED) + + name = "meta-imported" + vcs_url = "git://example.com/meta-imported" + subdir = "/layer" + gitrev = "d33d" + summary = "A imported layer" + description = "This was imported" + + imported_layer = Layer.objects.create(name=name, + vcs_url=vcs_url, + summary=summary, + description=description) + + self.imported_layer_version = Layer_Version.objects.create( + layer=imported_layer, + layer_source=layer_source, + branch=gitrev, + commit=gitrev, + dirpath=subdir, + project=self.project) + + self.initial_values = [name, vcs_url, subdir, gitrev, summary, + description] + self.url = reverse('layerdetails', + args=(self.project.pk, + self.imported_layer_version.pk)) + + def test_edit_layerdetails(self): + """ Edit all the editable fields for the layer refresh the page and + check that the new values exist""" + + self.get(self.url) + + self.click("#add-remove-layer-btn") + + # Open every edit box + for btn in self.find_all("dd .glyphicon-edit"): + btn.click() + + self.wait_until_visible("dd input") + + # Edit each value + for inputs in self.find_all("dd input[type=text]") + \ + self.find_all("dd textarea"): + # ignore the tt inputs (twitter typeahead input) + if "tt-" in inputs.get_attribute("class"): + continue + + value = inputs.get_attribute("value") + + self.assertTrue(value in self.initial_values, + "Expecting any of \"%s\"but got \"%s\"" % + (self.initial_values, value)) + + inputs.send_keys("-edited") + + for save_btn in self.find_all(".change-btn"): + save_btn.click() + + # Refresh the page to see if the new values are returned + self.get(self.url) + + new_values = ["%s-edited" % old_val + for old_val in self.initial_values] + + for inputs in self.find_all("dd input[type=text]") + \ + self.find_all("dd textarea"): + # ignore the tt inputs (twitter typeahead input) + if "tt-" in inputs.get_attribute("class"): + continue + + value = inputs.get_attribute("value") + + self.assertTrue(value in new_values, + "Expecting any of \"%s\"but got \"%s\"" % + (self.initial_values, value)) + + def test_delete_layer(self): + """ Delete the layer """ + + self.get(self.url) + + # Wait for the tables to load to avoid a race condition where the + # toaster tables have made an async request. If the layer is deleted + # before the request finishes it will cause an exception and fail this + # test. + wait = WebDriverWait(self.driver, 30) + + wait.until(EC.text_to_be_present_in_element( + (By.CLASS_NAME, + "table-count-recipestable"), "0")) + + wait.until(EC.text_to_be_present_in_element( + (By.CLASS_NAME, + "table-count-machinestable"), "0")) + + self.click('a[data-target="#delete-layer-modal"]') + self.wait_until_visible("#delete-layer-modal") + self.click("#layer-delete-confirmed") + + notification = self.wait_until_visible("#change-notification-msg") + expected_text = "You have deleted 1 layer from your project: %s" % \ + self.imported_layer_version.layer.name + + self.assertTrue(expected_text in notification.text, + "Expected notification text \"%s\" not found instead" + "it was \"%s\"" % + (expected_text, notification.text)) + + def test_addrm_to_project(self): + self.get(self.url) + + # Add the layer + self.click("#add-remove-layer-btn") + + notification = self.wait_until_visible("#change-notification-msg") + + expected_text = "You have added 1 layer to your project: %s" % \ + self.imported_layer_version.layer.name + + self.assertTrue(expected_text in notification.text, + "Expected notification text %s not found was " + " \"%s\" instead" % + (expected_text, notification.text)) + + # Remove the layer + self.click("#add-remove-layer-btn") + + notification = self.wait_until_visible("#change-notification-msg") + + expected_text = "You have removed 1 layer from your project: %s" % \ + self.imported_layer_version.layer.name + + self.assertTrue(expected_text in notification.text, + "Expected notification text %s not found was " + " \"%s\" instead" % + (expected_text, notification.text))
Signature · 1 line
2.7.4