0001-csv-files-datasource-5896.patch
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 |
- |