passerelle/passerelle/apps/gesbac/models.py

358 lines
12 KiB
Python

# 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 IntegrityError, models, transaction
from django.db.models import JSONField
from django.http import Http404
from django.utils.translation import gettext_lazy as _
from passerelle.base.models import BaseResource
from passerelle.utils import SFTPField
from passerelle.utils.api import APIError, endpoint
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 = '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:
content = io.TextIOWrapper(fd, encoding=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().hourly()
self.get_responses()
def send_demand(self, form_id):
form = Form.objects.get(id=form_id)
form.send()
@endpoint(
name='create-demand',
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:
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',
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(default=dict)
card_data = JSONField(default=dict)
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:
fd = io.TextIOWrapper(fd, encoding=FILES_ENCODING)
writer = csv.writer(fd, delimiter=CSV_DELIMITER)
for row in self.demand_data:
writer.writerow(row)
self.status = 'sent'
self.save()