passerelle/passerelle/apps/gesbac/models.py

443 lines
12 KiB
Python

# -*- coding: utf-8 -*-
# Copyright (C) 2019 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 io
from collections import OrderedDict
from django.db import models, transaction, IntegrityError
from django.utils import six
from django.utils.encoding import force_bytes
from django.utils.translation import ugettext_lazy as _
from django.http import Http404
from passerelle.base.models import BaseResource
from passerelle.utils.api import endpoint, APIError
from passerelle.utils import SFTPField
from jsonfield import JSONField
CSV_DELIMITER = ';'
FILES_ENCODING = 'latin-1'
APPLICANT_SCHEMA = OrderedDict((
("form_id", {
"type": "string",
}),
("demand_date", {
"type": "string",
"pattern": "^[0-9]{8}$",
}),
("demand_time", {
"type": "string",
"pattern": "^[0-9]{6}$",
}),
("producer_code", {
"type": "integer",
}),
("invariant_number", {
"type": "string",
"maxLength": 10,
"default": ""
}),
("city_insee_code", {
"type": "string",
}),
("street_rivoli_code", {
"type": "string",
}),
("street_name", {
"type": "string",
}),
("address_complement", {
"type": "string",
"maxLength": 32,
"default": ""
}),
("street_number", {
"type": "integer",
"default": 0
}),
("bis_ter", {
"type": "string",
"maxLength": 3,
"default": ""
}),
("building", {
"type": "string",
"maxLength": 5,
"default": ""
}),
("hall", {
"type": "string",
"maxLength": 5,
"default": ""
}),
("appartment_number", {
"type": "string",
"maxLength": 5,
"default": ""
}),
("producer_social_reason", {
"type": "string",
"maxLength": 38,
"default": ""
}),
("producer_title_code", {
"type": "integer",
"default": 0
}),
("producer_last_name", {
"type": "string",
"maxLength": 38,
"default": ""
}),
("producer_first_name", {
"type": "string",
"maxLength": 32,
"default": ""
}),
("producer_phone", {
"type": "string",
"maxLength": 20,
"default": ""
}),
("producer_email", {
"type": "string",
"maxLength": 50,
"default": ""
}),
("owner_last_name", {
"type": "string",
"maxLength": 38,
"default": ""
}),
("owner_first_name", {
"type": "string",
"maxLength": 32,
"default": ""
}),
("owner_phone", {
"type": "string",
"maxLength": 20,
"default": ""
}),
("owner_email", {
"type": "string",
"maxLength": 50,
"default": ""
}),
("activity_code", {
"type": "integer",
"default": 0
}),
("family_members_number", {
"type": "integer",
"default": 0
}),
("houses_number", {
"type": "integer",
"default": 0
}),
("t1_flats_number", {
"type": "integer",
"default": 0
}),
("t2_flats_number", {
"type": "integer",
"default": 0
}),
("t3_flats_number", {
"type": "integer",
"default": 0
}),
("t4_flats_number", {
"type": "integer",
"default": 0
}),
("t5_flats_number", {
"type": "integer",
"default": 0
}),
("t6_flats_number", {
"type": "integer",
"default": 0
}),
("shops_number", {
"type": "integer",
"default": 0
}),
("garden_size", {
"type": "integer",
"default": 0
}),
("expected_date", {
"type": "string",
"pattern": "^[0-9]{8}$",
"default": ""
}),
("expected_time", {
"type": "string",
"pattern": "^[0-9]{4}$",
"default": ""
}),
("modification_code", {
"type": "integer",
"default": 0
}),
("demand_reason_label", {
"type": "string",
"default": ""
}),
("comment", {
"type": "string",
"maxLength": 500,
"default": ""
}))
)
CARD_SCHEMA = OrderedDict((
("card_subject", {
"type": "integer",
}),
("card_type", {
"type": "integer",
}),
("card_demand_reason", {
"type": "integer",
}),
("cards_quantity", {
"type": "integer",
}),
("card_number", {
"type": "string",
"maxLength": 20,
}),
("card_bar_code", {
"type": "string",
"maxLength": 20,
"default": "",
}),
("card_code", {
"type": "string",
"maxLength": 20,
"default": "",
}),
("card_validity_start_date", {
"type": "string",
"pattern": "^[0-9]{8}$",
"default": "",
}),
("card_validity_end_date", {
"type": "string",
"pattern": "^[0-9]{8}$",
"default": "",
}),
("card_comment", {
"type": "string",
"maxLength": 100,
"default": "",
}))
)
DEMAND_SCHEMA = APPLICANT_SCHEMA.copy()
DEMAND_SCHEMA.update(CARD_SCHEMA)
SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Gesbac",
"description": "",
"type": "object",
"required": [
"form_id", "demand_date", "demand_time", "producer_code", "city_insee_code",
"street_rivoli_code", "street_name", "card_subject", "card_type", "card_demand_reason",
"cards_quantity"
],
"properties": DEMAND_SCHEMA,
}
class Gesbac(BaseResource):
outcoming_sftp = SFTPField(verbose_name=_('Outcoming SFTP'))
incoming_sftp = SFTPField(verbose_name=_('Incoming SFTP'))
output_files_prefix = models.CharField(_('Output files prefix'),
blank=False, max_length=32)
input_files_prefix = models.CharField(_('Input files prefix'),
blank=False, max_length=32)
category = _('Business Process Connectors')
class Meta:
verbose_name = u'Gesbac'
def check_status(self):
with self.outcoming_sftp.client() as out_sftp:
out_sftp.listdir()
with self.incoming_sftp.client() as in_sftp:
in_sftp.listdir()
def get_responses(self):
data = []
with self.incoming_sftp.client() as client:
for csv_file in client.listdir():
if not csv_file.startswith(self.input_files_prefix):
continue
with client.open(csv_file, 'rb') as fd:
if six.PY3:
content = io.TextIOWrapper(fd, encoding=FILES_ENCODING)
else:
content = io.BytesIO(force_bytes(fd.read().decode(FILES_ENCODING)))
for row in csv.reader(content, delimiter=CSV_DELIMITER):
data.append(row)
for card_data in data:
for form in self.form_set.filter(status='sent'):
if card_data[1] == form.get_gesbac_id():
form.card_data = card_data
form.status = 'closed'
form.save()
def hourly(self):
super(Gesbac, self).hourly()
self.get_responses()
def send_demand(self, form_id):
form = Form.objects.get(id=form_id)
form.send()
@endpoint(name='create-demand',
perm='can_access',
description=_('Create demand'),
post={
'description': _('Creates a demand file'),
'request_body': {
'schema': {
'application/json': SCHEMA
}
}
}
)
def create_demand(self, request, post_data):
form_id = post_data['form_id']
for counter in range(20):
try:
with transaction.atomic():
form = Form.objects.create(resource=self, form_id=form_id, counter=counter)
break
except IntegrityError as e:
continue
else:
raise APIError('fail: more than 20 demands')
post_data['form_id'] = form.get_gesbac_id()
data = []
applicant_data = ['E']
# get applicant attributes
for name, value in APPLICANT_SCHEMA.items():
if value['type'] == 'string':
item = post_data.get(name, '')
elif value['type'] == 'integer':
item = post_data.get(name, 0)
applicant_data.append(item)
data.append(applicant_data)
# get card attributes
card_data = ['CARTE', post_data['form_id']]
for name, value in CARD_SCHEMA.items():
if value['type'] == 'string':
item = post_data.get(name, '')
elif value['type'] == 'integer':
item = post_data.get(name, 0)
card_data.append(item)
data.append(card_data)
form.demand_data = data
form.save()
self.add_job('send_demand', form_id=form.id)
return {'data': {'filename': form.get_filename(),
'gesbac_id': form.get_gesbac_id()}}
@endpoint(name='get-response', perm='can_access',
description=_('Get response'),
parameters={
'gesbac_id': {
'description': _('Gesbac demand identifier'),
'example_value': '420001'
}
}
)
def get_response(self, request, gesbac_id):
try:
response = self.form_set.filter(status='closed',
gesbac_id=gesbac_id).latest()
return {'data': response.card_data}
except Form.DoesNotExist:
raise Http404('No response found')
FORM_STATUSES = (
('new', 'New'),
('sent', 'Sent'),
('closed', 'Closed')
)
class Form(models.Model):
resource = models.ForeignKey(Gesbac, on_delete=models.CASCADE)
form_id = models.CharField(max_length=64)
gesbac_id = models.CharField(max_length=64)
counter = models.IntegerField(default=0)
creation_datetime = models.DateTimeField(auto_now_add=True)
filename = models.CharField(max_length=128, null=True)
status = models.CharField(max_length=8, default='new',
choices=FORM_STATUSES
)
demand_data = JSONField()
card_data = JSONField()
class Meta:
get_latest_by = 'creation_datetime'
unique_together = ('form_id', 'counter')
def get_gesbac_id(self):
if not self.gesbac_id:
self.gesbac_id = self.form_id.replace('-', '0') + '%02d' % self.counter
self.save()
return self.gesbac_id
def get_filename(self):
if not self.filename:
timestamp = self.creation_datetime.strftime('%y%m%d-%H%M%S')
self.filename = '%s%s-%s.csv' % (self.resource.output_files_prefix,
timestamp, self.get_gesbac_id())
self.save()
return self.filename
def send(self):
with self.resource.outcoming_sftp.client() as client:
with client.open(self.get_filename(), mode='wb') as fd:
if six.PY3:
fd = io.TextIOWrapper(fd, encoding=FILES_ENCODING)
writer = csv.writer(fd, delimiter=CSV_DELIMITER)
for row in self.demand_data:
# encode strings to ASCII
if six.PY2:
row = [
item.encode(FILES_ENCODING) if isinstance(item, six.string_types)
else item for item in row
]
writer.writerow(row)
self.status = 'sent'
self.save()