Projet

Général

Profil

0001-csv-files-data-source-5896.patch

Serghei Mihai, 02 juillet 2015 16:35

Télécharger (16,2 ko)

Voir les différences:

Subject: [PATCH] csv files data source (#5896)

 passerelle/apps/csvdatasource/__init__.py          |  26 +++++
 passerelle/apps/csvdatasource/admin.py             |   3 +
 passerelle/apps/csvdatasource/forms.py             |  16 +++
 .../apps/csvdatasource/migrations/0001_initial.py  |  32 ++++++
 .../0002_csvdatasource_columns_titles.py           |  20 ++++
 .../apps/csvdatasource/migrations/__init__.py      |   0
 passerelle/apps/csvdatasource/models.py            | 124 +++++++++++++++++++++
 .../csvdatasource/csvdatasource_detail.html        |  52 +++++++++
 passerelle/apps/csvdatasource/tests.py             |   3 +
 passerelle/apps/csvdatasource/urls.py              |  27 +++++
 passerelle/apps/csvdatasource/views.py             |  52 +++++++++
 passerelle/settings.py                             |   1 +
 passerelle/static/css/style.css                    |   3 +
 13 files changed, 359 insertions(+)
 create mode 100644 passerelle/apps/csvdatasource/__init__.py
 create mode 100644 passerelle/apps/csvdatasource/admin.py
 create mode 100644 passerelle/apps/csvdatasource/forms.py
 create mode 100644 passerelle/apps/csvdatasource/migrations/0001_initial.py
 create mode 100644 passerelle/apps/csvdatasource/migrations/0002_csvdatasource_columns_titles.py
 create mode 100644 passerelle/apps/csvdatasource/migrations/__init__.py
 create mode 100644 passerelle/apps/csvdatasource/models.py
 create mode 100644 passerelle/apps/csvdatasource/templates/csvdatasource/csvdatasource_detail.html
 create mode 100644 passerelle/apps/csvdatasource/tests.py
 create mode 100644 passerelle/apps/csvdatasource/urls.py
 create mode 100644 passerelle/apps/csvdatasource/views.py
passerelle/apps/csvdatasource/__init__.py
1
# passerelle.contrib.csvdatasource
2
# Copyright (C) 2015  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import django.apps
18

  
19
class AppConfig(django.apps.AppConfig):
20
    name = 'csvdatasource'
21

  
22
    def get_after_urls(self):
23
        from . import urls
24
        return urls.urlpatterns
25

  
26
default_app_config = 'csvdatasource.AppConfig'
passerelle/apps/csvdatasource/admin.py
1
from django.contrib import admin
2

  
3
# Register your models here.
passerelle/apps/csvdatasource/forms.py
1
from django.utils.text import slugify
2
from django import forms
3

  
4
from .models import CsvDataSource
5

  
6

  
7
class CsvDataSourceForm(forms.ModelForm):
8

  
9
    class Meta:
10
        model = CsvDataSource
11
        exclude = ('slug', 'users')
12

  
13
    def save(self, commit=True):
14
        if not self.instance.slug:
15
            self.instance.slug = slugify(self.instance.title)
16
        return super(CsvDataSourceForm, self).save(commit=commit)
passerelle/apps/csvdatasource/migrations/0001_initial.py
1
# -*- coding: utf-8 -*-
2
from __future__ import unicode_literals
3

  
4
from django.db import models, migrations
5

  
6

  
7
class Migration(migrations.Migration):
8

  
9
    dependencies = [
10
        ('base', '0001_initial'),
11
    ]
12

  
13
    operations = [
14
        migrations.CreateModel(
15
            name='CsvDataSource',
16
            fields=[
17
                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
18
                ('title', models.CharField(max_length=50)),
19
                ('slug', models.SlugField()),
20
                ('description', models.TextField()),
21
                ('csv_file', models.FileField(upload_to=b'csv')),
22
                ('key_column', models.IntegerField(default=0)),
23
                ('text_column', models.IntegerField(default=1)),
24
                ('cache_duration', models.IntegerField(default=600, verbose_name='Cache duration in seconds')),
25
                ('users', models.ManyToManyField(to='base.ApiUser', blank=True)),
26
            ],
27
            options={
28
                'verbose_name': 'CSV File',
29
            },
30
            bases=(models.Model,),
31
        ),
32
    ]
passerelle/apps/csvdatasource/migrations/0002_csvdatasource_columns_titles.py
1
# -*- coding: utf-8 -*-
2
from __future__ import unicode_literals
3

  
4
from django.db import models, migrations
5

  
6

  
7
class Migration(migrations.Migration):
8

  
9
    dependencies = [
10
        ('csvdatasource', '0001_initial'),
11
    ]
12

  
13
    operations = [
14
        migrations.AddField(
15
            model_name='csvdatasource',
16
            name='columns_titles',
17
            field=models.CharField(help_text='in "[column index]:[column title],..." format', max_length=256, verbose_name='Optional column titles', blank=True),
18
            preserve_default=True,
19
        ),
20
    ]
passerelle/apps/csvdatasource/models.py
1
import re
2
import csv
3
import logging
4

  
5
from django.db import models
6
from django.core.cache import cache
7
from django.utils.translation import ugettext_lazy as _
8
from django.core.urlresolvers import reverse
9

  
10
from passerelle.base.models import BaseResource
11

  
12
logger = logging.getLogger(__name__)
13

  
14
_CACHE_SENTINEL = object()
15

  
16
class CsvError(Exception):
17
    pass
18

  
19

  
20
class CsvDataSource(BaseResource):
21
    csv_file = models.FileField(upload_to='csv')
22
    key_column = models.IntegerField(default=0)
23
    text_column = models.IntegerField(default=1)
24
    columns_titles = models.CharField(max_length=256, blank=True,
25
                            verbose_name=_('Optional column titles'),
26
                            help_text=_('in "[column index]:[column title],..." format'))
27
    cache_duration = models.IntegerField(default=600,
28
                            verbose_name=_('Cache duration in seconds'))
29

  
30

  
31
    category = _('Data Sources')
32

  
33
    class Meta:
34
        verbose_name = 'CSV File'
35

  
36
    @classmethod
37
    def get_verbose_name(cls):
38
        return cls._meta.verbose_name
39

  
40
    @classmethod
41
    def get_icon_class(cls):
42
        return 'grid'
43

  
44
    @classmethod
45
    def is_enabled(cls):
46
        return True
47

  
48
    @classmethod
49
    def get_add_url(cls):
50
        return reverse('csvdatasource-add')
51

  
52
    def get_edit_url(self):
53
        return reverse('csvdatasource-edit', kwargs={'slug': self.slug})
54

  
55
    def get_delete_url(self):
56
        return reverse('csvdatasource-delete', kwargs={'slug': self.slug})
57

  
58
    def get_absolute_url(self):
59
        return reverse('csvdatasource-detail', kwargs={'slug': self.slug})
60

  
61
    def has_cache(self):
62
        self.__content = cache.get(self.slug, _CACHE_SENTINEL)
63
        return self.__content is not _CACHE_SENTINEL
64

  
65
    def set_cache(self, data):
66
        cache.set(self.slug, data, self.cache_duration)
67

  
68
    @property
69
    def content(self):
70
        if self.has_cache():
71
            return cache.get(self.slug, _CACHE_SENTINEL)
72
        self.__content = self.csv_file.read()
73
        self.set_cache(self.__content)
74
        return self.__content
75

  
76
    def get_data(self, id_row=None, columns=None, filter_criteria=None):
77

  
78
        def get_text(row, columns):
79
            d = ''
80
            for col in columns:
81
                try:
82
                    d += row[int(col)] + ' '
83
                except ValueError:
84
                    raise CsvError(_('Wrong column format: %s') % col)
85
                except IndexError:
86
                    raise CsvError(_('Unknown column: %s') % col)
87
            return d.strip()
88

  
89
        def get_id(row, column=0):
90
            try:
91
                if column:
92
                    return unicode(row[int(column)], 'utf-8')
93
                else:
94
                    return unicode(row[int(self.key_column)], 'utf-8')
95
            except IndexError:
96
                raise CsvError(_('Unexisting columns for id'))
97
            except ValueError:
98
                raise CsvError(_('Wrong id index: %s') % column)
99

  
100
        def filter_row(row, columns, filter_criteria):
101
            for col in columns:
102
                if filter_criteria in unicode(row[int(col)], 'utf-8'):
103
                    return True
104

  
105
        data = []
106
        dialect = csv.Sniffer().sniff(self.content[:1024])
107
        reader = csv.reader(self.content.splitlines(), dialect)
108

  
109
        if self.columns_titles:
110
            captions = self.columns_titles.split(',')
111
        else:
112
            captions = []
113

  
114
        for row in reader:
115
            if filter_criteria and not filter_row(row, columns, filter_criteria):
116
                continue
117
            r = {'id': get_id(row, id_row), 'text': get_text(row, columns)}
118
            for caption in captions:
119
                index, title = caption.split(':')
120
                r[title] = get_text(row, [index])
121
            data.append(r)
122

  
123
        data.sort(lambda x,y: cmp(x['text'], y['text']))
124
        return data
passerelle/apps/csvdatasource/templates/csvdatasource/csvdatasource_detail.html
1
{% extends "passerelle/base.html" %}
2
{% load i18n passerelle %}
3

  
4

  
5

  
6
{% block appbar %}
7
<h2>CSV - {{ object.title }}</h2>
8

  
9
{% if perms.csvdatasource.change_csvdatasource %}
10
<a rel="popup" class="button" href="{% url 'csvdatasource-edit' slug=object.slug %}">{% trans 'edit' %}</a>
11
{% endif %}
12

  
13
{% if perms.csvdatasource.delete_csvdatasource %}
14
<a rel="popup" class="button" href="{% url 'csvdatasource-delete' slug=object.slug %}">{% trans 'delete' %}</a>
15
{% endif %}
16
{% endblock %}
17

  
18

  
19

  
20
{% block content %}
21
<p>
22
Data file : {{ object.csv_file.name }}
23
</p>
24

  
25
<div>
26
<h3>{% trans 'Endpoints' %}</h3>
27
<ul>
28
  <li>{% trans "Returning all lines containing 'abc': "%}
29
    <a href="{% url "csvdatasource-data" slug=object.slug %}?q=abc">{%  url "csvdatasource-data" slug=object.slug %}?q=abc</a>
30
  </li>
31
    <li>{% trans "Specify 'id' and 'text' values indexes :" %}
32
    <a href="{% url "csvdatasource-data" slug=object.slug %}?q=abc&id_col=2&text_col=5">{%  url "csvdatasource-data" slug=object.slug %}?q=abc&id_row=2&text_row=5</a>
33
  </li>
34
</ul>
35
</div>
36

  
37

  
38
{% if perms.base.view_accessright %}
39
<div>
40
<h3>{% trans "Security" %}</h3>
41

  
42
<p>
43
{% trans 'Accessing the listings is open, but posting requests is limited to the following API users:' %}
44
</p>
45

  
46
{% access_rights_table resource=object permission='can_post_request' %}
47
{% endif %}
48

  
49
</div>
50

  
51
{% endblock %}
52

  
passerelle/apps/csvdatasource/tests.py
1
from django.test import TestCase
2

  
3
# Create your tests here.
passerelle/apps/csvdatasource/urls.py
1
from django.conf.urls import patterns, include, url
2
from django.contrib.auth.decorators import login_required
3

  
4
from passerelle.urls_utils import decorated_includes, required, app_enabled
5

  
6
from views import *
7

  
8
public_urlpatterns = patterns('',
9
    url(r'^(?P<slug>[\w,-]+)/$', CsvDataSourceDetailView.as_view(), name='csvdatasource-detail'),
10
    url(r'^(?P<slug>[\w,-]+)/data$', CsvDataView.as_view(), name='csvdatasource-data'),
11
)
12

  
13
management_urlpatterns = patterns('',
14
    url(r'^add$', CsvDataSourceCreateView.as_view(), name='csvdatasource-add'),
15
    url(r'^(?P<slug>[\w,-]+)/edit$', CsvDataSourceUpdateView.as_view(), name='csvdatasource-edit'),
16
    url(r'^(?P<slug>[\w,-]+)/delete$', CsvDataSourceDeleteView.as_view(), name='csvdatasource-delete'),
17
)
18

  
19

  
20
urlpatterns = required(
21
    app_enabled('csvdatasource'),
22
    patterns('',
23
        url(r'^csvdatasource/', include(public_urlpatterns)),
24
        url(r'^manage/csvdatasource/',
25
            decorated_includes(login_required, include(management_urlpatterns))),
26
    )
27
)
passerelle/apps/csvdatasource/views.py
1
import json
2

  
3
from django.core.urlresolvers import reverse
4
from django.views.generic.edit import CreateView, UpdateView, DeleteView
5
from django.views.generic.detail import DetailView, SingleObjectMixin
6
from django.views.generic.base import View
7

  
8
from passerelle.base.views import ResourceView
9
from passerelle.utils import to_json
10

  
11
from .models import CsvDataSource
12
from .forms import CsvDataSourceForm
13

  
14

  
15
class CsvDataSourceDetailView(DetailView):
16
    model = CsvDataSource
17

  
18

  
19
class CsvDataSourceCreateView(CreateView):
20
    model = CsvDataSource
21
    template_name = 'passerelle/manage/service_form.html'
22
    form_class = CsvDataSourceForm
23

  
24

  
25
class CsvDataSourceUpdateView(UpdateView):
26
    model = CsvDataSource
27
    template_name = 'passerelle/manage/service_form.html'
28
    form_class = CsvDataSourceForm
29

  
30

  
31
class CsvDataSourceDeleteView(DeleteView):
32
    model = CsvDataSource
33
    template_name = 'passerelle/manage/service_confirm_delete.html'
34

  
35
    def get_success_url(self):
36
        return reverse('manage-home')
37

  
38

  
39
class CsvDataView(View, SingleObjectMixin):
40
    model = CsvDataSource
41

  
42
    @to_json('api')
43
    def get(self, request, *args, **kwargs):
44
        obj = self.get_object()
45
        filter_criteria = request.GET.get('q')
46
        id_col = request.GET.get('id_col')
47
        columns = [request.GET.get('text_col', obj.text_column)]
48
        text_cols = request.GET.get('text_cols')
49
        if text_cols:
50
            text_cols = text_cols.split(',')
51
            columns = set(columns) | set(text_cols)
52
        return obj.get_data(id_col, columns, filter_criteria)
passerelle/settings.py
108 108
    'concerto',
109 109
    'bdp',
110 110
    'base_adresse',
111
    'csvdatasource',
111 112
    # backoffice templates and static
112 113
    'gadjo',
113 114
)
passerelle/static/css/style.css
32 32

  
33 33
li.gis a { background-image: url(icons/icon-gis.png); }
34 34
li.gis a:hover { background-image: url(icons/icon-gis-hover.png); }
35

  
36
li.grid a { background-image: url(icons/icon-grid.png); }
37
li.grid a:hover { background-image: url(icons/icon-grid-hover.png); }
35
-