toulouse-foederis: add candidature endpoint (#77524) #246

Merged
csechet merged 1 commits from wip/77524-endpoint-candidature into main 2023-05-19 11:40:10 +02:00
2 changed files with 403 additions and 0 deletions

View File

@ -30,6 +30,229 @@ from passerelle.utils.api import endpoint
from passerelle.utils.json import datasource_array_schema, datasource_schema, response_schema
def boolean_field(description):
return {
'description': description,
'oneOf': [
{'type': 'boolean'},
{
'type': 'string',
'pattern': '[Oo]|[Nn]|[Tt][Rr][Uu][Ee]|[Ff][Aa][Ll][Ss][Ee]|1|0',
'pattern_description': _(
'Values "0", "1", "O", "N", "true" or "false" are allowed (case insensitive).'
csechet marked this conversation as resolved Outdated

Peut-être laisser la porte ouverte ici, pour être plus souple si les valeurs possibles venaient à changer.

Peut-être laisser la porte ouverte ici, pour être plus souple si les valeurs possibles venaient à changer.
),
},
],
}
def get_bool(obj):
if obj is True or str(obj).lower() in ['true', 'o', '1']:
return True
if obj is False or str(obj).lower() in ['false', 'n', '0']:
return False
return obj
APPLICATION_SCHEMA = {
'$schema': 'http://json-schema.org/draft-04/schema#',
'title': 'Toulouse Foederis application',

Une petite typo ici: association

Une petite typo ici: association

Bien vu et surtout c'est pas du tout AstreGS, c'est resté d'un copier / coller : j'ai corrigé.

Bien vu et surtout c'est pas du tout AstreGS, c'est resté d'un copier / coller : j'ai corrigé.
'description': '',
'type': 'object',
'properties': {
'type': {
'description': _('Application Type (External or Internal).'),
'type': 'string',
},
'announce_id': {
'description': _('ID of the concerned job offer.'),
'type': 'string',
'pattern': '^[0-9]*$',
csechet marked this conversation as resolved Outdated

C'est nouveau dans passerelle, on parle bien de ça ?
https://json-schema.org/understanding-json-schema/reference/string.html#built-in-formats
J'ai l'impression (je me trompe peut-être) que la pratique courante est de formater les date en anglais depuis w.c.s : {{ form_var_date|date:'Y-m-d' }}
Auquel cas peut-être ne pas prendre de risque et faire comme dans les autres connecteurs :

'type': 'string',
'pattern': '^[0-9]{4}-[0-9]{2}-[0-9]{2}$',
C'est nouveau dans passerelle, on parle bien de ça ? https://json-schema.org/understanding-json-schema/reference/string.html#built-in-formats J'ai l'impression (je me trompe peut-être) que la pratique courante est de formater les date en anglais depuis w.c.s : {{ form_var_date|date:'Y-m-d' }} Auquel cas peut-être ne pas prendre de risque et faire comme dans les autres connecteurs : ``` 'type': 'string', 'pattern': '^[0-9]{4}-[0-9]{2}-[0-9]{2}$', ```
},
'civility': {
'description': _("ID of an element of the data source 'civilite'."),
'type': 'string',
'pattern': '^[0-9]+$',
},
'first_name': {
'description': _('Applicant first name.'),
'type': 'string',
csechet marked this conversation as resolved Outdated

Tu n'as pas précisé de type ici.

Tu n'as pas précisé de type ici.
},
'last_name': {
'description': _('Applicant last name.'),
'type': 'string',
csechet marked this conversation as resolved Outdated

'type': 'boolean'
Je pense qu'il ne faut pas présumer que w.c.s. enverra toujours False/True.
Voir ce qui est fait du côté d'Axel : accepter aussi les chaînes Vrai/Faux, O/1, ...

'type': 'boolean' Je pense qu'il ne faut pas présumer que w.c.s. enverra toujours False/True. Voir ce qui est fait du côté d'Axel : accepter aussi les chaînes Vrai/Faux, O/1, ...
},
'gender': {
'description': _('Applicant gender.'),
'type': 'string',
'enum': ['H', 'F', ''],
},
'birth_date': {
'description': _('Applicant birth date.'),
'type': 'string',
'pattern': '^([0-9]{4}-[0-9]{2}-[0-9]{2})?$',
},
'nationality': {
'description': _("ID of an element of the data source 'nationalite'."),
'type': 'string',
'pattern': '^[0-9]+$',
},
'work_authorization_end_date': {
'description': _("Applicant end of working authorization, if nationality is 'other'."),
'example_value': '2023-04-05',
'type': 'string',
'pattern': '^([0-9]{4}-[0-9]{2}-[0-9]{2})?$',
},
'rqth': boolean_field(_('RQTH.')),
'rqth_end_date': {
'description': _('End of RQTH, or none if not applicable.'),
'type': 'string',
'pattern': '^([0-9]{4}-[0-9]{2}-[0-9]{2})?$',
},
'driving_license': {
'description': _('Driving license.'),
'type': 'string',
},
'fimo': boolean_field(_('FIMO licence.')),
'fimo_delivrance_date': {
'description': _('FIMO licence delivrance date.'),
'type': 'string',
'pattern': '^([0-9]{4}-[0-9]{2}-[0-9]{2})?$',
},
'fimo_end_validity_date': {
'description': _('FIMO licence end validity date.'),
'type': 'string',
'pattern': '^([0-9]{4}-[0-9]{2}-[0-9]{2})?$',
},
'current_situation': {
'description': _("ID of an element of the data source 'situation-actuelle'."),
'type': 'string',
'pattern': '^[0-9]+$',
},
'agent_collectivity': {
'description': _("Agent's collectivity"),
'type': 'string',
},
'availability_start_date': {
'description': _('Applicant availability start date.'),
'type': 'string',
'pattern': '^([0-9]{4}-[0-9]{2}-[0-9]{2})?$',
},
'availability_end_date': {
'description': _('Applicant availability end date.'),
'type': 'string',
'pattern': '^([0-9]{4}-[0-9]{2}-[0-9]{2})?$',
},
'salary_expectations': {
'description': _('Applicant salary expectations.'),
'type': 'string',
},
'address': {
'description': _('Applicant address.'),
'type': 'string',
},
'address_complement': {
'description': _('Applicant address complement.'),
'type': 'string',
},
'zip': {
'description': _('Applicant zip code.'),
'type': 'string',
},
'city': {
'description': _('Applicant city.'),
'type': 'string',
},
'phone': {
'description': _('Applicant phone number.'),
'type': 'string',
},
'email': {
'description': _('Applicant email.'),
'type': 'string',
},
'contract_start_date': {
'description': _('Applicant contract start date.'),
'type': 'string',
'pattern': '^([0-9]{4}-[0-9]{2}-[0-9]{2})?$',
},
'contract_end_date': {
'description': _('Applicant contract end date.'),
'type': 'string',
'pattern': '^([0-9]{4}-[0-9]{2}-[0-9]{2})?$',
},
'additional_informations': {
'description': _('Application information complement.'),
'type': 'string',
},
'origin': {
'description': _("ID of an element of the data source 'origine-candidature'."),
'pattern': '^[0-9]*$',
'type': 'string',
},
'origin_precisions': {
'description': _("Precisions if 'origine' is 'other'."),
'type': 'string',
},
'rgpd_agreement': boolean_field(_('RGPD agreement.')),
'job_type': {
'description': _("ID of an element of the data source 'type-emploi'."),
'type': 'string',
'pattern': '^[0-9]*$',
},
'job_realm': {
'description': _("ID of an element of the data source 'domaine-emploi'."),
'type': 'string',
'pattern': '^[0-9]*$',
},
'job_family': {
'description': _("ID of an element of the data source 'sous-domaine-emploi'."),
'type': 'string',
'pattern': '^[0-9]*$',
},
'job': {
'description': _("ID of an element of the data source 'emploi'."),
'type': 'string',
'pattern': '^[0-9]*$',
},
'desired_work_time': {
'description': _('TC / TNC.'),
'type': 'string',
'enum': ['TC', 'TNC'],
},
'internship_duration': {
'description': _('Duration of the desired internship.'),
'type': 'string',
},
'school_name': {
'description': _("Candidate trainee's school name."),
'type': 'string',
},
'diploma_name': {
'description': _("Candidate trainee's diploma name."),
'type': 'string',
},
'diploma_speciality': {
'description': _("Candidate trainee's diploma speciality."),
'type': 'string',
},
'aimed_diploma_level': {
'description': _("ID of an element of the data source 'niveau-diplome'."),
'type': 'string',
'pattern': '^[0-9]*$',
},
'last_obtained_diploma': {
'description': _("Candidate trainee's last obtained diploma."),
'type': 'string',
},
'last_course_taken': {
'description': _("Candidate trainee's last taken course."),
'type': 'string',
},
},
}
class UpdateError(Exception):
pass
@ -295,6 +518,78 @@ class Resource(BaseResource, HTTPResource):
with document.pdf.open() as fd:
return HttpResponse(fd, content_type='application/pdf')
@endpoint(
perm="can_access",
name="create-application",
post={
"description": _("Creates an application"),
"request_body": {"schema": {"application/json": APPLICATION_SCHEMA}},
csechet marked this conversation as resolved Outdated

Ne fonctionne pas, faire comme c'est fait ailleurs :
'rqth': 'O' if post_data['rqth'] else 'N'

Ne fonctionne pas, faire comme c'est fait ailleurs : `'rqth': 'O' if post_data['rqth'] else 'N'`
},
)
def create_application(self, request, post_data):
def _get_id(field_name):
id = post_data.get(field_name, None)
if id is None or id == '':
return None
return int(id)
request_data = {
'type_de_candidature': post_data.get('type', 'E'),
'annonce': _get_id('announce_id'),
self.REFERENTIELS_FKEYS['civilite']: _get_id('civility'),
'firstName': post_data.get('first_name', None),
'lastName': post_data.get('last_name', None),
'sexe': post_data.get('gender', None),
'date_de_naissance': post_data.get('birth_date', None),
self.REFERENTIELS_FKEYS['nationalite']: _get_id('nationality'),
'date_fin_autorisation_de_travail': post_data.get('work_authorization_end_date', None),
'rqth': 'O' if get_bool(post_data.get('rqth', False)) else 'N',
'date_fin_rqth': post_data.get('rqth_end_date', None),
'permis_de_conduire': post_data.get('driving_license', None),
'fimo': 'O' if get_bool(post_data.get('fimo', False)) else 'N',
'Date_delivrance_fimo': post_data.get('fimo_delivrance_date', None),
'date_fin_validite_fimo': post_data.get('fimo_end_validity_date', None),
self.REFERENTIELS_FKEYS['situation_actuelle']: _get_id('current_situation'),
'collectivite_agent': post_data.get('agent_collectivity', None),
'date_debut_disponibilite': post_data.get('availability_start_date', None),
'date_fin_disponibilite': post_data.get('availability_end_date', None),
'pretentions_salariales': post_data.get('salary_expectations', None),
'adresse': post_data.get('address', None),
'adresse_ligne_2': post_data.get('address_complement', None),
'code_postal': post_data.get('zip', None),
'ville': post_data.get('city', None),
'telephone': post_data.get('phone', None),
'email': post_data.get('email', None),

Quels sont les champs qui nécessite de ne pas être passés lorsqu'il sont vides ?
C'est spécifié quelque part dans le excel ?
Si c'est vraiment utile, alors peut-être ajouter un test qui donne le cas d'usage.

Quels sont les champs qui nécessite de ne pas être passés lorsqu'il sont vides ? C'est spécifié quelque part dans le excel ? Si c'est vraiment utile, alors peut-être ajouter un test qui donne le cas d'usage.

Le Excel dit n'importe quoi, tous les champs sont optionnels dans les faits. Ici on supprime juste les valeurs absente du post. Il n'y a pas de cas d'usage, ils seront tous transmis, c'est juste pour éviter de sauvegarder n'importe quoi dans Foederis s'il y a un oubli à un moment donné (rappel que ce connecteur n'a rien de générique : on a un paramétrage ad-hoc en face). J'ai supprimé des champs dans les tests pour vérifier qu'ils sont bien exclus lors de l'appel Foederis.

Edit: En fait il y a des cas où des champs sont laissés vides (des champs spécifiques à une candidature de stage par ex.).

Le Excel dit n'importe quoi, tous les champs sont optionnels dans les faits. Ici on supprime juste les valeurs absente du post. Il n'y a pas de cas d'usage, ils seront tous transmis, c'est juste pour éviter de sauvegarder n'importe quoi dans Foederis s'il y a un oubli à un moment donné (rappel que ce connecteur n'a rien de générique : on a un paramétrage ad-hoc en face). J'ai supprimé des champs dans les tests pour vérifier qu'ils sont bien exclus lors de l'appel Foederis. Edit: En fait il y a des cas où des champs sont laissés vides (des champs spécifiques à une candidature de stage par ex.).

Désolé, je n'arrive pas à me projeter.
Est-ce que tu peux poser ton WF de test sur la recette, stp ?
(j'ai pas trouvé : https://demarches-montoulouse.test.entrouvert.org/backoffice/workflows/247/status/1/items/1/)

Désolé, je n'arrive pas à me projeter. Est-ce que tu peux poser ton WF de test sur la recette, stp ? (j'ai pas trouvé : https://demarches-montoulouse.test.entrouvert.org/backoffice/workflows/247/status/1/items/1/)
'date_debut_contrat': post_data.get('contract_start_date', None),
'date_fin_contrat': post_data.get('contract_end_date', None),
'complement_information_candidature': post_data.get('additional_informations', None),
self.REFERENTIELS_FKEYS['origine_candidature']: _get_id('origin'),
'precision_origine_candidature': post_data.get('origin_precisions', None),
'accord_RGPD': get_bool(post_data.get('rgpd_agreement', False)),
self.REFERENTIELS_FKEYS['type_emploi']: _get_id('job_type'),
self.REFERENTIELS_FKEYS['domaine_emploi']: _get_id('job_realm'),
self.REFERENTIELS_FKEYS['sous_domaine_emploi']: _get_id('job_family'),
self.REFERENTIELS_FKEYS['emploi']: _get_id('job'),
'temps_de_travail_souhaite': post_data.get('desired_work_time', None),
'duree_du_contrat_de_stage_apprentissage': post_data.get('internship_duration', None),
'ecole_centre_de_formation_mission_loc': post_data.get('school_name', None),
'intitule_diplome_vise': post_data.get('diploma_name', None),
'specialite_diplome': post_data.get('diploma_speciality', None),
self.REFERENTIELS_FKEYS['niveau_diplome']: _get_id('aimed_diploma_level'),
'dernier_diplome_obtenu': post_data.get('last_obtained_diploma', None),
'derniere_classe_suivie': post_data.get('last_course_taken', None),
}
request_data = {k: v for k, v in request_data.items() if v is not None and v != ''}
results = self.http_request(
'POST', 'data/candidature?viewIntegrationName=api_publik', json=request_data
)
return {
'err': 0,
'data': {'application_id': results[0]['id']},
}
@endpoint(
description=_('List announces'),
long_description=_(

View File

@ -405,6 +405,114 @@ class TestEndpoints:
app.get('/toulouse-foederis/foederis/announce/111/pdf/', status=404)
assert response.headers['content-type'] == 'application/pdf'
def test_create_application(self, resource, app):

Cette variable n'est pas utilisée (étrange que pylint ne l'ai pas vu).

Cette variable n'est pas utilisée (étrange que pylint ne l'ai pas vu).

Bien vu. C'est une variable globale, pylint ne peut pas deviner qu'elle n'est pas utilisée dans un autre module.

Bien vu. C'est une variable globale, pylint ne peut pas deviner qu'elle n'est pas utilisée dans un autre module.
@httmock.urlmatch(path=r'^.*/data/candidature$')
def handler(url, request):
assert request.headers['content-type'] == 'application/json'
assert request.headers['api-key'] == APIKEY
payload = json.loads(request.body)
assert payload == {
"type_de_candidature": "E",
"annonce": 524522,
"R60284409": 170013,
"firstName": "John",
"lastName": "Doe",
"sexe": "H",
"date_de_naissance": "1985-03-06",
"R1249730": 93421,
"date_fin_autorisation_de_travail": "2023-05-09",
"rqth": "N",
"date_fin_rqth": "2023-05-08",
"permis_de_conduire": "A,B",
"fimo": "O",
"Date_delivrance_fimo": "2023-05-07",
"date_fin_validite_fimo": "2023-05-08",
"R1258320": 1258319,
"collectivite_agent": "Mairie de Toulouse",
"date_debut_disponibilite": "2023-05-02",
"date_fin_disponibilite": "2023-05-01",
"pretentions_salariales": "1000",
"adresse": "12 Sesame Street",
"code_postal": "77710",
"ville": "Nemours",
"telephone": "+33 636656565",
"email": "csechet@entrouvert.com",
"date_debut_contrat": "2023-05-06",
"date_fin_contrat": "2023-05-04",
"complement_information_candidature": "I need money.",
"R1261279": 1561049,
"accord_RGPD": True,
"R1249707": 157193,
"R60845221": 5776395,
"R60845244": 5776394,
"temps_de_travail_souhaite": "TC",
"duree_du_contrat_de_stage_apprentissage": "2h",
"ecole_centre_de_formation_mission_loc": "Ecole de la vie",
"intitule_diplome_vise": "BE",
"specialite_diplome": "Curling",
"R1249737": 1124022,
"dernier_diplome_obtenu": "BAC",
"derniere_classe_suivie": "Terminale",
}
return httmock.response(200, json.dumps({"code": 200, "results": [{"id": 42}]}))
@httmock.urlmatch()
def error_handler(url, request):
assert False, 'should not be reached'
with httmock.HTTMock(handler, error_handler):
response = app.post_json(
'/toulouse-foederis/foederis/create-application',
params={
"additional_informations": "I need money.",
"address": "12 Sesame Street",
"address_complement": "",
"agent_collectivity": "Mairie de Toulouse",
"aimed_diploma_level": "1124022",
"announce_id": "0524522",
"availability_end_date": "2023-05-01",
"availability_start_date": "2023-05-02",
"birth_date": "1985-03-06",
"city": "Nemours",
"civility": "170013",
"contract_end_date": "2023-05-04",
"contract_start_date": "2023-05-06",
"current_situation": "1258319",
"desired_work_time": "TC",
"diploma_name": "BE",
"diploma_speciality": "Curling",
"driving_license": "A,B",
"email": "csechet@entrouvert.com",
"fimo": "o",
"fimo_delivrance_date": "2023-05-07",
"fimo_end_validity_date": "2023-05-08",
"first_name": "John",
"gender": "H",
"internship_duration": "2h",
"job_family": "5776394",
"job_realm": "5776395",
"job_type": "157193",
"last_course_taken": "Terminale",
"last_name": "Doe",
"last_obtained_diploma": "BAC",
"nationality": "93421",
"origin": "1561049",
"origin_precisions": "",
"phone": "+33 636656565",
"rgpd_agreement": "tRuE",
"rqth": False,
"rqth_end_date": "2023-05-08",
"salary_expectations": "1000",
"school_name": "Ecole de la vie",
"type": "E",
"work_authorization_end_date": "2023-05-09",
"zip": "77710",
},
)
assert response.json["data"]["application_id"] == 42
def test_migration_0003_no_null_no_charfield(migration):
with connection.cursor() as cur: