Skip to contentSkip to conversation

Implement CSV export for layer recipes

PATCH In review Paul Eggleton 2 participants 4 messages May 7, 2018

lore
Share & tools
lore
#0 lore raw

cover Implement CSV export for layer recipes

Ross asked for the ability to export a layer's recipes in CSV format, and here it is - as well as cleanup of some old cruft that was supposed to do a similar thing but wasn't finished.

The following changes since commit 837359e6f18aac985c2967344d035d70c05e427a:

detail: Fix layer description not wrapping properly (2018-05-04 23:52:51 +1200)

are available in the Git repository at:

git://git.yoctoproject.org/layerindex-web paule/recipe-csv-export
http://git.yoctoproject.org/cgit.cgi//log/?h=paule/recipe-csv-export

Paul Eggleton (2):
Drop old raw recipe export code
Add CSV export for layer recipes

 layerindex/urls.py                  |  8 +-------
 layerindex/urls_branch.py           |  5 ++++-
 layerindex/views.py                 | 29 ++++++++++++++++++++++-------
 templates/layerindex/detail.html    | 14 +++++++-------
 templates/layerindex/rawrecipes.txt |  2 --
 5 files changed, 34 insertions(+), 24 deletions(-)
 delete mode 100644 templates/layerindex/rawrecipes.txt
Signature · 1 line
2.14.3
#1 lore raw

PATCH 1/2 Drop old raw recipe export code

We're about to replace this with a proper CSV export function, so we don't need this dead code hanging around anymore.

---
 layerindex/urls.py                  | 8 +-------
 layerindex/urls_branch.py           | 2 +-
 layerindex/views.py                 | 7 -------
 templates/layerindex/rawrecipes.txt | 2 --
 4 files changed, 2 insertions(+), 17 deletions(-)
 delete mode 100644 templates/layerindex/rawrecipes.txt
layerindex/urls.py +1 -7
index b0b74314..f80606fc 100644
@@ -8,7 +8,7 @@ from django.conf.urls import *
 from django.views.generic import TemplateView, DetailView, ListView, RedirectView
 from django.views.defaults import page_not_found
 from django.core.urlresolvers import reverse_lazy
-from layerindex.views import LayerListView, LayerReviewListView, LayerReviewDetailView, RecipeSearchView, MachineSearchView, PlainTextListView, LayerDetailView, edit_layer_view, delete_layer_view, edit_layernote_view, delete_layernote_view, HistoryListView, EditProfileFormView, AdvancedRecipeSearchView, BulkChangeView, BulkChangeSearchView, bulk_change_edit_view, bulk_change_patch_view, BulkChangeDeleteView, RecipeDetailView, RedirectParamsView, ClassicRecipeSearchView, ClassicRecipeDetailView, ClassicRecipeStatsView, LayerUpdateDetailView, UpdateListView, UpdateDetailView, StatsView, publish_view
+from layerindex.views import LayerListView, LayerReviewListView, LayerReviewDetailView, RecipeSearchView, MachineSearchView, LayerDetailView, edit_layer_view, delete_layer_view, edit_layernote_view, delete_layernote_view, HistoryListView, EditProfileFormView, AdvancedRecipeSearchView, BulkChangeView, BulkChangeSearchView, bulk_change_edit_view, bulk_change_patch_view, BulkChangeDeleteView, RecipeDetailView, RedirectParamsView, ClassicRecipeSearchView, ClassicRecipeDetailView, ClassicRecipeStatsView, LayerUpdateDetailView, UpdateListView, UpdateDetailView, StatsView, publish_view
 from layerindex.models import LayerItem, Recipe, RecipeChangeset
 from rest_framework import routers
 from . import restviews
@@ -101,12 +101,6 @@ urlpatterns = [
             name="bulk_change_delete"),
     url(r'^branch/(?P<branch>[-\w]+)/',
         include('layerindex.urls_branch')),
-    #url(r'^raw/recipes.txt$',
-    #    PlainTextListView.as_view(
-    #        queryset=Recipe.objects.order_by('pn', 'layerbranch__layer'),
-    #        context_object_name='recipe_list',
-    #        template_name='layerindex/rawrecipes.txt'),
-    #        name='recipe_list_raw'),
     url(r'^updates/$',
         UpdateListView.as_view(
             template_name='layerindex/updatelist.html'),
layerindex/urls_branch.py +1 -1
index a71af1ce..0e41435e 100644
@@ -7,7 +7,7 @@
 from django.conf.urls import *
 from django.views.defaults import page_not_found
 from django.core.urlresolvers import reverse_lazy
-from layerindex.views import LayerListView, RecipeSearchView, MachineSearchView, DistroSearchView, ClassSearchView, PlainTextListView, LayerDetailView, edit_layer_view, delete_layer_view, edit_layernote_view, delete_layernote_view, RedirectParamsView, DuplicatesView, LayerUpdateDetailView
+from layerindex.views import LayerListView, RecipeSearchView, MachineSearchView, DistroSearchView, ClassSearchView, LayerDetailView, edit_layer_view, delete_layer_view, edit_layernote_view, delete_layernote_view, RedirectParamsView, DuplicatesView, LayerUpdateDetailView
 
 urlpatterns = [
     url(r'^$', 
layerindex/views.py +0 -7
index bc3cddfa..dbf08497 100644
@@ -771,13 +771,6 @@ class ClassSearchView(ListView):
         context['this_url_name'] = resolve(self.request.path_info).url_name
         return context
 
-class PlainTextListView(ListView):
-    def render_to_response(self, context):
-        "Returns a plain text response rendering of the template"
-        template = get_template(self.template_name)
-        return HttpResponse(template.render(context),
-                                 content_type='text/plain')
-
 class HistoryListView(ListView):
     context_object_name = "revisions"
     paginate_by = 50
templates/layerindex/rawrecipes.txt deleted +0 -2
deleted file mode 100644
index c1911c9c..00000000
@@ -1,2 +0,0 @@
-{% for recipe in recipe_list %}{{ recipe.layerbranch.layer.name }}	{{ recipe.pn }}	{{ recipe.pv }}	{{ recipe.full_path }}
-{% endfor %}
Signature · 1 line
2.14.3
#2 lore raw

PATCH 2/2 Add CSV export for layer recipes

In reply to Paul Eggleton

Add the ability to export the recipe listing for a layer to a CSV file for importing into external tools. At the moment we include name, version and license, but there is a parameter that lets you specify the fields to include in the URL if desired.

Implements [YOCTO #12722].

---
 layerindex/urls_branch.py        |  5 ++++-
 layerindex/views.py              | 22 ++++++++++++++++++++++
 templates/layerindex/detail.html | 14 +++++++-------
 3 files changed, 33 insertions(+), 8 deletions(-)
layerindex/urls_branch.py +4 -1
index 0e41435e..2809147b 100644
@@ -7,7 +7,7 @@
 from django.conf.urls import *
 from django.views.defaults import page_not_found
 from django.core.urlresolvers import reverse_lazy
-from layerindex.views import LayerListView, RecipeSearchView, MachineSearchView, DistroSearchView, ClassSearchView, LayerDetailView, edit_layer_view, delete_layer_view, edit_layernote_view, delete_layernote_view, RedirectParamsView, DuplicatesView, LayerUpdateDetailView
+from layerindex.views import LayerListView, RecipeSearchView, MachineSearchView, DistroSearchView, ClassSearchView, LayerDetailView, edit_layer_view, delete_layer_view, edit_layernote_view, delete_layernote_view, RedirectParamsView, DuplicatesView, LayerUpdateDetailView, layer_export_recipes_csv_view
 
 urlpatterns = [
     url(r'^$', 
@@ -20,6 +20,9 @@ urlpatterns = [
         LayerDetailView.as_view(
             template_name='layerindex/detail.html'),
             name='layer_item'),
+    url(r'^layer/(?P<slug>[-\w]+)/recipes/csv/$',
+        layer_export_recipes_csv_view,
+        name='layer_export_recipes_csv'),
     url(r'^recipes/$',
         RecipeSearchView.as_view(
             template_name='layerindex/recipes.html'),
layerindex/views.py +22 -0
index dbf08497..06d35261 100644
@@ -1022,3 +1022,25 @@ class StatsView(TemplateView):
                 machine_count=Count('layerbranch__machine', distinct=True),
                 distro_count=Count('layerbranch__distro', distinct=True))
         return context
+
+
+def layer_export_recipes_csv_view(request, branch, slug):
+    import csv
+    layer = get_object_or_404(LayerItem, name=slug)
+    layerbranch = layer.get_layerbranch(branch)
+
+    response = HttpResponse(content_type='text/csv')
+    response['Content-Disposition'] = 'attachment; filename="recipes_%s_%s.csv"' % (layer.name, layerbranch.branch.name)
+
+    fieldlist = request.GET.get('fields', 'pn,pv,license').split(',')
+    recipe_fields = [f.name for f in Recipe._meta.get_fields() if not (f.auto_created and f.is_relation)]
+    for field in fieldlist:
+        if field not in recipe_fields:
+            return HttpResponse('Field %s is invalid' % field)
+
+    writer = csv.writer(response)
+    for recipe in layerbranch.sorted_recipes():
+        values = [getattr(recipe, field) for field in fieldlist]
+        writer.writerow(values)
+
+    return response
templates/layerindex/detail.html +7 -7
index 220d475b..67c21126 100644
@@ -199,13 +199,13 @@
                         <div class="navbar-inner">
                             <a class="brand pull-left">{{ layeritem.name }} recipes <span class="muted">({{ layerbranch.sorted_recipes.count }})</span></a>
 
-                            <ul class="nav pull-right">
-                                <li>
-                                    <form action="" class="navbar-search pull-right" id="filter-form">
-                                        <input type="text" placeholder="Search recipes" class="search-query" id="filter">
-                                    </form>
-                                </li>
-                            </ul>
+                            <div class="pull-right">
+                                <a href="{% url 'layer_export_recipes_csv' layerbranch.branch.name layerbranch.layer.name %}" class="btn"><i class="icon-file"></i> Export CSV</a>
+
+                                <form action="" class="navbar-search nav-spacer pull-right" id="filter-form">
+                                    <input type="text" placeholder="Search recipes" class="search-query" id="filter">
+                                </form>
+                            </div>
                         </div>
                     </div>
 
Signature · 1 line
2.14.3
#3 lore raw

Re: [layerindex-web][PATCH 2/2] Add CSV export for layer recipes

Quoted 6 lines · Paul Eggleton

Add the ability to export the recipe listing for a layer to a CSV file for importing into external tools. At the moment we include name, version and license, but there is a parameter that lets you specify the fields to include in the URL if desired.

Implements [YOCTO #12722].

With the bitbake layer library I'm working on, it should be possible to do this directly out of bitbake.

This wouldn't limit it to just recipes either, but any data. Loading (querying) the json data from the layer index is easy, and the library would know how to filter to the right branch/layer/etc.

Something to consider as we go forward with this and other work.

--Mark

Quoted 86 lines
---
 layerindex/urls_branch.py        |  5 ++++-
 layerindex/views.py              | 22 ++++++++++++++++++++++
 templates/layerindex/detail.html | 14 +++++++-------
 3 files changed, 33 insertions(+), 8 deletions(-)
layerindex/urls_branch.py +4 -1
index 0e41435e..2809147b 100644
@@ -7,7 +7,7 @@
 from django.conf.urls import *
 from django.views.defaults import page_not_found
 from django.core.urlresolvers import reverse_lazy
-from layerindex.views import LayerListView, RecipeSearchView, MachineSearchView, DistroSearchView, ClassSearchView, LayerDetailView, edit_layer_view, delete_layer_view, edit_layernote_view, delete_layernote_view, RedirectParamsView, DuplicatesView, LayerUpdateDetailView
+from layerindex.views import LayerListView, RecipeSearchView, MachineSearchView, DistroSearchView, ClassSearchView, LayerDetailView, edit_layer_view, delete_layer_view, edit_layernote_view, delete_layernote_view, RedirectParamsView, DuplicatesView, LayerUpdateDetailView, layer_export_recipes_csv_view
 
 urlpatterns = [
     url(r'^$', 
@@ -20,6 +20,9 @@ urlpatterns = [
         LayerDetailView.as_view(
             template_name='layerindex/detail.html'),
             name='layer_item'),
+    url(r'^layer/(?P<slug>[-\w]+)/recipes/csv/$',
+        layer_export_recipes_csv_view,
+        name='layer_export_recipes_csv'),
     url(r'^recipes/$',
         RecipeSearchView.as_view(
             template_name='layerindex/recipes.html'),
layerindex/views.py +22 -0
index dbf08497..06d35261 100644
@@ -1022,3 +1022,25 @@ class StatsView(TemplateView):
                 machine_count=Count('layerbranch__machine', distinct=True),
                 distro_count=Count('layerbranch__distro', distinct=True))
         return context
+
+
+def layer_export_recipes_csv_view(request, branch, slug):
+    import csv
+    layer = get_object_or_404(LayerItem, name=slug)
+    layerbranch = layer.get_layerbranch(branch)
+
+    response = HttpResponse(content_type='text/csv')
+    response['Content-Disposition'] = 'attachment; filename="recipes_%s_%s.csv"' % (layer.name, layerbranch.branch.name)
+
+    fieldlist = request.GET.get('fields', 'pn,pv,license').split(',')
+    recipe_fields = [f.name for f in Recipe._meta.get_fields() if not (f.auto_created and f.is_relation)]
+    for field in fieldlist:
+        if field not in recipe_fields:
+            return HttpResponse('Field %s is invalid' % field)
+
+    writer = csv.writer(response)
+    for recipe in layerbranch.sorted_recipes():
+        values = [getattr(recipe, field) for field in fieldlist]
+        writer.writerow(values)
+
+    return response
templates/layerindex/detail.html +7 -7
index 220d475b..67c21126 100644
@@ -199,13 +199,13 @@
                         <div class="navbar-inner">
                             <a class="brand pull-left">{{ layeritem.name }} recipes <span class="muted">({{ layerbranch.sorted_recipes.count }})</span></a>
 
-                            <ul class="nav pull-right">
-                                <li>
-                                    <form action="" class="navbar-search pull-right" id="filter-form">
-                                        <input type="text" placeholder="Search recipes" class="search-query" id="filter">
-                                    </form>
-                                </li>
-                            </ul>
+                            <div class="pull-right">
+                                <a href="{% url 'layer_export_recipes_csv' layerbranch.branch.name layerbranch.layer.name %}" class="btn"><i class="icon-file"></i> Export CSV</a>
+
+                                <form action="" class="navbar-search nav-spacer pull-right" id="filter-form">
+                                    <input type="text" placeholder="Search recipes" class="search-query" id="filter">
+                                </form>
+                            </div>
                         </div>
                     </div>