Merge branch 'master' into nanterre-recette

This commit is contained in:
Thomas NOËL 2018-02-01 14:49:21 +01:00
commit c2a7715635
18 changed files with 897 additions and 34 deletions

View File

@ -9,6 +9,7 @@ import random
import faker
from django.core.management import call_command
from django.contrib.auth.models import User
from zoo.zoo_meta.models import EntitySchema, RelationSchema
from zoo.zoo_data.models import Entity, Transaction, Relation
@ -266,6 +267,14 @@ def rsu(rsu_schema):
return entities
@pytest.fixture
def admin(db):
user = User.objects.create(username='admin', is_staff=True, is_superuser=True)
user.set_password('admin')
user.save()
return user
@pytest.fixture
def app(request):
wtm = django_webtest.WebTestMixin()

View File

@ -5,6 +5,7 @@ import datetime
import isodate
import requests
import threading
import urlparse
import httmock
@ -648,16 +649,25 @@ def test_create_individu(transactional_db, app, rsu_schema):
assert response.json['err'] == 0
assert Log.objects.filter(entity__id=first_id)[0].content['id'] == i
qs = Log.objects.filter(entity__id=first_id)
more = journal_url + '?limit=10'
for i in range(10):
count = 0
all_data = []
while more:
response = app.get(more)
assert response.json['err'] == 0
if 'more' not in response.json:
break
assert len(response.json['data']) == 10
more = response.json['more']
assert 'cookie' in response.json
assert response.json['cookie'] in response.json['more']
assert len(response.json['data'])
count += len(response.json['data'])
all_data.extend(response.json['data'])
more = response.json.get('more')
if more:
assert 'cookie' in response.json
parsed = urlparse.urlparse(response.json['more'])
query = parsed.query
assert urlparse.parse_qs(query)['cookie'] == [response.json['cookie']]
assert sorted(d['id'] for d in all_data) == sorted(qs.values_list('id', flat=True))
assert count == qs.count()
assert 'more' not in response.json
# lecture avec filtrage

View File

@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
import csv
import StringIO
from webtest import Upload
def test_synchronize_federations(settings, app, nanterre_classic_family, admin):
settings.NANTERRE_SYNCHRONIZE_FEDERATIONS_DO_LATER = False
f = nanterre_classic_family
# ajout de clés de fédération technocarte
for entity in f.values():
entity.content.setdefault('cles_de_federation', {})['technocarte'] = str(entity.id + 1000)
entity.save()
response = app.get('/admin/').follow()
response.form.set('username', 'admin')
response.form.set('password', 'admin')
response = response.form.submit().follow()
response = response.click(u'Synchroniser les fédérations')
response = response.click(u'Nouvel import')
response.form.set('app_id', 'technocarte')
content = '\n'.join(map(str, [f['kevin'].id + 1000, f['marie'].id + 1000, '1222']))
response.form.set('csv_uploaded', Upload('federations.csv', content, 'application/octet-stream'))
response = response.form.submit().follow()
assert len(response.pyquery('table#result-list tbody tr')) == 1
response = response.click('Rapport')
def check_csv_response(csv_response):
reader = csv.DictReader(StringIO.StringIO(csv_response.content))
reader.fieldnames = reader.reader.next()
rows = list(reader)
def rows_by_action(action):
return [row for row in rows if row['action'] == action]
assert len(rows_by_action('KEEP')) == 2
assert len(rows_by_action('DELETE')) == 2
assert len(rows_by_action('UNKNOWN')) == 1
assert len(set(row['action'] for row in rows)) == 3
csv_response = response.click('CSV')
check_csv_response(csv_response)
response = response.click(u'Synchroniser les fédérations')
response = response.form.submit().follow()
response = response.click(u'Rapport d\'exécution')
csv_response = response.click('CSV')
check_csv_response(csv_response)

View File

@ -50,7 +50,6 @@ ALLOWED_HOSTS = ['*']
INSTALLED_APPS = [
'gadjo',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
@ -63,6 +62,7 @@ INSTALLED_APPS = [
'zoo.zoo_data',
'zoo.zoo_nanterre',
'zoo.zoo_demo',
'django.contrib.admin',
]
MIDDLEWARE = [

View File

@ -16,6 +16,7 @@
from django.contrib import admin
from django.forms import ModelForm
from django.apps import apps
from rangefilter.filter import DateTimeRangeFilter
@ -114,6 +115,15 @@ class EntityAdmin(DataAdmin):
actions.update(app_config.get_entity_actions())
return actions
def get_urls(self):
urls = super(EntityAdmin, self).get_urls()
cls_name = self.__class__.__name__.lower()
for app in apps.get_app_configs():
name = 'get_%s_urls' % cls_name
if hasattr(app, name):
urls = getattr(app, name)(self) + urls
return urls
class RelationAdmin(DataAdmin):
raw_id_fields = DataAdmin.raw_id_fields + ['left', 'right']

View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.9 on 2018-01-15 10:41
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('zoo_data', '0007_remove_log_url'),
]
operations = [
migrations.AlterModelOptions(
name='log',
options={'ordering': ('-timestamp', 'id'), 'verbose_name': 'log', 'verbose_name_plural': 'logs'},
),
]

View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.9 on 2018-01-25 08:39
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('zoo_data', '0007_remove_log_url'),
]
operations = [
migrations.AlterModelOptions(
name='entity',
options={'ordering': ('created',), 'permissions': (('action1_entity', 'Can do action1 on entities'), ('action2_entity', 'Can do action2 on entities'), ('action3_entity', 'Can do action3 on entities'), ('action4_entity', 'Can do action4 on entities'), ('action5_entity', 'Can do action5 on entities'), ('action6_entity', 'Can do action6 on entities'), ('action7_entity', 'Can do action7 on entities')), 'verbose_name': 'entity', 'verbose_name_plural': 'entities'},
),
]

View File

@ -142,6 +142,15 @@ class Entity(CommonData):
ordering = ('created',)
verbose_name = _('entity')
verbose_name_plural = _('entities')
permissions = (
("action1_entity", _("Can do action1 on entities")),
("action2_entity", _("Can do action2 on entities")),
("action3_entity", _("Can do action3 on entities")),
("action4_entity", _("Can do action4 on entities")),
("action5_entity", _("Can do action5 on entities")),
("action6_entity", _("Can do action6 on entities")),
("action7_entity", _("Can do action7 on entities")),
)
class Relation(CommonData):
@ -207,7 +216,7 @@ class Log(models.Model):
verbose_name=_('content'))
class Meta:
ordering = ('-timestamp', '-id')
ordering = ('-timestamp', 'id')
verbose_name = _('log')
verbose_name_plural = _('logs')
@ -225,6 +234,10 @@ class JobQuerySet(QuerySet):
def set_unrecoverable_error(self):
return self.update(state=self.model.STATE_UNRECOVERABLE_ERROR)
def by_action(self, action_klass):
class_path = self.model.get_classpath(action_klass)
return self.filter(**{'content__$classpath': class_path})
class Job(models.Model):
'''Store synchronization messages sent to applications'''
@ -273,9 +286,13 @@ class Job(models.Model):
return logging.getLogger(__name__)
@classmethod
def get_classpath(cls, instance):
module = instance.__class__.__module__
class_name = instance.__class__.__name__
def get_classpath(cls, class_or_instance):
if isinstance(class_or_instance, type):
klass = class_or_instance
else:
klass = class_or_instance.__class__
module = klass.__module__
class_name = klass.__name__
return '%s.%s' % (module, class_name)
@classmethod
@ -283,10 +300,12 @@ class Job(models.Model):
# conserve class path of the job_action to recreate it later
class_path = cls.get_classpath(job_action)
content = job_action.to_json()
assert isinstance(content, dict), 'action must serialize to a dict'
content['$classpath'] = class_path
job = Job.objects.create(content=content, transaction=transaction)
assert isinstance(job.content, dict), 'action must serialize to json'
job.state = cls.STATE_TODO
job_action.job = job
try:
if not do_later:
job.state = job_action.do(job=job)
@ -313,21 +332,15 @@ class Job(models.Model):
klass = getattr(module, klass_name)
except:
raise Exception('classpath %s unknown' % classpath)
return klass.from_json(content)
action = klass.from_json(content)
action.job = self
return action
def do(self, log=True):
content = self.content.copy()
action = self.action
try:
classpath = content.pop('$classpath')
module_name, klass_name = classpath.rsplit('.', 1)
try:
module = importlib.import_module(module_name)
klass = getattr(module, klass_name)
except:
raise Exception('classpath %s unknown' % classpath)
job_action = klass.from_json(content)
self.state = job_action.do(job=self)
content = job_action.to_json()
self.state = action.do(job=self)
content = action.to_json()
assert isinstance(content, dict), 'action must serialize to a dict'
self.content = content
except Exception as e:
@ -337,7 +350,7 @@ class Job(models.Model):
self.state = self.STATE_UNRECOVERABLE_ERROR
self.content['$exc_detail'] = unicode(e)
self.content['$exc_tb'] = traceback.format_exc()
self.content['$classpath'] = classpath
self.content['$classpath'] = self.get_classpath(action)
self.save()
@property

View File

@ -816,27 +816,39 @@ class JournalView(IndividuViewMixin, APIView):
except ValueError:
limit = 10
try:
cookie = int(request.GET.get('cookie', ''))
except ValueError:
cookie = 0
cookie = request.GET.get('cookie', '')
timestamp, last_id = cookie.split('_', 1)
timestamp = isodate.parse_datetime(timestamp)
last_id = int(last_id)
except:
cookie = None
limit = min(limit, 100)
qs = Log.objects.filter(entity=individu)
qs = qs.order_by('-timestamp', 'id')
if filters:
qs = qs.filter(**filters)
if cookie:
qs = qs.filter(id__lt=cookie)
qs = qs.filter(Q(timestamp__lt=timestamp) |
Q(timestamp=timestamp, id__gt=last_id))
qs = qs[:limit + 1]
data = [{'timestamp': log.timestamp.isoformat(), 'content': log.content}
for log in qs[:limit]]
data = [
{
'id': log.id,
'timestamp': log.timestamp.isoformat(),
'content': log.content
} for log in qs[:limit]
]
content = {
'err': 0,
'data': data,
}
if len(qs) > limit:
max_id = min(log.id for log in qs[:limit])
timestamp = qs[limit-1].timestamp.isoformat()
last_id = qs[limit-1].id
cookie = '%s_%s' % (timestamp, last_id)
more_url = request.build_absolute_uri(
reverse('rsu-api-journal', kwargs={'identifier': individu.id}))
cookie = str(max_id)
more_url += '?' + urlencode({'limit': limit, 'cookie': cookie})
content['cookie'] = cookie
content['more'] = more_url

View File

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
#
# zoo - versatile objects management
# Copyright (C) 2016 Entr'ouvert
#
@ -17,7 +19,9 @@
import functools
from django.apps import AppConfig
from django.conf.urls import url
from django.utils.translation import ugettext_lazy as _
from django.db.models.signals import post_migrate
class ZooNanterreConfig(AppConfig):
@ -41,3 +45,62 @@ class ZooNanterreConfig(AppConfig):
def get_job_list_filter(self):
from .admin import JobApplicationListFilter
return [JobApplicationListFilter]
def get_entityadmin_urls(self, model_admin):
from . import views
descs = [
{
're': r'',
'view': '',
'name': '',
},
{
're': r'add/',
'view': '_add',
'name': '-new-import',
},
{
're': r'(?P<job_id>\d+)/download-report/(?P<filename>.*)',
'view': '_download_report',
'name': '-download-report',
},
{
're': r'(?P<job_id>\d+)/report/(?P<filename>.*)',
'view': '_report',
'name': '-show-report',
},
{
're': r'(?P<job_id>\d+)/download-apply-report/(?P<filename>.*)',
'view': '_download_apply_report',
'name': '-download-apply-report',
},
{
're': r'(?P<job_id>\d+)/apply-report/(?P<filename>.*)',
'view': '_apply_report',
'name': '-show-apply-report',
},
{
're': r'(?P<job_id>\d+)/apply/',
'view': '_apply',
'name': '-apply',
},
]
urls = []
for desc in descs:
urls.append(url(
r'^synchronize-federations/%s$' % desc['re'],
model_admin.admin_site.admin_view(
getattr(views, 'synchronize_federations' + desc['view'])),
kwargs={'model_admin': model_admin},
name='synchronize-federations' + desc['name'],
))
return urls
def post_migrate(self, *args, **kwargs):
from django.contrib.auth.models import Permission
Permission.objects.filter(codename='action1_entity').update(name=u'Peut synchroniser les fédérations')
def ready(self):
post_migrate.connect(self.post_migrate)

View File

@ -15,8 +15,10 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django import forms
from django.utils.translation import ugettext_lazy as _
from .utils import PersonSearch
from .utils import PersonSearch, get_applications, get_application
from . import synchronize_federations
class SearchForm(forms.Form):
@ -40,3 +42,22 @@ class SearchForm(forms.Form):
except ValueError:
limit = 0.5
return iter(PersonSearch(limit=limit).search_query(query))
def application_choices():
yield ('', '---')
for slug in get_applications(rsu_ws_url=True):
dfn = get_application(slug)
yield (slug, dfn.get('name', slug))
class SynchronizeFederationsForm(forms.Form):
app_id = forms.ChoiceField(
choices=application_choices,
label=_('Application'))
csv_uploaded = forms.FileField(
label=_('CSV file'))
def save(self):
synchronize_federations.SynchronizeFederationsAction.synchronize(
**self.cleaned_data)

View File

@ -0,0 +1,283 @@
# -*- coding: utf-8 -*-
#
# zoo - versatile objects management
# Copyright (C) 2018 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import csv
import StringIO
from django.core.files.storage import default_storage
from django.core.urlresolvers import reverse
from django.conf import settings
from django.db import DatabaseError
from django.db.transaction import atomic
from zoo.zoo_meta.models import EntitySchema
from zoo.zoo_data.models import Job, Entity, Transaction, Log
from . import utils
class SynchronizeFederationsImport(object):
def __init__(self, action):
self.action = action
self.federations = []
self.errors = []
self.actions = []
self.keep_count = 0
self.new_count = 0
self.delete_count = 0
self.unknown_count = 0
def analyze(self):
self.federations = []
self.actions = []
self.keep_count = 0
self.new_count = 0
self.delete_count = 0
self.unknown_count = 0
max_federation = 0
for i, line in enumerate(default_storage.open(self.action.csv_filepath)):
line = line.strip()
self.federations.append(line)
try:
max_federation = max(max_federation, int(line))
except ValueError as e:
pass
self.federations.sort()
app_id = self.action.app_id
individus = Entity.objects.filter(
schema__slug=utils.INDIVIDU_ENT,
**{'content__cles_de_federation__%s__isnull' % app_id: False})
individus = individus.order_by('id')
federations_set = set(self.federations)
seen = set()
for individu in individus:
cle = individu.content['cles_de_federation'][app_id]
seen.add(cle)
if cle in federations_set:
action = 'KEEP'
self.keep_count += 1
elif cle.isdigit() and int(cle) > max_federation:
action = 'NEW'
self.new_count += 1
else:
action = 'DELETE'
self.delete_count += 1
self.actions.append((
individu.id,
individu.content['prenoms'],
individu.content['nom_de_naissance'],
individu.content['nom_d_usage'],
app_id,
cle,
action))
for federation in self.federations:
if federation not in seen:
self.unknown_count += 1
self.actions.append((
'',
'',
'',
'',
app_id,
federation,
'UNKNOWN'))
def analyze_and_report(self):
self.analyze()
self.report('report')
def report(self, target):
output_file = StringIO.StringIO()
writer = csv.writer(output_file)
writer.writerow(['RSU ID', 'prenoms', 'nom de naissance',
'nom d\'usage', 'application', 'federation', 'action'])
for action in self.actions:
action = [unicode(v).encode('utf-8') for v in action]
writer.writerow(action)
setattr(self.action, target + '_csv_filename',
self.action.csv_filename + '-report.csv')
setattr(self.action, target + '_csv_filepath',
default_storage.save(self.action.csv_filepath + '-report-.csv', output_file))
self.action.counts = {
'keep': self.keep_count,
'new': self.new_count,
'delete': self.delete_count,
'unknown': self.unknown_count,
}
def apply(self):
self.analyze()
deletes = {}
for row in self.actions:
entity_id, _, _, _, app_id, federation, action = row
assert app_id == self.action.app_id
if action != 'DELETE':
continue
deletes[entity_id] = federation
entities = Entity.objects.filter(id__in=deletes.keys())
app_name = utils.get_application(app_id)['name']
try:
with atomic():
transaction = Transaction.objects.create()
transaction.content = {
'action': 'synchronize-federation',
'job_id': self.action.job.id,
'app_id': app_id,
'federations_deleted': [],
}
logs = []
self.action.job.transaction = transaction
for entity in entities:
federation = deletes[entity.id]
if entity.content.get('cles_de_federation', {}).get(app_id) == federation:
del entity.content['cles_de_federation'][app_id]
transaction.content['federations_deleted'].append(
[entity.id, app_id, federation])
entity.modified = transaction
text = u'Supression de la fédération %s: %s' % (app_name, federation)
logs.append(
Log(transaction=transaction,
entity=entity,
content={
'text': text,
}))
entity.save()
else:
raise ValueError
transaction.save()
self.action.job.save()
Log.objects.bulk_create(logs)
except ValueError:
return False, 'atomic update failed'
except DatabaseError:
return False, 'concurrent update'
self.report('apply_report')
return True, None
class SynchronizeFederationsAction(object):
report_csv_filename = None
report_csv_filepath = None
apply_report_csv_filename = None
apply_report_csv_filepath = None
apply = False
errors = None
def __init__(self, app_id, csv_filename, csv_filepath,
report_csv_filename=None, report_csv_filepath=None, errors=None,
counts=None, apply_report_csv_filename=None, apply_report_csv_filepath=None, apply=False, **kwargs):
self.app_id = app_id
self.csv_filename = csv_filename
self.csv_filepath = csv_filepath
self.report_csv_filename = report_csv_filename
self.report_csv_filepath = report_csv_filepath
self.counts = counts
self.apply_report_csv_filename = apply_report_csv_filename
self.apply_report_csv_filepath = apply_report_csv_filepath
self.apply = apply
def to_json(self):
return {
'app_id': self.app_id,
'csv_filename': self.csv_filename,
'csv_filepath': self.csv_filepath,
'report_csv_filename': self.report_csv_filename,
'report_csv_filepath': self.report_csv_filepath,
'counts': self.counts,
'apply_report_csv_filename': self.apply_report_csv_filename,
'apply_report_csv_filepath': self.apply_report_csv_filepath,
'apply': self.apply,
}
@classmethod
def from_json(cls, d):
return cls(**d)
def do(self, job=None, **kwargs):
synchronize_federations_import = SynchronizeFederationsImport(self)
if self.apply:
result, errors = synchronize_federations_import.apply()
if not result:
self.errors = errors
return Job.STATE_ERROR
else:
synchronize_federations_import.analyze_and_report()
return Job.STATE_SUCCESS
@classmethod
def synchronize(cls, app_id, csv_uploaded):
csv_filepath = default_storage.save(
csv_uploaded.name, csv_uploaded)
self = cls(app_id, csv_uploaded.name, csv_filepath)
Job.create(self, do_later=getattr(settings, 'NANTERRE_SYNCHRONIZE_FEDERATIONS_DO_LATER', True))
@classmethod
def get_jobs(cls):
qs = Job.objects.by_action(cls)
return qs
@property
def report(self):
if not self.report_csv_filepath:
return None
return default_storage.open(self.report_csv_filepath)
def make_url(self, action, prefix):
stream = getattr(self, prefix)
if not stream:
return None
url_name = 'admin:synchronize-federations-%s-%s' % (action, prefix.replace('_', '-'))
filename = getattr(self, prefix + '_csv_filename')
return reverse(url_name, kwargs={
'job_id': self.job.id,
'filename': filename,
})
@property
def download_report_url(self):
return self.make_url('download', 'report')
@property
def report_url(self):
return self.make_url('show', 'report')
@property
def download_apply_report_url(self):
return self.make_url('download', 'apply_report')
@property
def apply_report_url(self):
return self.make_url('show', 'apply_report')
@property
def apply_report(self):
if not self.apply_report_csv_filepath:
return None
return default_storage.open(self.apply_report_csv_filepath)
def set_apply(self, job):
job.content['apply'] = True
job.state = job.STATE_TODO
job.save()
if not getattr(settings, 'NANTERRE_SYNCHRONIZE_FEDERATIONS_DO_LATER', True):
job.do()

View File

@ -0,0 +1,7 @@
{% extends "admin/base.html" %}
{% block title %}Administration technique du RSU{% endblock %}
{% block branding %}
<h1 id="site-name"><a href="{% url 'admin:index' %}">Administration technique du RSU</a></h1>
{% endblock %}

View File

@ -0,0 +1,99 @@
{% extends "admin/base_site.html" %}
{% load i18n static %}
{% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% static "admin/css/dashboard.css" %}" />{% endblock %}
{% block coltype %}colMS{% endblock %}
{% block bodyclass %}{{ block.super }} dashboard{% endblock %}
{% block breadcrumbs %}{% endblock %}
{% block content %}
<div id="content-main">
{% if app_list %}
{% for app in app_list %}
<div class="app-{{ app.app_label }} module">
<table>
<caption>
<a href="{{ app.app_url }}" class="section" title="{% blocktrans with name=app.name %}Models in the {{ name }} application{% endblocktrans %}">{{ app.name }}</a>
</caption>
{% for model in app.models %}
<tr class="model-{{ model.object_name|lower }}">
{% if model.admin_url %}
<th scope="row"><a href="{{ model.admin_url }}">{{ model.name }}</a></th>
{% else %}
<th scope="row">{{ model.name }}</th>
{% endif %}
{% if model.add_url %}
<td><a href="{{ model.add_url }}" class="addlink">{% trans 'Add' %}</a></td>
{% else %}
<td>&nbsp;</td>
{% endif %}
{% if model.admin_url %}
<td><a href="{{ model.admin_url }}" class="changelink">{% trans 'Change' %}</a></td>
{% else %}
<td>&nbsp;</td>
{% endif %}
</tr>
{% endfor %}
</table>
</div>
{% endfor %}
{% else %}
<p>{% trans "You don't have permission to edit anything." %}</p>
{% endif %}
<div class="module">
<table>
<caption>
Actions globales
</caption>
{% if perms.zoo_data.action1_entity %}
<tr>
<th scope="row">
<a href="{% url "admin:synchronize-federations" %}">
Synchroniser les fédérations
</a>
</th>
<td>&nbsp;</td>
<td>&nbsp;</td>
</tr>
{% endif %}
</table>
</div>
</div>
{% endblock %}
{% block sidebar %}
<div id="content-related">
<div class="module" id="recent-actions-module">
<h2>{% trans 'Recent actions' %}</h2>
<h3>{% trans 'My actions' %}</h3>
{% load log %}
{% get_admin_log 10 as admin_log for_user user %}
{% if not admin_log %}
<p>{% trans 'None available' %}</p>
{% else %}
<ul class="actionlist">
{% for entry in admin_log %}
<li class="{% if entry.is_addition %}addlink{% endif %}{% if entry.is_change %}changelink{% endif %}{% if entry.is_deletion %}deletelink{% endif %}">
{% if entry.is_deletion or not entry.get_admin_url %}
{{ entry.object_repr }}
{% else %}
<a href="{{ entry.get_admin_url }}">{{ entry.object_repr }}</a>
{% endif %}
<br/>
{% if entry.content_type %}
<span class="mini quiet">{% filter capfirst %}{{ entry.content_type }}{% endfilter %}</span>
{% else %}
<span class="mini quiet">{% trans 'Unknown content' %}</span>
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,64 @@
{% extends "admin/base_site.html" %}
{% load i18n admin_urls static %}
{% block extrastyle %}
{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static "admin/css/changelists.css" %}" />
{% endblock %}
{% block coltype %}flex{% endblock %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
&rsaquo; Synchroniser les fédérations
</div>
{% endblock %}
{% block content %}
<div id="content-main">
<ul class="object-tools">
<li><a href="{% url "admin:synchronize-federations-new-import" %}">Nouvel import</a></li>
</ul>
<table id="result-list">
{% comment %}Liste des imports {% endcomment %}
<thead>
<tr>
<th>Date</th>
<th>Application</th>
<th>Statut</th>
<th>Nom du fichier</th>
<th>Rapport d'import</th>
<th>Synchroniser ?</th>
</tr>
</thead>
<tbody>
{% for job in jobs %}
<tr>
<td>{{ job.created }}</td>
<td>{{ job.content.app_id }}</td>
<td>{{ job.get_state_display }}</td>
<td>{{ job.content.csv_filename }}</td>
<td>
{% if job.action.report_url %}
<a href="{{ job.action.report_url }}">Rapport</a>
{% endif %}
</td>
<td>
{% if job.action.download_apply_report_url %}
<a href="{{ job.action.apply_report_url }}">Rapport d'exécution</a>
{% elif job.state == job.STATE_SUCCESS %}
<form action="{% url "admin:synchronize-federations-apply" job_id=job.id %}" method="post">
{% csrf_token %}
<input type="submit" value="{% trans "Apply" %}">
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

View File

@ -0,0 +1,21 @@
{% extends "admin/base_site.html" %}
{% load i18n admin_urls %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
&rsaquo; <a href="{% url 'admin:synchronize-federations' %}">Synchroniser les fédérations</a>
&rsaquo; Nouvel import
</div>
{% endblock %}
{% block content %}
<div id="content-main">
<form enctype="multipart/form-data" method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Importer">
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,48 @@
{% extends "admin/base_site.html" %}
{% load i18n admin_urls static %}
{% block extrastyle %}
{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static "admin/css/changelists.css" %}" />
{% endblock %}
{% block coltype %}flex{% endblock %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
&rsaquo; <a href="{% url 'admin:synchronize-federations' %}">Synchroniser les fédérations</a>
&rsaquo; {{ job.created }} - {{ job.action.app_id }}
</div>
{% endblock %}
{% block content %}
<div id="content-main">
<a href="{{ csv_url }}">CSV</a> ( {{ csv_filesize|filesizeformat }} )
<div id="changelist">
<table id="result-list">
<thead>
<tr>
<th>RSU ID</th>
<th>Prénoms</th>
<th>Nom de naissance</th>
<th>Nom d'usage</th>
<th>Application</th>
<th>Fédération</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% for row in csv %}
<tr>
{% for cell in row %}
<td>{{ cell }}</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

View File

@ -16,11 +16,20 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import csv
from django.template.response import TemplateResponse
from django.views.generic import TemplateView
from django.shortcuts import redirect, get_object_or_404
from django.http import Http404, FileResponse
from django.db.transaction import non_atomic_requests
from django.db import connection
from django.contrib.auth.decorators import permission_required
from zoo.zoo_data.models import Job
from . import forms
from .synchronize_federations import SynchronizeFederationsAction
class Demo(TemplateView):
@ -76,3 +85,104 @@ class ImportControl(TemplateView):
search = non_atomic_requests(Search.as_view())
import_control = ImportControl.as_view()
@permission_required('zoo_data.action1_entity')
def synchronize_federations(request, model_admin, *args, **kwargs):
jobs = SynchronizeFederationsAction.get_jobs()
context = dict(
model_admin.admin_site.each_context(request),
title='Synchronises les applications',
jobs=jobs,
)
return TemplateResponse(request, "admin/zoo_data/entity/synchronize_federations.html", context)
@permission_required('zoo_data.action1_entity')
def synchronize_federations_report(request, job_id, model_admin, *args, **kwargs):
jobs = SynchronizeFederationsAction.get_jobs()
job = get_object_or_404(jobs, id=job_id)
report = job.action.report
if not report:
raise Http404('no report')
reader = csv.reader(report)
next(reader)
actions = [row for row in reader if row[6] != 'KEEP']
context = dict(
model_admin.admin_site.each_context(request),
title=job.created,
job=job,
csv=actions,
csv_url=job.action.download_report_url,
csv_filesize=report.size,
)
return TemplateResponse(request, "admin/zoo_data/entity/synchronize_federations_report.html", context)
@permission_required('zoo_data.action1_entity')
def synchronize_federations_download_report(request, job_id, model_admin, *args, **kwargs):
jobs = SynchronizeFederationsAction.get_jobs()
job = get_object_or_404(jobs, id=job_id)
report = job.action.report
if not report:
raise Http404('no report')
return FileResponse(report, content_type='text/csv')
@permission_required('zoo_data.action1_entity')
def synchronize_federations_apply_report(request, job_id, model_admin, *args, **kwargs):
jobs = SynchronizeFederationsAction.get_jobs()
job = get_object_or_404(jobs, id=job_id)
report = job.action.apply_report
if not report:
raise Http404('no report')
reader = csv.reader(report)
next(reader)
actions = [row for row in reader if row[6] != 'KEEP']
context = dict(
model_admin.admin_site.each_context(request),
title=u'Application - %s' % job.created,
job=job,
csv=actions,
csv_url=job.action.download_apply_report_url,
csv_filesize=report.size,
)
return TemplateResponse(request, "admin/zoo_data/entity/synchronize_federations_report.html", context)
@permission_required('zoo_data.action1_entity')
def synchronize_federations_download_apply_report(request, job_id, model_admin, *args, **kwargs):
jobs = SynchronizeFederationsAction.get_jobs()
job = get_object_or_404(jobs, id=job_id)
report = job.action.apply_report
if not report:
raise Http404('no report')
return FileResponse(report, content_type='text/csv')
@permission_required('zoo_data.action1_entity')
def synchronize_federations_add(request, model_admin, *args, **kwargs):
if request.method == 'POST':
form = forms.SynchronizeFederationsForm(request.POST, request.FILES)
if form.is_valid():
form.save()
return redirect('admin:synchronize-federations')
else:
form = forms.SynchronizeFederationsForm()
context = dict(
model_admin.admin_site.each_context(request),
form=form,
)
return TemplateResponse(request, "admin/zoo_data/entity/synchronize_federations_add.html", context)
@permission_required('zoo_data.action1_entity')
def synchronize_federations_apply(request, job_id, model_admin, **kwargs):
if request.method == 'POST':
jobs = SynchronizeFederationsAction.get_jobs()
job = get_object_or_404(jobs, id=job_id)
apply_report = job.action.apply_report
if apply_report:
raise Http404
job.action.set_apply(job)
return redirect('admin:synchronize-federations')