Projet

Général

Profil

0001-csv-files-datasource-5896.patch

Serghei Mihai, 26 août 2015 15:44

Télécharger (19 ko)

Voir les différences:

Subject: [PATCH] csv files datasource (#5896)

 passerelle/apps/csvdatasource/__init__.py          |  27 ++++++
 passerelle/apps/csvdatasource/admin.py             |   3 +
 passerelle/apps/csvdatasource/forms.py             |  16 ++++
 .../csvdatasource/locale/fr/LC_MESSAGES/django.mo  | Bin 0 -> 1441 bytes
 .../csvdatasource/locale/fr/LC_MESSAGES/django.po  |  92 ++++++++++++++++++
 .../apps/csvdatasource/migrations/0001_initial.py  |  32 +++++++
 passerelle/apps/csvdatasource/models.py            | 105 +++++++++++++++++++++
 .../csvdatasource/csvdatasource_detail.html        |  61 ++++++++++++
 passerelle/apps/csvdatasource/tests.py             |   3 +
 passerelle/apps/csvdatasource/urls.py              |  27 ++++++
 passerelle/apps/csvdatasource/views.py             |  45 +++++++++
 passerelle/settings.py                             |   1 +
 passerelle/static/css/style.css                    |   3 +
 13 files changed, 415 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/locale/fr/LC_MESSAGES/django.mo
 create mode 100644 passerelle/apps/csvdatasource/locale/fr/LC_MESSAGES/django.po
 create mode 100644 passerelle/apps/csvdatasource/migrations/0001_initial.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.apps.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

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

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

  
27
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/locale/fr/LC_MESSAGES/django.po
1
# SOME DESCRIPTIVE TITLE.
2
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3
# This file is distributed under the same license as the PACKAGE package.
4
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
5
#
6
#, fuzzy
7
msgid ""
8
msgstr ""
9
"Project-Id-Version: PACKAGE VERSION\n"
10
"Report-Msgid-Bugs-To: \n"
11
"POT-Creation-Date: 2015-08-26 08:25-0500\n"
12
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
13
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
14
"Language-Team: LANGUAGE <LL@li.org>\n"
15
"Language: \n"
16
"MIME-Version: 1.0\n"
17
"Content-Type: text/plain; charset=UTF-8\n"
18
"Content-Transfer-Encoding: 8bit\n"
19
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
20

  
21
#: models.py:19
22
msgid "CSV File"
23
msgstr "Fichier CSV"
24

  
25
#: models.py:20
26
msgid "Remote file URL"
27
msgstr "Adresse du fichier distant"
28

  
29
#: models.py:22
30
msgid "Optional column titles"
31
msgstr "Titres optionnels des colonnes"
32

  
33
#: models.py:23
34
msgid "in \"[column1_title],[column2_title],...\" format"
35
msgstr "au format \"[titre_colonne1],[titre_colonne2],...\""
36

  
37
#: models.py:25
38
msgid "Cache duration in seconds"
39
msgstr "Durée du cache en secondes"
40

  
41
#: models.py:26
42
msgid "applies to remote file"
43
msgstr "s'applique au fichier distant"
44

  
45
#: models.py:29
46
msgid "Data Sources"
47
msgstr "Source des données"
48

  
49
#: templates/csvdatasource/csvdatasource_detail.html:16
50
msgid "edit"
51
msgstr "modifier"
52

  
53
#: templates/csvdatasource/csvdatasource_detail.html:20
54
msgid "delete"
55
msgstr "supprimer"
56

  
57
#: templates/csvdatasource/csvdatasource_detail.html:26
58
#, python-format
59
msgid "File: %(file)s "
60
msgstr "Fichier: %(file)s"
61

  
62
#: templates/csvdatasource/csvdatasource_detail.html:28
63
#, python-format
64
msgid ""
65
"URL: \n"
66
"  <a href=\"%(address)s\">%(address)s</a>\n"
67
"  "
68
msgstr ""
69
"Adresse: \n"
70
"  <a href=\"%(address)s\">%(address)s</a>\n"
71
"  "
72
#: templates/csvdatasource/csvdatasource_detail.html:35
73
msgid "Endpoints"
74
msgstr ""
75

  
76
#: templates/csvdatasource/csvdatasource_detail.html:37
77
msgid "Returning all file lines: "
78
msgstr "Retourne toutes les lignes du fichier: "
79

  
80
#: templates/csvdatasource/csvdatasource_detail.html:40
81
msgid "Returning all lines containing 'abc' in 'text' column if defined : "
82
msgstr ""
83
"Retourne toutes les lignes du fichier contenant 'abc' dans la colonne 'text' "
84
"si définie :"
85

  
86
#: templates/csvdatasource/csvdatasource_detail.html:49
87
msgid "Security"
88
msgstr ""
89

  
90
#: templates/csvdatasource/csvdatasource_detail.html:52
91
msgid "Access is limited to the following API users:"
92
msgstr ""
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
                ('local_csv_file', models.FileField(upload_to=b'csv', verbose_name='CSV File', blank=True)),
22
                ('remote_csv_file', models.URLField(verbose_name='Remote file URL', blank=True)),
23
                ('columns_titles', models.CharField(help_text='in "[column1_title],[column2_title],..." format', max_length=256, verbose_name='Optional column titles', blank=True)),
24
                ('cache_duration', models.IntegerField(default=600, help_text='applies to remote file', 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/models.py
1
import re
2
import csv
3
import logging
4
import requests
5

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

  
11
from passerelle.base.models import BaseResource
12

  
13
logger = logging.getLogger(__name__)
14

  
15
_CACHE_SENTINEL = object()
16

  
17

  
18
class CsvDataSource(BaseResource):
19
    local_csv_file = models.FileField(_('CSV File'), upload_to='csv', blank=True)
20
    remote_csv_file = models.URLField(_('Remote file URL'), blank=True)
21
    columns_titles = models.CharField(max_length=256, blank=True,
22
                            verbose_name=_('Optional column titles'),
23
                            help_text=_('in "[column1_title],[column2_title],..." format'))
24
    cache_duration = models.IntegerField(default=600,
25
                                         verbose_name=_('Cache duration in seconds'),
26
                                         help_text=_('applies to remote file'))
27

  
28

  
29
    category = _('Data Sources')
30

  
31
    class Meta:
32
        verbose_name = 'CSV File'
33

  
34
    @classmethod
35
    def get_verbose_name(cls):
36
        return cls._meta.verbose_name
37

  
38
    @classmethod
39
    def get_icon_class(cls):
40
        return 'grid'
41

  
42
    @classmethod
43
    def is_enabled(cls):
44
        return True
45

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

  
50
    def get_absolute_url(self):
51
        return reverse('csvdatasource-detail', kwargs={'slug': self.slug})
52

  
53
    def has_cache(self):
54
        return cache.get(self.slug, _CACHE_SENTINEL) is not _CACHE_SENTINEL
55

  
56
    def set_cache(self, data):
57
        cache.set(self.slug, data, self.cache_duration)
58

  
59
    @property
60
    def content(self):
61
        if self.local_csv_file:
62
            logger.debug('returning data from local csv file')
63
            return self.local_csv_file.read()
64

  
65
        if self.remote_csv_file:
66
            if self.has_cache():
67
                logger.debug('returning cache content for remote file')
68
                return cache.get(self.slug, _CACHE_SENTINEL)
69
            logger.debug('getting data from url %s', self.remote_csv_file)
70
            r = requests.get(self.remote_csv_file)
71
            if r.ok:
72
                logger.debug('data successfully downloaded from %s',
73
                            self.remote_csv_file)
74
                self.set_cache(r.content)
75
                return r.content
76

  
77
    def get_data(self, filter_criteria=None):
78

  
79
        def filter_row(row, filter_criteria):
80
            if 'text' in self.columns_titles:
81
                col = self.columns_titles.index('text')
82
                if filter_criteria not in unicode(row[col], 'utf-8'):
83
                    return False
84
            return True
85

  
86
        data = []
87
        content = self.content
88
        if not content:
89
            return None
90

  
91
        dialect = csv.Sniffer().sniff(content[:1024])
92
        reader = csv.reader(content.splitlines(), dialect)
93

  
94
        if self.columns_titles:
95
            self.columns_titles = self.columns_titles.split(',')
96
        else:
97
            self.columns_titles = reader.next()
98

  
99
        for row in reader:
100
            if filter_criteria and not filter_row(row, filter_criteria):
101
                continue
102
            line = dict(zip(self.columns_titles, row))
103
            data.append(line)
104

  
105
        return data
passerelle/apps/csvdatasource/templates/csvdatasource/csvdatasource_detail.html
1
{% extends "passerelle/base.html" %}
2
{% load i18n passerelle %}
3

  
4
{% block more-user-links %}
5
{{ block.super }}
6
{% if object.id %}
7
<a href="{% url 'csvdatasource-detail' slug=object.slug %}">{{ object.title }}</a>
8
{% endif %}
9
{% endblock %}
10

  
11

  
12
{% block appbar %}
13
<h2>CSV - {{ object.title }}</h2>
14

  
15
{% if perms.csvdatasource.change_csvdatasource %}
16
<a rel="popup" class="button" href="{% url 'csvdatasource-edit' slug=object.slug %}">{% trans 'edit' %}</a>
17
{% endif %}
18

  
19
{% if perms.csvdatasource.delete_csvdatasource %}
20
<a rel="popup" class="button" href="{% url 'csvdatasource-delete' slug=object.slug %}">{% trans 'delete' %}</a>
21
{% endif %}
22
{% endblock %}
23

  
24
{% block content %}
25
<p>
26
  {% if object.local_csv_file.name %} {% blocktrans with file=object.local_csv_file.name %}File: {{ file }} {% endblocktrans %}
27
  {% elif object.remote_csv_file %}
28
  {% blocktrans with address=object.remote_csv_file %}URL: 
29
  <a href="{{ address }}">{{ address }}</a>
30
  {% endblocktrans %}
31
  {% endif %}
32
</p>
33

  
34
<div>
35
<h3>{% trans 'Endpoints' %}</h3>
36
<ul>
37
  <li>{% trans "Returning all file lines: "%}
38
    <a href="{% url "csvdatasource-data" slug=object.slug %}">{%  url "csvdatasource-data" slug=object.slug %}</a>
39
  </li>
40
  <li>{% trans "Returning all lines containing 'abc' in 'text' column if defined : "%}
41
    <a href="{% url "csvdatasource-data" slug=object.slug %}?q=abc">{%  url "csvdatasource-data" slug=object.slug %}?q=abc</a>
42
  </li>
43
</ul>
44
</div>
45

  
46

  
47
{% if perms.base.view_accessright %}
48
<div>
49
<h3>{% trans "Security" %}</h3>
50

  
51
<p>
52
  {% trans 'Access is limited to the following API users:' %}
53
</p>
54

  
55
{% access_rights_table resource=object permission='can_access' %}
56
{% endif %}
57

  
58
</div>
59

  
60
{% endblock %}
61

  
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 import utils
9

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

  
13

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

  
17

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

  
23

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

  
29

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

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

  
37

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

  
41
    @utils.protected_api('can_access')
42
    @utils.to_json('api')
43
    def get(self, request, *args, **kwargs):
44
        obj = self.get_object()
45
        return obj.get_data(request.GET.get('q'))
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
-