Skip to contentSkip to conversation

add link to delete imported layer

PATCH New Elliot Smith 1 participant 5 messages Jul 6, 2016

lore
Share & tools
lore
#0 lore raw

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

#1 lore raw

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
index 961b594..a024748 100644
@@ -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
index 0f0b2b4..143ec25 100644
@@ -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
index 9510a38..15b1063 100644
@@ -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
#2 lore raw

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
index a024748..112ce58 100644
@@ -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
#3 lore raw

PATCH 3/4 toaster: add Layer delete front end feature to layerdetails

In reply to Elliot Smith

From: 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
index a56087b..683486e 100644
@@ -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
index 6d92490..df79849 100644
@@ -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
index 143ec25..4b51d1a 100644
@@ -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
index 15b1063..1c0ccbb 100644
@@ -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
#4 lore raw

PATCH 4/4 toaster: tests Add selenium test for layerdetails page

In reply to Elliot Smith

From: 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
new file mode 100644
index 0000000..fb1007f
@@ -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