It manages synchronization of meetings agendas with the ANTS hub.
This commit is contained in:
parent
05bce1763e
commit
e09da74527
|
@ -0,0 +1,73 @@
|
|||
# chrono - agendas system
|
||||
# Copyright (C) 2023 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 requests
|
||||
from django.conf import settings
|
||||
from requests.adapters import HTTPAdapter
|
||||
from urllib3.util.retry import Retry
|
||||
|
||||
|
||||
class AntsHubException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def make_http_session(retries=3):
|
||||
session = requests.Session()
|
||||
retry = Retry(
|
||||
total=retries,
|
||||
read=retries,
|
||||
connect=retries,
|
||||
backoff_factor=0.5,
|
||||
status_forcelist=(502, 503),
|
||||
)
|
||||
adapter = HTTPAdapter(max_retries=retry)
|
||||
session.mount('http://', adapter)
|
||||
session.mount('https://', adapter)
|
||||
return session
|
||||
|
||||
|
||||
def make_url(path):
|
||||
return f'{settings.CHRONO_ANTS_HUB_URL}{path}'
|
||||
|
||||
|
||||
def ping(timeout=1):
|
||||
session = make_http_session()
|
||||
try:
|
||||
response = session.get(make_url('ping/'), timeout=timeout)
|
||||
response.raise_for_status()
|
||||
err = response.json()['err']
|
||||
if err != 0:
|
||||
raise AntsHubException(err)
|
||||
except requests.Timeout:
|
||||
pass
|
||||
except (TypeError, KeyError, requests.RequestException) as e:
|
||||
raise AntsHubException(str(e))
|
||||
|
||||
|
||||
def push_rendez_vous_disponibles(payload):
|
||||
session = make_http_session()
|
||||
try:
|
||||
response = session.post(make_url('rendez-vous-disponibles/'), json=payload)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
err = data['err']
|
||||
if err != 0:
|
||||
raise AntsHubException(err)
|
||||
return data
|
||||
except requests.Timeout:
|
||||
return True
|
||||
except (TypeError, KeyError, requests.RequestException) as e:
|
||||
raise AntsHubException(str(e))
|
|
@ -0,0 +1,26 @@
|
|||
# chrono - agendas system
|
||||
# Copyright (C) 2023 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/>.
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from chrono.apps.ants_hub import models
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Synchronize agendas with the ANTS hub.'
|
||||
|
||||
def handle(self, **options):
|
||||
models.City.push()
|
|
@ -0,0 +1,159 @@
|
|||
# Generated by Django 3.2.18 on 2023-04-06 00:34
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
import chrono.apps.ants_hub.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('agendas', '0152_auto_20230331_0834'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='City',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
('name', chrono.apps.ants_hub.models.CharField(unique=True, verbose_name='Name')),
|
||||
(
|
||||
'url',
|
||||
chrono.apps.ants_hub.models.URLField(
|
||||
blank=True,
|
||||
default=chrono.apps.ants_hub.models.get_portal_url,
|
||||
verbose_name='Portal URL',
|
||||
),
|
||||
),
|
||||
('logo_url', chrono.apps.ants_hub.models.URLField(blank=True, verbose_name='Logo URL')),
|
||||
(
|
||||
'meeting_url',
|
||||
chrono.apps.ants_hub.models.URLField(
|
||||
blank=True,
|
||||
help_text='URL of the web form to make a booking.',
|
||||
verbose_name='Booking URL',
|
||||
),
|
||||
),
|
||||
(
|
||||
'management_url',
|
||||
chrono.apps.ants_hub.models.URLField(
|
||||
blank=True,
|
||||
help_text='Generic URL to find and manage an existing booking.',
|
||||
verbose_name='Booking management URL',
|
||||
),
|
||||
),
|
||||
(
|
||||
'cancel_url',
|
||||
chrono.apps.ants_hub.models.URLField(
|
||||
blank=True,
|
||||
help_text='Generic URL to find and cancel an existing booking.',
|
||||
verbose_name='Booking cancellation URL',
|
||||
),
|
||||
),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
|
||||
('last_update', models.DateTimeField(auto_now=True, verbose_name='Last update')),
|
||||
('full_sync', models.BooleanField(default=False, verbose_name='Full sync')),
|
||||
('last_sync', models.DateTimeField(editable=False, null=True, verbose_name='Last sync')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'City',
|
||||
'verbose_name_plural': 'Cities',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Place',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
('name', chrono.apps.ants_hub.models.CharField(verbose_name='Name')),
|
||||
('address', chrono.apps.ants_hub.models.CharField(verbose_name='Address')),
|
||||
('zipcode', chrono.apps.ants_hub.models.CharField(verbose_name='Code postal')),
|
||||
('city_name', chrono.apps.ants_hub.models.CharField(verbose_name='City name')),
|
||||
('longitude', models.FloatField(default=2.476, verbose_name='Longitude')),
|
||||
('latitude', models.FloatField(default=46.596, verbose_name='Latitude')),
|
||||
('url', chrono.apps.ants_hub.models.URLField(blank=True, verbose_name='Portal URL')),
|
||||
('logo_url', chrono.apps.ants_hub.models.URLField(blank=True, verbose_name='Logo URL')),
|
||||
(
|
||||
'meeting_url',
|
||||
chrono.apps.ants_hub.models.URLField(
|
||||
blank=True,
|
||||
help_text='URL of the web form to make a booking.',
|
||||
verbose_name='Booking URL',
|
||||
),
|
||||
),
|
||||
(
|
||||
'management_url',
|
||||
chrono.apps.ants_hub.models.URLField(
|
||||
blank=True,
|
||||
help_text='Generic URL to find and manage an existing booking.',
|
||||
verbose_name='Booking management URL',
|
||||
),
|
||||
),
|
||||
(
|
||||
'cancel_url',
|
||||
chrono.apps.ants_hub.models.URLField(
|
||||
blank=True,
|
||||
help_text='Generic URL to find and cancel an existing booking.',
|
||||
verbose_name='Booking cancellation URL',
|
||||
),
|
||||
),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
|
||||
('last_update', models.DateTimeField(auto_now=True, verbose_name='Last update')),
|
||||
('full_sync', models.BooleanField(default=False, verbose_name='Full sync')),
|
||||
('last_sync', models.DateTimeField(editable=False, null=True, verbose_name='Last sync')),
|
||||
(
|
||||
'city',
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='places',
|
||||
to='ants_hub.city',
|
||||
verbose_name='City',
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'place',
|
||||
'verbose_name_plural': 'places',
|
||||
'unique_together': {('city', 'name')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PlaceAgenda',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
('setting', models.JSONField(blank=True, default=dict, verbose_name='Setting')),
|
||||
(
|
||||
'agenda',
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='+',
|
||||
related_query_name='ants_place',
|
||||
to='agendas.agenda',
|
||||
verbose_name='Agenda',
|
||||
),
|
||||
),
|
||||
(
|
||||
'place',
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='agendas',
|
||||
to='ants_hub.place',
|
||||
verbose_name='Place',
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('place', 'agenda')},
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,355 @@
|
|||
# chrono - agendas system
|
||||
# Copyright (C) 2023 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 collections
|
||||
import datetime
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.db import models, transaction
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from chrono.agendas.models import Agenda, Booking
|
||||
from chrono.utils.timezone import localtime, now
|
||||
|
||||
from .hub import push_rendez_vous_disponibles
|
||||
|
||||
# fields without max_length problems
|
||||
|
||||
|
||||
class CharField(models.TextField):
|
||||
'''TextField using forms.TextInput as widget'''
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
defaults = {'widget': forms.TextInput}
|
||||
defaults.update(**kwargs)
|
||||
return super().formfield(**defaults)
|
||||
|
||||
|
||||
class URLField(models.URLField):
|
||||
'''URLField using a TEXT column for storage'''
|
||||
|
||||
def get_internal_type(self):
|
||||
return 'TextField'
|
||||
|
||||
|
||||
class CityManager(models.Manager):
|
||||
def get_by_natural_key(self, name):
|
||||
return self.get(name=name)
|
||||
|
||||
|
||||
def get_portal_url():
|
||||
template_vars = getattr(settings, 'TEMPLATE_VARS', {})
|
||||
return template_vars.get('portal_url', '')
|
||||
|
||||
|
||||
class City(models.Model):
|
||||
name = CharField(verbose_name=_('Name'), unique=True)
|
||||
url = URLField(verbose_name=_('Portal URL'), default=get_portal_url, blank=True)
|
||||
logo_url = URLField(verbose_name=_('Logo URL'), blank=True)
|
||||
meeting_url = URLField(
|
||||
verbose_name=_('Booking URL'), help_text=_('URL of the web form to make a booking.'), blank=True
|
||||
)
|
||||
management_url = URLField(
|
||||
verbose_name=_('Booking management URL'),
|
||||
help_text=_('Generic URL to find and manage an existing booking.'),
|
||||
blank=True,
|
||||
)
|
||||
cancel_url = URLField(
|
||||
verbose_name=_('Booking cancellation URL'),
|
||||
help_text=_('Generic URL to find and cancel an existing booking.'),
|
||||
blank=True,
|
||||
)
|
||||
created = models.DateTimeField(verbose_name=_('Created'), auto_now_add=True)
|
||||
last_update = models.DateTimeField(verbose_name=_('Last update'), auto_now=True)
|
||||
full_sync = models.BooleanField(
|
||||
verbose_name=_('Full synchronization next time'), default=False, editable=False
|
||||
)
|
||||
last_sync = models.DateTimeField(verbose_name=_('Last synchronization'), null=True, editable=False)
|
||||
|
||||
objects = CityManager()
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.name}'
|
||||
|
||||
def natural_key(self):
|
||||
return (self.name,)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('chrono-manager-ants-hub')
|
||||
|
||||
def details(self):
|
||||
details = []
|
||||
for key in ['url', 'logo_url', 'management_url', 'cancel_url', 'full_sync']:
|
||||
value = getattr(self, key, None)
|
||||
if value:
|
||||
verbose_name = type(self)._meta.get_field(key).verbose_name
|
||||
details.append((verbose_name, value))
|
||||
return details
|
||||
|
||||
@classmethod
|
||||
@transaction.atomic(savepoint=False)
|
||||
def push(cls):
|
||||
reference = now()
|
||||
# prevent concurrent pushes with locks
|
||||
cities = list(cls.objects.select_for_update())
|
||||
push_rendez_vous_disponibles(
|
||||
{
|
||||
'collectivites': [city.export_to_push() for city in cities],
|
||||
}
|
||||
)
|
||||
City.objects.update(last_sync=reference)
|
||||
Place.objects.update(last_sync=reference)
|
||||
|
||||
def export_to_push(self):
|
||||
payload = {
|
||||
'full': True,
|
||||
'id': str(self.pk),
|
||||
'nom': self.name,
|
||||
'url': self.url,
|
||||
'logo_url': self.logo_url,
|
||||
'rdv_url': self.meeting_url,
|
||||
'gestion_url': self.management_url,
|
||||
'annulation_url': self.cancel_url,
|
||||
'lieux': [place.export_to_push() for place in self.places.all()],
|
||||
}
|
||||
return payload
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('City')
|
||||
verbose_name_plural = _('Cities')
|
||||
|
||||
|
||||
class PlaceManager(models.Manager):
|
||||
def get_by_natural_key(self, name, *args):
|
||||
return self.get(name=name, city=City.objects.get_by_natural_key(*args))
|
||||
|
||||
|
||||
class Place(models.Model):
|
||||
city = models.ForeignKey(
|
||||
verbose_name=_('City'), to=City, related_name='places', on_delete=models.CASCADE, editable=False
|
||||
)
|
||||
name = CharField(verbose_name=_('Name'))
|
||||
address = CharField(verbose_name=_('Address'))
|
||||
zipcode = CharField(verbose_name=_('Code postal'))
|
||||
city_name = CharField(verbose_name=_('City name'))
|
||||
longitude = models.FloatField(verbose_name=_('Longitude'), default=2.476)
|
||||
latitude = models.FloatField(verbose_name=_('Latitude'), default=46.596)
|
||||
url = URLField(verbose_name=_('Portal URL'), blank=True)
|
||||
logo_url = URLField(verbose_name=_('Logo URL'), blank=True)
|
||||
meeting_url = URLField(
|
||||
verbose_name=_('Booking URL'), help_text=_('URL of the web form to make a booking.'), blank=True
|
||||
)
|
||||
management_url = URLField(
|
||||
verbose_name=_('Booking management URL'),
|
||||
help_text=_('Generic URL to find and manage an existing booking.'),
|
||||
blank=True,
|
||||
)
|
||||
cancel_url = URLField(
|
||||
verbose_name=_('Booking cancellation URL'),
|
||||
help_text=_('Generic URL to find and cancel an existing booking.'),
|
||||
blank=True,
|
||||
)
|
||||
created = models.DateTimeField(verbose_name=_('Created'), auto_now_add=True)
|
||||
last_update = models.DateTimeField(verbose_name=_('Last update'), auto_now=True)
|
||||
full_sync = models.BooleanField(verbose_name=_('Full synchronization'), default=False)
|
||||
last_sync = models.DateTimeField(verbose_name=_('Last synchronization'), null=True, editable=False)
|
||||
|
||||
objects = PlaceManager()
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.name}'
|
||||
|
||||
def natural_key(self):
|
||||
return (self.name,) + self.city.natural_key()
|
||||
|
||||
natural_key.dependencies = ['ants_hub.city']
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('chrono-manager-ants-hub-place', kwargs={'city_pk': self.city_id, 'pk': self.pk})
|
||||
|
||||
def url_details(self):
|
||||
details = []
|
||||
for key in ['url', 'logo_url', 'management_url', 'cancel_url', 'full_sync']:
|
||||
value = getattr(self, key, None)
|
||||
if value:
|
||||
verbose_name = type(self)._meta.get_field(key).verbose_name
|
||||
details.append((verbose_name, value))
|
||||
return details
|
||||
|
||||
def export_to_push(self):
|
||||
payload = {
|
||||
'full': True,
|
||||
'id': str(self.pk),
|
||||
'nom': self.name,
|
||||
'numero_rue': self.address,
|
||||
'code_postal': self.zipcode,
|
||||
'ville': self.city_name,
|
||||
'longitude': self.longitude,
|
||||
'latitude': self.latitude,
|
||||
'url': self.url,
|
||||
'rdv_url': self.meeting_url,
|
||||
'gestion_url': self.management_url,
|
||||
'annulation_url': self.cancel_url,
|
||||
'plages': list(self.iter_open_dates()),
|
||||
'rdvs': list(self.iter_predemandes()),
|
||||
}
|
||||
return payload
|
||||
|
||||
def iter_open_dates(self):
|
||||
for place_agenda in self.agendas.all():
|
||||
yield from place_agenda.iter_open_dates()
|
||||
|
||||
def iter_predemandes(self):
|
||||
agendas = Agenda.objects.filter(ants_place__place=self)
|
||||
agendas |= Agenda.objects.filter(virtual_agendas__ants_place__place=self)
|
||||
agendas = set(agendas)
|
||||
bookings = (
|
||||
Booking.objects.filter(
|
||||
event__desk__agenda__in=agendas,
|
||||
event__start_datetime__gt=now(),
|
||||
extra_data__ants_identifiant_predemande__isnull=False,
|
||||
)
|
||||
.values_list(
|
||||
'extra_data__ants_identifiant_predemande', 'event__start_datetime', 'cancellation_datetime'
|
||||
)
|
||||
.order_by('event__state_datetime')
|
||||
)
|
||||
for identifiant_predemande, start_datetime, cancellation_datetime in bookings:
|
||||
if not isinstance(identifiant_predemande, str):
|
||||
continue
|
||||
rdv = {
|
||||
'id': identifiant_predemande,
|
||||
'date': start_datetime.isoformat(),
|
||||
}
|
||||
if cancellation_datetime is not None:
|
||||
rdv['annule'] = True
|
||||
yield rdv
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('place')
|
||||
verbose_name_plural = _('places')
|
||||
unique_together = [
|
||||
('city', 'name'),
|
||||
]
|
||||
|
||||
|
||||
class ANTSMeetingType(models.IntegerChoices):
|
||||
CNI = 1, _('CNI')
|
||||
PASSPORT = 2, _('Passport')
|
||||
CNI_PASSPORT = 3, _('CNI and passport')
|
||||
|
||||
@property
|
||||
def ants_name(self):
|
||||
return super().name.replace('_', '-')
|
||||
|
||||
|
||||
class ANTSPersonsNumber(models.IntegerChoices):
|
||||
ONE = 1, _('1 person')
|
||||
TWO = 2, _('2 persons')
|
||||
THREE = 3, _('3 persons')
|
||||
FOUR = 4, _('4 persons')
|
||||
|
||||
|
||||
class PlaceAgenda(models.Model):
|
||||
place = models.ForeignKey(
|
||||
verbose_name=_('Place'),
|
||||
to=Place,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='agendas',
|
||||
)
|
||||
agenda = models.ForeignKey(
|
||||
verbose_name=_('Agenda'),
|
||||
to='agendas.Agenda',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='+',
|
||||
related_query_name='ants_place',
|
||||
)
|
||||
setting = models.JSONField(verbose_name=_('Setting'), default=dict, blank=True)
|
||||
|
||||
def set_meeting_type_setting(self, meeting_type, key, value):
|
||||
assert key in ['ants_meeting_type', 'ants_persons_number']
|
||||
meeting_types = self.setting.setdefault('meeting-types', {})
|
||||
value = map(int, value)
|
||||
if key == 'ants_meeting_type':
|
||||
value = list(map(ANTSMeetingType, value))
|
||||
else:
|
||||
value = list(map(ANTSPersonsNumber, value))
|
||||
meeting_types.setdefault(str(meeting_type.slug), {})[key] = value
|
||||
|
||||
def get_meeting_type_setting(self, meeting_type, key):
|
||||
assert key in ['ants_meeting_type', 'ants_persons_number']
|
||||
meeting_types = self.setting.setdefault('meeting-types', {})
|
||||
value = meeting_types.get(str(meeting_type.slug), {}).get(key, [])
|
||||
value = map(int, value)
|
||||
if key == 'ants_meeting_type':
|
||||
value = list(map(ANTSMeetingType, value))
|
||||
else:
|
||||
value = list(map(ANTSPersonsNumber, value))
|
||||
return value
|
||||
|
||||
def iter_ants_meeting_types_and_persons(self):
|
||||
d = collections.defaultdict(set)
|
||||
for meeting_type in self.agenda.iter_meetingtypes():
|
||||
for ants_meeting_type in self.get_meeting_type_setting(meeting_type, 'ants_meeting_type'):
|
||||
for ants_persons_number in self.get_meeting_type_setting(meeting_type, 'ants_persons_number'):
|
||||
meeting_type.id = meeting_type.slug
|
||||
d[(meeting_type, ants_persons_number)].add(ants_meeting_type)
|
||||
for (meeting_type, ants_persons_number), ants_meeting_types in d.items():
|
||||
yield meeting_type, ants_persons_number, ants_meeting_types
|
||||
|
||||
@property
|
||||
def ants_properties(self):
|
||||
rdv = set()
|
||||
persons = set()
|
||||
for meeting_type in self.agenda.meetingtype_set.all():
|
||||
rdv.update(self.get_meeting_type_setting(meeting_type, 'ants_meeting_type'))
|
||||
persons.update(self.get_meeting_type_setting(meeting_type, 'ants_persons_number'))
|
||||
rdv = sorted(list(rdv))
|
||||
persons = sorted(list(persons))
|
||||
return [x.label for x in rdv] + [x.label for x in persons]
|
||||
|
||||
def __str__(self):
|
||||
return str(self.agenda)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return self.place.get_absolute_url()
|
||||
|
||||
def iter_open_dates(self):
|
||||
settings = list(self.iter_ants_meeting_types_and_persons())
|
||||
if not settings:
|
||||
return
|
||||
intervals = self.agenda.get_free_time(end_datetime=now() + datetime.timedelta(days=6 * 30))
|
||||
for start, end in intervals:
|
||||
for meeting_type, ants_persons_number, ants_meeting_types in settings:
|
||||
duration = datetime.timedelta(minutes=meeting_type.duration)
|
||||
if end - start < duration:
|
||||
continue
|
||||
yield {
|
||||
'date': localtime(start).date().isoformat(),
|
||||
# use naive local time representation
|
||||
'heure_debut': localtime(start).time().replace(tzinfo=None).isoformat(),
|
||||
'heure_fin': localtime(end).time().replace(tzinfo=None).isoformat(),
|
||||
'duree': meeting_type.duration,
|
||||
'personnes': int(ants_persons_number),
|
||||
'types_rdv': [x.ants_name for x in ants_meeting_types],
|
||||
}
|
||||
|
||||
class Meta:
|
||||
unique_together = [
|
||||
('place', 'agenda'),
|
||||
]
|
|
@ -0,0 +1,57 @@
|
|||
{% extends "chrono/manager_ants_hub_base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block appbar %}
|
||||
<h2>{% trans 'ANTS Hub' %}</h2>
|
||||
<span class="actions">
|
||||
<a class="extra-actions-menu-opener"></a>
|
||||
<ul class="extra-actions-menu">
|
||||
<li><a rel="popup" href="{% url 'chrono-manager-ants-hub-synchronize' %}">{% trans 'Synchronize agendas' %}</a></li>
|
||||
</ul>
|
||||
<a rel="popup" href="{% url 'chrono-manager-ants-hub-city-add' %}">{% trans 'New city' %}</a>
|
||||
</span>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if object_list %}
|
||||
{% for object in object_list %}
|
||||
<div class="section">
|
||||
<h3>
|
||||
<span>
|
||||
{{ object }}
|
||||
<a class="icon-edit" rel="popup" href="{% url 'chrono-manager-ants-hub-city-edit' pk=object.pk %}"></a>
|
||||
</span>
|
||||
<a class="button delete-button" rel="popup" href="{% url 'chrono-manager-ants-hub-city-delete' pk=object.pk %}">{% trans "Remove" %}</a>
|
||||
</h3>
|
||||
<div>
|
||||
<ul class="objects-list single-links">
|
||||
<p>
|
||||
{% if not object.last_sync %}{% trans "Never synchronized" %}{% else %}{% blocktrans with date=object.last_sync|date:"SHORT_DATETIME_FORMAT" %}Last synchronization on {{ date }}{% endblocktrans %}{% endif %}.
|
||||
</p>
|
||||
{% for label, value in object.details %}
|
||||
<p>{{ label }}: {{ value }}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% for place in object.places.all %}
|
||||
<li>
|
||||
<a href="{% url 'chrono-manager-ants-hub-place' city_pk=object.pk pk=place.pk %}">{{ place }}
|
||||
{% if place.agendas.count %}<span class="identifier">({% blocktrans count counter=place.agendas.count %}1 agenda{% plural %}{{ counter }} agendas{% endblocktrans %})</span>{% endif %}</a>
|
||||
<a class="delete" rel="popup" href="{% url 'chrono-manager-ants-hub-place-delete' city_pk=object.pk pk=place.pk %}">{% trans "remove" %}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<p>
|
||||
<a class="button" rel="popup" href="{% url 'chrono-manager-ants-hub-place-add' pk=object.pk %}">{% trans "Add place" %}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="big-msg-info">
|
||||
{% blocktrans trimmed %}
|
||||
This site doesn't have any city yet. Click on the "New city" button in the top
|
||||
right of the page to add a first one.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,19 @@
|
|||
{% extends "chrono/manager_home.html" %}
|
||||
{% load i18n gadjo %}
|
||||
|
||||
{% block appbar %}
|
||||
<h2>{% if object %}{{ object }}{% else %}{{ view.name }}{% endif %}</h2>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{{ form|with_template }}
|
||||
<div class="buttons">
|
||||
<button class="submit-button">{% trans "Save" %}</button>
|
||||
<a class="cancel" href="{% url 'chrono-manager-ants-hub' %}">{% trans 'Cancel' %}</a>
|
||||
</div>
|
||||
{% block after-form %}
|
||||
{% endblock %}
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -0,0 +1,57 @@
|
|||
{% extends "chrono/manager_home.html" %}
|
||||
{% load i18n gadjo %}
|
||||
|
||||
{% block appbar %}
|
||||
<h2><a href="{{ object.agenda.get_absolute_url }}">{{ object.agenda }}</a></h2>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<p>{% blocktrans with url=object.agenda.get_absolute_url name=object.agenda %}Configure the mapping between meeting types and ANTS meeting types for agenda <a href="{{ url }}">{{ name }}</a>.{% endblocktrans %}</p>
|
||||
{% if form.meeting_types %}
|
||||
<table class="main ants-setting">
|
||||
<tbody>
|
||||
{% for label, fields in form.field_by_labels %}
|
||||
<tr>
|
||||
<td class="meeting-type">{{ label }}</td>
|
||||
<td>
|
||||
{% for field in fields %}
|
||||
{% if field.errors %}
|
||||
<div class="error"><p>
|
||||
{% for error in field.errors %}
|
||||
{{ error }}{% if not forloop.last %}<br>{% endif %}
|
||||
{% endfor %}
|
||||
</p></div>
|
||||
{% endif %}
|
||||
{{ field }}
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="big-msg-info">{% blocktrans trimmed %}
|
||||
This agenda doesn't have any meeting type yet.
|
||||
{% endblocktrans %}</div>
|
||||
{% endif %}
|
||||
<script>
|
||||
$('form').on('click', 'label', function (event) {
|
||||
console.log(event);
|
||||
});
|
||||
$('form').on('change', 'input', function (event) {
|
||||
$(event.target).parent().toggleClass('checked');
|
||||
});
|
||||
$('form input:checked').each(function (i, elt) {
|
||||
$(elt).parent().toggleClass('checked');
|
||||
});
|
||||
</script>
|
||||
<div class="buttons">
|
||||
{% if form.meeting_types %}
|
||||
<button class="submit-button">{% trans "Save" %}</button>
|
||||
{% endif %}
|
||||
<a class="cancel" href="{% url 'chrono-manager-ants-hub' %}">{% trans 'Cancel' %}</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -0,0 +1,7 @@
|
|||
{% extends "chrono/manager_base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
<a href="{% url "chrono-manager-ants-hub" %}">{% trans "ANTS Hub" %}</a>
|
||||
{% endblock %}
|
|
@ -0,0 +1,78 @@
|
|||
{% extends "chrono/manager_ants_hub_base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
<a href=".">{{ view.place }}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block appbar %}
|
||||
<h2>{{ view.place }}</h2>
|
||||
<a rel="popup" class="delete-button" href="{% url 'chrono-manager-ants-hub-place-delete' city_pk=view.place.city_id pk=view.place.pk %}">{% trans "Remove" %}</a>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<p>
|
||||
{% if not view.place.last_sync %}{% trans "Never synchronized" %}{% else %}{% blocktrans with date=view.place.last_sync|date:"SHORT_DATETIME_FORMAT" %}Last synchronization on {{ date }}{% endblocktrans %}{% endif %}.
|
||||
</p>
|
||||
<div class="section">
|
||||
<h3>
|
||||
{% trans "Address" %}
|
||||
<a class="button" rel="popup" href="{% url 'chrono-manager-ants-hub-place-edit' city_pk=view.place.city_id pk=view.place.pk %}">{% trans "Edit" %}</a>
|
||||
</h3>
|
||||
<div>
|
||||
<p>
|
||||
{{ view.place.address }}</br>
|
||||
{{ view.place.zipcode }} {{ view.place.city_name }}
|
||||
</p>
|
||||
<p>
|
||||
{% trans "Geolocation:" %} {{ view.place.longitude }} {{ view.place.latitude }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h3>
|
||||
{% trans "URLs" %}
|
||||
<a class="button" rel="popup" href="{% url 'chrono-manager-ants-hub-place-url' city_pk=view.place.city_id pk=view.place.pk %}">{% trans "Edit" %}</a>
|
||||
</h3>
|
||||
<table class="main">
|
||||
<tbody>
|
||||
{% for label, value in view.place.url_details %}
|
||||
<tr><td>{{ label }}</td><td>{{ value }}</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h3>
|
||||
{% trans "Agendas" %}
|
||||
<a rel="popup" class="button" href="{% url 'chrono-manager-ants-hub-agenda-add' city_pk=view.place.city_id pk=view.place.pk %}">{% trans 'Add' %}</a>
|
||||
</h3>
|
||||
{% if object_list %}
|
||||
<ul class="objects-list single-links">
|
||||
{% for object in object_list %}
|
||||
<li {% if not object.ants_properties %}class="ants-setting-not-configured"{% endif %}>
|
||||
<a rel="popup" id="open-place-agenda-{{ object.pk }}" class="edit" href="{% url 'chrono-manager-ants-hub-agenda-edit' pk=object.pk city_pk=view.place.city_id place_pk=view.place.pk %}">
|
||||
<span class="label">{{ object }}</span>
|
||||
<span class="properties">({% if object.ants_properties %}{{ object.ants_properties|join:", " }}{% else %}{% trans "not configured" %}{% endif %})</span></a>
|
||||
<a class="delete" rel="popup" href="{% url 'chrono-manager-ants-hub-agenda-delete' pk=object.pk city_pk=view.place.city_id place_pk=view.place.pk %}">{% trans "remove" %}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if not object_list %}
|
||||
<div class="big-msg-info">
|
||||
{% blocktrans trimmed %}
|
||||
This site doesn't have any agenda yet. Click on the "New place" button in the top
|
||||
right of the page to add a first one.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<script>
|
||||
setTimeout(function () {
|
||||
$(window.location.hash).click();
|
||||
}, 100);
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -0,0 +1,17 @@
|
|||
{% extends "chrono/manager_ants_hub_add_form.html" %}
|
||||
{% load i18n gadjo %}
|
||||
|
||||
{% block after-form %}
|
||||
<script>
|
||||
$('form').on('change keyup', '#id_address, #id_zipcode, #id_city_name', function (event) {
|
||||
var q = $('#id_address').val() + ' ' + $('#id_zipcode').val() + ' ' + $('#id_city_name').val();
|
||||
console.log('q', q)
|
||||
var url = "https://api-adresse.data.gouv.fr/search/?q=" + encodeURIComponent(q);
|
||||
$.ajax(url).done(function (data) {
|
||||
var coords = data.features[0].geometry.coordinates;
|
||||
$('#id_longitude').val(coords[0]);
|
||||
$('#id_latitude').val(coords[1]);
|
||||
})
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -0,0 +1,19 @@
|
|||
{% extends "chrono/manager_home.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block appbar %}
|
||||
<h2>{% trans "Synchronize agendas" %}</h2>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<p>
|
||||
{% blocktrans %}Are you sure you want to synchronize your agendas with the ANTS now?{% endblocktrans %}
|
||||
</p>
|
||||
<div class="buttons">
|
||||
<button class="button" >{% trans 'Synchronize' %}</button>
|
||||
<a class="cancel" href="{% url 'chrono-manager-ants-hub' %}">{% trans 'Cancel' %}</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -0,0 +1,87 @@
|
|||
# chrono - agendas system
|
||||
# Copyright (C) 2023 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 functools
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import path
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
def view_decorator(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(request, *args, **kwargs):
|
||||
if not request.user.is_staff:
|
||||
raise PermissionDenied
|
||||
if not settings.CHRONO_ANTS_HUB_URL:
|
||||
messages.info(
|
||||
request, _('Configure CHRONO_ANTS_HUB_URL to get access to ANTS-Hub configuration panel.')
|
||||
)
|
||||
return redirect('chrono-manager-homepage')
|
||||
return func(request, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.Homepage.as_view(), name='chrono-manager-ants-hub'),
|
||||
path('synchronize/', views.Synchronize.as_view(), name='chrono-manager-ants-hub-synchronize'),
|
||||
path('city/add/', views.CityAddView.as_view(), name='chrono-manager-ants-hub-city-add'),
|
||||
path('city/<int:pk>/edit/', views.CityEditView.as_view(), name='chrono-manager-ants-hub-city-edit'),
|
||||
path('city/<int:pk>/delete/', views.CityDeleteView.as_view(), name='chrono-manager-ants-hub-city-delete'),
|
||||
path('city/<int:pk>/place/add/', views.PlaceAddView.as_view(), name='chrono-manager-ants-hub-place-add'),
|
||||
path(
|
||||
'city/<int:city_pk>/place/<int:pk>/', views.PlaceView.as_view(), name='chrono-manager-ants-hub-place'
|
||||
),
|
||||
path(
|
||||
'city/<int:city_pk>/place/<int:pk>/edit/',
|
||||
views.PlaceEditView.as_view(),
|
||||
name='chrono-manager-ants-hub-place-edit',
|
||||
),
|
||||
path(
|
||||
'city/<int:city_pk>/place/<int:pk>/url/',
|
||||
views.PlaceUrlEditView.as_view(),
|
||||
name='chrono-manager-ants-hub-place-url',
|
||||
),
|
||||
path(
|
||||
'city/<int:city_pk>/place/<int:pk>/delete/',
|
||||
views.PlaceDeleteView.as_view(),
|
||||
name='chrono-manager-ants-hub-place-delete',
|
||||
),
|
||||
path(
|
||||
'city/<int:city_pk>/place/<int:pk>/agenda/add/',
|
||||
views.PlaceAgendaAddView.as_view(),
|
||||
name='chrono-manager-ants-hub-agenda-add',
|
||||
),
|
||||
path(
|
||||
'city/<int:city_pk>/place/<int:place_pk>/agenda/<int:pk>/edit/',
|
||||
views.PlaceAgendaEditView.as_view(),
|
||||
name='chrono-manager-ants-hub-agenda-edit',
|
||||
),
|
||||
path(
|
||||
'city/<int:city_pk>/place/<int:place_pk>/agenda/<int:pk>/delete/',
|
||||
views.PlaceAgendaDeleteView.as_view(),
|
||||
name='chrono-manager-ants-hub-agenda-delete',
|
||||
),
|
||||
]
|
||||
|
||||
for pattern in urlpatterns:
|
||||
pattern.callback = view_decorator(pattern.callback)
|
|
@ -0,0 +1,241 @@
|
|||
# chrono - agendas system
|
||||
# Copyright (C) 2023 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/>.
|
||||
|
||||
from django import forms
|
||||
from django.contrib import messages
|
||||
from django.core.cache import cache
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import CreateView, DeleteView, ListView, TemplateView, UpdateView
|
||||
|
||||
from . import hub, models
|
||||
|
||||
|
||||
class Homepage(ListView):
|
||||
template_name = 'chrono/manager_ants_hub.html'
|
||||
model = models.City
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ok = cache.get('ants-hub-ok')
|
||||
if not ok:
|
||||
try:
|
||||
hub.ping()
|
||||
except hub.AntsHubException as e:
|
||||
messages.warning(self.request, _('ANTS Hub is down: "%s".') % e)
|
||||
else:
|
||||
messages.info(self.request, _('ANTS Hub is responding.'))
|
||||
cache.set('ants-hub-ok', True, 600)
|
||||
return ctx
|
||||
|
||||
|
||||
class CityAddView(CreateView):
|
||||
template_name = 'chrono/manager_ants_hub_add_form.html'
|
||||
model = models.City
|
||||
name = _('New city')
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class CityMixin:
|
||||
def dispatch(self, request, pk):
|
||||
self.city = get_object_or_404(models.City, pk=pk)
|
||||
return super().dispatch(request, pk=pk)
|
||||
|
||||
|
||||
class CityView(CityMixin, ListView):
|
||||
template_name = 'chrono/manager_ants_hub_city.html'
|
||||
model = models.Place
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().filter(city=self.city)
|
||||
|
||||
|
||||
class CityEditView(UpdateView):
|
||||
template_name = 'chrono/manager_ants_hub_add_form.html'
|
||||
model = models.City
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class CityDeleteView(DeleteView):
|
||||
template_name = 'chrono/manager_confirm_delete.html'
|
||||
model = models.City
|
||||
success_url = '../../../'
|
||||
|
||||
|
||||
class PlaceForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = models.Place
|
||||
exclude = ['city']
|
||||
|
||||
|
||||
class PlaceAddView(CityMixin, CreateView):
|
||||
template_name = 'chrono/manager_ants_hub_add_form.html'
|
||||
model = models.Place
|
||||
form_class = PlaceForm
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return _('New place in %s') % self.city
|
||||
|
||||
def dispatch(self, request, pk):
|
||||
self.city = get_object_or_404(models.City, pk=pk)
|
||||
return super().dispatch(request, pk)
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['instance'] = models.Place(city=self.city)
|
||||
return kwargs
|
||||
|
||||
|
||||
class PlaceMixin:
|
||||
def dispatch(self, request, city_pk, pk):
|
||||
self.place = get_object_or_404(models.Place, pk=pk, city_id=city_pk)
|
||||
self.city = self.place.city
|
||||
return super().dispatch(request, pk=pk)
|
||||
|
||||
|
||||
class PlaceView(PlaceMixin, ListView):
|
||||
template_name = 'chrono/manager_ants_hub_place.html'
|
||||
model = models.PlaceAgenda
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().filter(place=self.place)
|
||||
|
||||
|
||||
class PlaceEditForm(PlaceForm):
|
||||
class Meta:
|
||||
model = models.Place
|
||||
fields = ['name', 'address', 'zipcode', 'city_name', 'longitude', 'latitude']
|
||||
|
||||
|
||||
class PlaceEditView(UpdateView):
|
||||
template_name = 'chrono/manager_ants_hub_place_edit_form.html'
|
||||
model = models.Place
|
||||
fields = ['zipcode', 'city_name', 'address', 'longitude', 'latitude']
|
||||
|
||||
|
||||
class PlaceDeleteView(DeleteView):
|
||||
template_name = 'chrono/manager_confirm_delete.html'
|
||||
model = models.Place
|
||||
success_url = '../../../../../'
|
||||
|
||||
|
||||
class PlaceUrlEditView(UpdateView):
|
||||
template_name = 'chrono/manager_ants_hub_add_form.html'
|
||||
model = models.Place
|
||||
fields = ['url', 'logo_url', 'meeting_url', 'management_url', 'cancel_url']
|
||||
|
||||
|
||||
class PlaceAgendaAddForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# hide agendas already linked to the place
|
||||
self.fields['agenda'].queryset = self.fields['agenda'].queryset.exclude(
|
||||
ants_place__place=self.instance.place
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = models.PlaceAgenda
|
||||
exclude = ['place', 'setting']
|
||||
|
||||
|
||||
class PlaceAgendaAddView(PlaceMixin, CreateView):
|
||||
template_name = 'chrono/manager_ants_hub_add_form.html'
|
||||
model = models.PlaceAgenda
|
||||
form_class = PlaceAgendaAddForm
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return _('New agenda for %s') % self.place
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['instance'] = models.PlaceAgenda(place=self.place)
|
||||
return kwargs
|
||||
|
||||
def get_success_url(self):
|
||||
return f'../../#open-place-agenda-{self.object.pk}'
|
||||
|
||||
|
||||
class PlaceAgendaEditForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.meeting_types = self.instance.agenda.meetingtype_set.order_by('label')
|
||||
for meeting_type in self.instance.agenda.iter_meetingtypes():
|
||||
field_meeting_type = forms.TypedMultipleChoiceField(
|
||||
label=_('%s') % meeting_type.label,
|
||||
choices=models.ANTSMeetingType.choices,
|
||||
widget=forms.CheckboxSelectMultiple(attrs={'class': 'inline'}),
|
||||
required=False,
|
||||
initial=self.instance.get_meeting_type_setting(meeting_type, 'ants_meeting_type'),
|
||||
)
|
||||
field_meeting_type.meeting_type = meeting_type
|
||||
field_meeting_type.key = 'ants_meeting_type'
|
||||
field_persons_number = forms.TypedMultipleChoiceField(
|
||||
label=_('%s') % meeting_type.label,
|
||||
choices=models.ANTSPersonsNumber.choices,
|
||||
widget=forms.CheckboxSelectMultiple(attrs={'class': 'inline'}),
|
||||
required=False,
|
||||
initial=self.instance.get_meeting_type_setting(meeting_type, 'ants_persons_number'),
|
||||
)
|
||||
field_persons_number.meeting_type = meeting_type
|
||||
field_persons_number.key = 'ants_persons_number'
|
||||
self.fields[f'mt_{meeting_type.slug}_1'] = field_meeting_type
|
||||
self.fields[f'mt_{meeting_type.slug}_2'] = field_persons_number
|
||||
|
||||
def field_by_labels(self):
|
||||
d = {}
|
||||
for bound_field in self:
|
||||
d.setdefault(bound_field.label, []).append(bound_field)
|
||||
return list(d.items())
|
||||
|
||||
def clean(self):
|
||||
for key, field in self.fields.items():
|
||||
value = self.cleaned_data.get(key, [])
|
||||
self.instance.set_meeting_type_setting(field.meeting_type, field.key, value)
|
||||
return self.cleaned_data
|
||||
|
||||
class Meta:
|
||||
model = models.PlaceAgenda
|
||||
fields = []
|
||||
|
||||
|
||||
class PlaceAgendaEditView(UpdateView):
|
||||
template_name = 'chrono/manager_ants_hub_agenda_edit_form.html'
|
||||
model = models.PlaceAgenda
|
||||
form_class = PlaceAgendaEditForm
|
||||
success_url = '../../../'
|
||||
|
||||
|
||||
class PlaceAgendaDeleteView(DeleteView):
|
||||
template_name = 'chrono/manager_confirm_delete.html'
|
||||
model = models.PlaceAgenda
|
||||
success_url = '../../../'
|
||||
|
||||
|
||||
class Synchronize(TemplateView):
|
||||
template_name = 'chrono/manager_ants_hub_synchronize.html'
|
||||
|
||||
def post(self, request):
|
||||
self.synchronize()
|
||||
messages.info(request, _('Synchronization has been launched.'))
|
||||
return redirect('chrono-manager-ants-hub')
|
||||
|
||||
@classmethod
|
||||
def synchronize(cls):
|
||||
from chrono.utils.spooler import ants_hub_city_push
|
||||
|
||||
ants_hub_city_push.spool()
|
|
@ -604,3 +604,46 @@ table.partial-bookings {
|
|||
outline: solid 1px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ants-hub */
|
||||
ul.objects-list.single-links li.ants-setting-not-configured a.edit {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.ants-setting {
|
||||
.meeting-type {
|
||||
vertical-align: top;
|
||||
text-align: center;
|
||||
}
|
||||
ul.inline {
|
||||
display: flex;
|
||||
width: 40em;
|
||||
margin: 1ex;
|
||||
margin-block-start: 0em;
|
||||
padding-inline-start: 0em;
|
||||
}
|
||||
ul.inline li {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
border: 1px solid grey;
|
||||
border-width: 1px 0px 1px 1px;
|
||||
list-style: none;
|
||||
}
|
||||
ul.inline li:first-child {
|
||||
border-radius: 5px 0px 0px 5px;
|
||||
}
|
||||
li label {
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
}
|
||||
ul.inline li:last-child {
|
||||
border-radius: 0px 5px 5px 0px;
|
||||
border-width: 1px 1px 1px 1px;
|
||||
}
|
||||
ul.inline input {
|
||||
display: none;
|
||||
}
|
||||
label.checked {
|
||||
background: lightblue;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
{% load i18n %}
|
||||
|
||||
{% block appbar %}
|
||||
<h2>{{ view.model.get_verbose_name }}</h2>
|
||||
<h2>{% firstof object view.model.get_verbose_name %}</h2>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
|
|
@ -21,6 +21,9 @@
|
|||
{% if user.is_staff %}
|
||||
<li><a href="{% url 'chrono-manager-resource-list' %}">{% trans 'Resources' %}</a></li>
|
||||
{% endif %}
|
||||
{% if ants_hub_enabled and user.is_staff %}
|
||||
<li><a href="{% url 'chrono-manager-ants-hub' %}">{% trans 'ANTS Hub' %}</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if user.is_staff %}
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
# 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/>.
|
||||
|
||||
from django.urls import path, re_path
|
||||
from django.urls import include, path, re_path
|
||||
|
||||
from . import views
|
||||
|
||||
|
@ -510,4 +510,5 @@ urlpatterns = [
|
|||
name='chrono-manager-shared-custody-agenda-delete-period',
|
||||
),
|
||||
re_path(r'^menu.json$', views.menu_json),
|
||||
path('ants/', include('chrono.apps.ants_hub.urls')),
|
||||
]
|
||||
|
|
|
@ -174,6 +174,7 @@ class HomepageView(ListView):
|
|||
context = super().get_context_data(**kwargs)
|
||||
context['has_access_to_unavailability_calendars'] = self.has_access_to_unavailability_calendars()
|
||||
context['shared_custody_enabled'] = settings.SHARED_CUSTODY_ENABLED
|
||||
context['ants_hub_enabled'] = bool(settings.CHRONO_ANTS_HUB_URL)
|
||||
return context
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
|
|
@ -57,6 +57,7 @@ INSTALLED_APPS = (
|
|||
'chrono.agendas',
|
||||
'chrono.api',
|
||||
'chrono.manager',
|
||||
'chrono.apps.ants_hub',
|
||||
'rest_framework',
|
||||
'django_filters',
|
||||
)
|
||||
|
@ -201,6 +202,8 @@ SHARED_CUSTODY_ENABLED = False
|
|||
LEGACY_FILLSLOTS_ENABLED = False
|
||||
PARTIAL_BOOKINGS_ENABLED = False
|
||||
|
||||
CHRONO_ANTS_HUB_URL = None
|
||||
|
||||
local_settings_file = os.environ.get(
|
||||
'CHRONO_SETTINGS_FILE', os.path.join(os.path.dirname(__file__), 'local_settings.py')
|
||||
)
|
||||
|
|
|
@ -69,3 +69,17 @@ def event_notify_checked(args):
|
|||
return
|
||||
|
||||
event.notify_checked()
|
||||
|
||||
|
||||
@spool
|
||||
def ants_hub_city_push(args):
|
||||
if args.get('domain'):
|
||||
# multitenant installation
|
||||
set_connection(args['domain'])
|
||||
|
||||
from chrono.apps.ants_hub.models import City
|
||||
|
||||
try:
|
||||
City.push()
|
||||
except Exception: # noqa pylint: disable=broad-except
|
||||
pass
|
||||
|
|
|
@ -21,6 +21,8 @@ spooler-max-tasks = 20
|
|||
cron2 = minute=-5,unique=1 /usr/bin/chrono-manage tenant_command cancel_events --all-tenants -v0
|
||||
cron2 = minute=-5,unique=1 /usr/bin/chrono-manage tenant_command send_email_notifications --all-tenants -v0
|
||||
cron2 = minute=-5,unique=1 /usr/bin/chrono-manage tenant_command update_event_recurrences --all-tenants -v0
|
||||
# every fifteen minutes
|
||||
cron2 = minute=-15,unique=1 /usr/bin/chrono-manage tenant_command sync-ants-hub --all-tenants
|
||||
# hourly
|
||||
cron2 = minute=3,unique=1 /usr/bin/chrono-manage tenant_command clearsessions --all-tenants
|
||||
cron2 = minute=13,unique=1 /usr/bin/chrono-manage tenant_command send_booking_reminders --all-tenants
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
# chrono - agendas system
|
||||
# Copyright (C) 2023 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 pytest
|
||||
import responses
|
||||
from django.core.cache import cache
|
||||
|
||||
from chrono.apps.ants_hub.models import City, Place, PlaceAgenda
|
||||
from tests.manager.conftest import admin_user, simple_user # noqa pylint: disabled=unused-import
|
||||
from tests.utils import build_agendas
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ants_settings(settings):
|
||||
settings.CHRONO_ANTS_HUB_URL = 'https://toto:@ants-hub.example.com/api/chrono/'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def hub(ants_settings):
|
||||
cache.delete('ants-hub-ok')
|
||||
with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
|
||||
rsps.add(responses.GET, 'https://toto:@ants-hub.example.com/api/chrono/ping/', json={'err': 0})
|
||||
yield rsps
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def city():
|
||||
return City.objects.create(
|
||||
id=1, name='Newcity', logo_url='https://newcity.com/logo.png', url='https://newcity.com/rdv/'
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def place(city):
|
||||
return Place.objects.create(
|
||||
id=1,
|
||||
city=city,
|
||||
name='Townhall',
|
||||
address='221B Baker Street',
|
||||
zipcode='13260',
|
||||
city_name='Newcity',
|
||||
longitude='2.3',
|
||||
latitude='-40.3',
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def agenda():
|
||||
return build_agendas(
|
||||
'''
|
||||
meetings CNI
|
||||
desk "Desk1"
|
||||
timeperiod monday-friday 08:00-17:00
|
||||
meeting-type 15
|
||||
'''
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def place_agenda(place, agenda):
|
||||
return PlaceAgenda.objects.create(
|
||||
id=1,
|
||||
place=place,
|
||||
agenda=agenda,
|
||||
setting={
|
||||
'meeting-types': {
|
||||
'mt-15': {
|
||||
'ants_meeting_type': [1],
|
||||
'ants_persons_number': [1],
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
|
@ -0,0 +1,70 @@
|
|||
# chrono - agendas system
|
||||
# Copyright (C) 2023 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 json
|
||||
|
||||
import responses
|
||||
from django.core.management import call_command
|
||||
|
||||
|
||||
def test_sync_ants_hub(db, hub, place_agenda, freezer):
|
||||
freezer.move_to('2023-06-01T17:12:00+02:00')
|
||||
response = hub.add(
|
||||
responses.POST,
|
||||
'https://toto:@ants-hub.example.com/api/chrono/rendez-vous-disponibles/',
|
||||
json={'err': 0},
|
||||
)
|
||||
call_command('sync-ants-hub')
|
||||
assert response.call_count == 1
|
||||
payload = json.loads(hub.calls[-1].request.body)
|
||||
assert len(payload['collectivites']) == 1
|
||||
assert {**payload['collectivites'][0], 'lieux': None} == {
|
||||
'annulation_url': '',
|
||||
'full': True,
|
||||
'gestion_url': '',
|
||||
'id': '1',
|
||||
'lieux': None,
|
||||
'logo_url': 'https://newcity.com/logo.png',
|
||||
'nom': 'Newcity',
|
||||
'rdv_url': '',
|
||||
'url': 'https://newcity.com/rdv/',
|
||||
}
|
||||
assert len(payload['collectivites'][0]['lieux']) == 1
|
||||
assert {**payload['collectivites'][0]['lieux'][0], 'plages': None} == {
|
||||
'annulation_url': '',
|
||||
'code_postal': '13260',
|
||||
'full': True,
|
||||
'gestion_url': '',
|
||||
'id': '1',
|
||||
'latitude': -40.3,
|
||||
'longitude': 2.3,
|
||||
'nom': 'Townhall',
|
||||
'numero_rue': '221B Baker Street',
|
||||
'plages': None,
|
||||
'rdv_url': '',
|
||||
'rdvs': [],
|
||||
'url': '',
|
||||
'ville': 'Newcity',
|
||||
}
|
||||
assert len(payload['collectivites'][0]['lieux'][0]['plages']) == 39
|
||||
assert payload['collectivites'][0]['lieux'][0]['plages'][0] == {
|
||||
'date': '2023-06-02',
|
||||
'duree': 15,
|
||||
'heure_debut': '08:00:00',
|
||||
'heure_fin': '17:00:00',
|
||||
'personnes': 1,
|
||||
'types_rdv': ['CNI'],
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
# chrono - agendas system
|
||||
# Copyright (C) 2023 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 pytest
|
||||
import requests
|
||||
import responses
|
||||
|
||||
from chrono.apps.ants_hub.hub import AntsHubException, ping, push_rendez_vous_disponibles
|
||||
|
||||
|
||||
def test_ping_timeout(hub):
|
||||
hub.replace(
|
||||
responses.GET, 'https://toto:@ants-hub.example.com/api/chrono/ping/', body=requests.Timeout('boom!')
|
||||
)
|
||||
ping()
|
||||
|
||||
|
||||
def test_push_rendez_vous_disponibles_timeout(hub):
|
||||
hub.add(
|
||||
responses.POST,
|
||||
'https://toto:@ants-hub.example.com/api/chrono/rendez-vous-disponibles/',
|
||||
body=requests.Timeout('boom!'),
|
||||
)
|
||||
push_rendez_vous_disponibles({})
|
||||
|
||||
|
||||
def test_ping_internal_server_error(hub):
|
||||
hub.replace(responses.GET, 'https://toto:@ants-hub.example.com/api/chrono/ping/', status=500)
|
||||
with pytest.raises(AntsHubException):
|
||||
ping()
|
||||
|
||||
|
||||
def test_push_rendez_vous_disponibles_internal_server_error(hub):
|
||||
hub.add(
|
||||
responses.POST, 'https://toto:@ants-hub.example.com/api/chrono/rendez-vous-disponibles/', status=500
|
||||
)
|
||||
with pytest.raises(AntsHubException):
|
||||
push_rendez_vous_disponibles({})
|
||||
|
||||
|
||||
def test_ping_application_error(hub):
|
||||
hub.replace(
|
||||
responses.GET, 'https://toto:@ants-hub.example.com/api/chrono/ping/', json={'err': 'overload'}
|
||||
)
|
||||
with pytest.raises(AntsHubException, match='overload'):
|
||||
ping()
|
||||
|
||||
|
||||
def test_push_rendez_vous_disponibles_application_error(hub):
|
||||
hub.add(
|
||||
responses.POST,
|
||||
'https://toto:@ants-hub.example.com/api/chrono/rendez-vous-disponibles/',
|
||||
json={'err': 'overload'},
|
||||
)
|
||||
with pytest.raises(AntsHubException, match='overload'):
|
||||
push_rendez_vous_disponibles({})
|
|
@ -0,0 +1,192 @@
|
|||
# chrono - agendas system
|
||||
# Copyright (C) 2023 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/>.
|
||||
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
import responses
|
||||
|
||||
from chrono.apps.ants_hub.models import ANTSMeetingType, ANTSPersonsNumber, City, PlaceAgenda
|
||||
from tests.utils import login
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_unconfigured(app, admin_user):
|
||||
login(app)
|
||||
resp = app.get('/manage/', status=200)
|
||||
assert 'ANTS' not in resp
|
||||
|
||||
|
||||
def test_unlogged(ants_settings, app):
|
||||
app.get('/manage/ants/', status=302)
|
||||
|
||||
|
||||
def test_configured(hub, app, admin_user):
|
||||
login(app)
|
||||
resp = app.get('/manage/', status=200)
|
||||
resp = resp.click('ANTS')
|
||||
assert 'ANTS Hub is responding' in resp
|
||||
assert 'New city' in resp
|
||||
|
||||
|
||||
def test_hub_is_down(hub, app, admin_user):
|
||||
hub.replace(responses.GET, 'https://toto:@ants-hub.example.com/api/chrono/ping/', status=500)
|
||||
login(app)
|
||||
resp = app.get('/manage/', status=200)
|
||||
resp = resp.click('ANTS')
|
||||
assert 'ANTS Hub is down' in resp
|
||||
|
||||
|
||||
def test_city_add(hub, app, admin_user):
|
||||
login(app)
|
||||
resp = app.get('/manage/ants/', status=200)
|
||||
assert 'Newcity' not in resp
|
||||
resp = resp.click('New city')
|
||||
resp.form.set('name', 'Newcity')
|
||||
resp.form.set('url', 'https://newcity.com/rdv/')
|
||||
resp.form.set('logo_url', 'https://newcity.com/logo.png')
|
||||
resp = resp.form.submit().follow()
|
||||
assert 'Newcity' in resp
|
||||
|
||||
|
||||
def test_city_edit(hub, app, admin_user, city):
|
||||
login(app)
|
||||
resp = app.get('/manage/ants/')
|
||||
resp = resp.click(href='edit')
|
||||
assert 'logo.png' in resp
|
||||
|
||||
|
||||
def test_city_delete(hub, app, admin_user, city):
|
||||
login(app)
|
||||
resp = app.get('/manage/ants/')
|
||||
resp = resp.click(href='delete')
|
||||
resp = resp.form.submit(status=302)
|
||||
assert City.objects.count() == 0
|
||||
|
||||
|
||||
def test_add_place(hub, app, admin_user, city):
|
||||
login(app)
|
||||
resp = app.get('/manage/ants/')
|
||||
resp = resp.click('Add place')
|
||||
resp.form.set('name', 'Townhall')
|
||||
resp.form.set('address', '221B Baker Street')
|
||||
resp.form.set('zipcode', '13260')
|
||||
resp.form.set('city_name', 'Newcity')
|
||||
resp.form.set('longitude', '2.3')
|
||||
resp.form.set('latitude', '-40.3')
|
||||
assert city.places.count() == 0
|
||||
resp = resp.form.submit().follow()
|
||||
assert city.places.count() == 1
|
||||
assert 'Newcity' in resp
|
||||
assert 'Townhall' in resp
|
||||
|
||||
|
||||
def test_edit_place(hub, app, admin_user, city, place):
|
||||
login(app)
|
||||
resp = app.get('/manage/ants/')
|
||||
resp = resp.click('Townhall')
|
||||
resp = resp.click('Edit', href='edit')
|
||||
assert 'Baker Street' in resp
|
||||
resp.form.set('address', 'Downing Street')
|
||||
resp = resp.form.submit().follow()
|
||||
assert 'Downing Street' in resp
|
||||
place.refresh_from_db()
|
||||
assert place.address == 'Downing Street'
|
||||
|
||||
|
||||
def test_url_edit_place(hub, app, admin_user, city, place):
|
||||
login(app)
|
||||
resp = app.get('/manage/ants/')
|
||||
resp = resp.click('Townhall')
|
||||
assert 'https://townhall.example.com/rdv/' not in resp
|
||||
resp = resp.click('Edit', href='url')
|
||||
resp.form.set('url', 'https://townhall.example.com/rdv/')
|
||||
resp = resp.form.submit().follow()
|
||||
assert 'https://townhall.example.com/rdv/' in resp
|
||||
place.refresh_from_db()
|
||||
assert place.url == 'https://townhall.example.com/rdv/'
|
||||
|
||||
|
||||
def test_delete_place(hub, app, admin_user, city, place):
|
||||
login(app)
|
||||
resp = app.get('/manage/ants/')
|
||||
resp = resp.click('Townhall')
|
||||
resp = resp.click('Remove')
|
||||
assert city.places.count() == 1
|
||||
resp = resp.form.submit().follow()
|
||||
assert city.places.count() == 0
|
||||
|
||||
|
||||
def test_add_agenda(hub, app, admin_user, city, place, agenda, freezer):
|
||||
freezer.move_to('2023-06-01T17:12:00+02:00')
|
||||
login(app)
|
||||
resp = app.get('/manage/ants/')
|
||||
resp = resp.click('Townhall')
|
||||
resp = resp.click('Add')
|
||||
resp.form.set('agenda', str(agenda.pk))
|
||||
assert not place.agendas.exists()
|
||||
resp = resp.form.submit().follow()
|
||||
place_agenda = place.agendas.get()
|
||||
assert list(place_agenda.iter_open_dates()) == []
|
||||
assert not place_agenda.setting.get('meeting-types')
|
||||
assert '(not configured)' in resp
|
||||
assert '1 person' not in resp
|
||||
resp = resp.click(href='agenda.*edit')
|
||||
# make the meeting-type of 15 minutes correpond to a meeting to get a CNI
|
||||
# for 1 person
|
||||
resp.form.set('mt_mt-15_1', [str(ANTSMeetingType.CNI)])
|
||||
resp.form.set('mt_mt-15_2', [str(ANTSPersonsNumber.ONE)])
|
||||
resp = resp.form.submit().follow()
|
||||
place_agenda.refresh_from_db()
|
||||
assert place_agenda.setting.get('meeting-types')
|
||||
assert '(not configured)' not in resp
|
||||
assert '(CNI, 1 person)' in resp
|
||||
assert place_agenda.setting == {
|
||||
'meeting-types': {'mt-15': {'ants_meeting_type': [1], 'ants_persons_number': [1]}}
|
||||
}
|
||||
|
||||
open_dates = list(place_agenda.iter_open_dates())
|
||||
assert len(open_dates) == 39
|
||||
assert open_dates[0] == {
|
||||
'date': '2023-06-02',
|
||||
'heure_debut': '08:00:00',
|
||||
'heure_fin': '17:00:00',
|
||||
'duree': 15,
|
||||
'personnes': 1,
|
||||
'types_rdv': ['CNI'],
|
||||
}
|
||||
|
||||
|
||||
def test_delete_agenda(hub, app, admin_user, city, place, agenda, place_agenda):
|
||||
login(app)
|
||||
resp = app.get('/manage/ants/')
|
||||
resp = resp.click('Townhall')
|
||||
resp = resp.click(href='agenda.*delete')
|
||||
resp = resp.form.submit().follow()
|
||||
assert 'CNI' not in resp
|
||||
assert PlaceAgenda.objects.count() == 0
|
||||
|
||||
|
||||
def test_synchronize(hub, app, admin_user, city, place, agenda, place_agenda, freezer):
|
||||
freezer.move_to('2023-06-01T17:12:00+02:00')
|
||||
login(app)
|
||||
resp = app.get('/manage/', status=200)
|
||||
resp = resp.click('ANTS')
|
||||
resp = resp.click('Synchronize')
|
||||
with mock.patch('chrono.apps.ants_hub.views.Synchronize.synchronize') as method:
|
||||
resp = resp.form.submit().follow()
|
||||
assert method.called
|
|
@ -0,0 +1,616 @@
|
|||
# chrono - agendas system
|
||||
# Copyright (C) 2023 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 pytest
|
||||
|
||||
from chrono.apps.ants_hub.models import City, Place, PlaceAgenda
|
||||
from tests.utils import add_meeting, build_meetings_agenda, build_virtual_agenda, paris
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def ants_setup(db, freezer):
|
||||
freezer.move_to(paris('2023-04-07 17:32'))
|
||||
|
||||
class Namespace:
|
||||
durations = [15, 30, 45, 60]
|
||||
mairie_agenda = build_meetings_agenda(
|
||||
label='cni_passport_mairie',
|
||||
meeting_types=durations,
|
||||
desks=('desk1', 'monday-friday 9:00-12:00 14:00-17:00'),
|
||||
maximal_booking_delay=10,
|
||||
)
|
||||
annexe_agenda = build_virtual_agenda(
|
||||
label='annexe',
|
||||
agendas={
|
||||
'Agenda 1': {
|
||||
'desks': ('Bureau 1', 'monday-wednesday 08:00-12:00'),
|
||||
'meeting_types': durations,
|
||||
},
|
||||
'Agenda 2': {
|
||||
'desks': ('Bureau 2', 'thursday-friday 14:00-17:00'),
|
||||
'meeting_types': durations,
|
||||
},
|
||||
},
|
||||
maximal_booking_delay=5,
|
||||
)
|
||||
city = City.objects.create(
|
||||
id=1,
|
||||
name='Saint-Didier',
|
||||
url='https://saint-didier.fr/',
|
||||
)
|
||||
mairie = Place.objects.create(
|
||||
id=1,
|
||||
city=city,
|
||||
name='Mairie',
|
||||
address='2 rue du four',
|
||||
zipcode='99999',
|
||||
city_name='Saint-Didier',
|
||||
)
|
||||
annexe = Place.objects.create(
|
||||
id=2,
|
||||
city=city,
|
||||
name='Mairie annexe',
|
||||
address='3 rue du four',
|
||||
zipcode='99999',
|
||||
city_name='Saint-Didier',
|
||||
)
|
||||
PlaceAgenda.objects.create(
|
||||
place=mairie,
|
||||
agenda=mairie_agenda,
|
||||
setting={
|
||||
'meeting-types': {
|
||||
'mt-15': {
|
||||
'ants_meeting_type': [1, 2],
|
||||
'ants_persons_number': [1],
|
||||
},
|
||||
'mt-30': {
|
||||
'ants_meeting_type': [1, 2],
|
||||
'ants_persons_number': [2],
|
||||
},
|
||||
'mt-45': {
|
||||
'ants_meeting_type': [1, 2],
|
||||
'ants_persons_number': [3],
|
||||
},
|
||||
'mt-60': {
|
||||
'ants_meeting_type': [1, 2],
|
||||
'ants_persons_number': [4],
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
PlaceAgenda.objects.create(
|
||||
place=annexe,
|
||||
agenda=annexe_agenda,
|
||||
setting={
|
||||
'meeting-types': {
|
||||
'mt-15': {
|
||||
'ants_meeting_type': [1, 2],
|
||||
'ants_persons_number': [1],
|
||||
},
|
||||
'mt-30': {
|
||||
'ants_meeting_type': [1, 2],
|
||||
'ants_persons_number': [2],
|
||||
},
|
||||
'mt-45': {
|
||||
'ants_meeting_type': [1, 2],
|
||||
'ants_persons_number': [3],
|
||||
},
|
||||
'mt-60': {
|
||||
'ants_meeting_type': [1, 2],
|
||||
'ants_persons_number': [4],
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
add_meeting(
|
||||
mairie_agenda,
|
||||
paris('2023-04-10 09:00'),
|
||||
cancellation_datetime=paris('2023-04-08 13:15'),
|
||||
meeting_type='mt-15',
|
||||
extra_data={'ants_identifiant_predemande': '12345678'},
|
||||
)
|
||||
add_meeting(
|
||||
mairie_agenda,
|
||||
paris('2023-04-11 11:00'),
|
||||
meeting_type='mt-30',
|
||||
extra_data={'ants_identifiant_predemande': 'ABCDEFGH'},
|
||||
)
|
||||
|
||||
add_meeting(
|
||||
annexe_agenda._agenda_1,
|
||||
paris('2023-04-10 10:00'),
|
||||
cancellation_datetime=paris('2023-04-08 13:15'),
|
||||
meeting_type='mt-45',
|
||||
extra_data={'ants_identifiant_predemande': '1234ABCD'},
|
||||
)
|
||||
add_meeting(
|
||||
annexe_agenda._agenda_1,
|
||||
paris('2023-04-10 11:00'),
|
||||
meeting_type='mt-45',
|
||||
extra_data={'ants_identifiant_predemande': 'XYZ12JKL'},
|
||||
)
|
||||
add_meeting(
|
||||
annexe_agenda._agenda_2,
|
||||
paris('2023-04-11 11:00'),
|
||||
meeting_type='mt-60',
|
||||
extra_data={'ants_identifiant_predemande': 'ABCD1234'},
|
||||
)
|
||||
|
||||
return Namespace
|
||||
|
||||
|
||||
def test_export_to_push(ants_setup):
|
||||
assert ants_setup.city.export_to_push() == {
|
||||
'full': True,
|
||||
'id': '1',
|
||||
'nom': 'Saint-Didier',
|
||||
'url': 'https://saint-didier.fr/',
|
||||
'logo_url': '',
|
||||
'rdv_url': '',
|
||||
'annulation_url': '',
|
||||
'gestion_url': '',
|
||||
'lieux': [
|
||||
{
|
||||
'full': True,
|
||||
'id': '1',
|
||||
'nom': 'Mairie',
|
||||
'numero_rue': '2 rue du four',
|
||||
'code_postal': '99999',
|
||||
'ville': 'Saint-Didier',
|
||||
'latitude': 46.596,
|
||||
'longitude': 2.476,
|
||||
'url': '',
|
||||
'rdv_url': '',
|
||||
'gestion_url': '',
|
||||
'annulation_url': '',
|
||||
'rdvs': [
|
||||
{'annule': True, 'date': '2023-04-10T07:00:00+00:00', 'id': '12345678'},
|
||||
{'date': '2023-04-11T09:00:00+00:00', 'id': 'ABCDEFGH'},
|
||||
],
|
||||
'plages': [
|
||||
{
|
||||
'date': '2023-04-10',
|
||||
'duree': 15,
|
||||
'heure_debut': '09:00:00',
|
||||
'heure_fin': '12:00:00',
|
||||
'personnes': 1,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-10',
|
||||
'duree': 30,
|
||||
'heure_debut': '09:00:00',
|
||||
'heure_fin': '12:00:00',
|
||||
'personnes': 2,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-10',
|
||||
'duree': 45,
|
||||
'heure_debut': '09:00:00',
|
||||
'heure_fin': '12:00:00',
|
||||
'personnes': 3,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-10',
|
||||
'duree': 60,
|
||||
'heure_debut': '09:00:00',
|
||||
'heure_fin': '12:00:00',
|
||||
'personnes': 4,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-10',
|
||||
'duree': 15,
|
||||
'heure_debut': '14:00:00',
|
||||
'heure_fin': '17:00:00',
|
||||
'personnes': 1,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-10',
|
||||
'duree': 30,
|
||||
'heure_debut': '14:00:00',
|
||||
'heure_fin': '17:00:00',
|
||||
'personnes': 2,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-10',
|
||||
'duree': 45,
|
||||
'heure_debut': '14:00:00',
|
||||
'heure_fin': '17:00:00',
|
||||
'personnes': 3,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-10',
|
||||
'duree': 60,
|
||||
'heure_debut': '14:00:00',
|
||||
'heure_fin': '17:00:00',
|
||||
'personnes': 4,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-11',
|
||||
'duree': 15,
|
||||
'heure_debut': '09:00:00',
|
||||
'heure_fin': '11:00:00',
|
||||
'personnes': 1,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-11',
|
||||
'duree': 30,
|
||||
'heure_debut': '09:00:00',
|
||||
'heure_fin': '11:00:00',
|
||||
'personnes': 2,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-11',
|
||||
'duree': 45,
|
||||
'heure_debut': '09:00:00',
|
||||
'heure_fin': '11:00:00',
|
||||
'personnes': 3,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-11',
|
||||
'duree': 60,
|
||||
'heure_debut': '09:00:00',
|
||||
'heure_fin': '11:00:00',
|
||||
'personnes': 4,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-11',
|
||||
'duree': 15,
|
||||
'heure_debut': '11:30:00',
|
||||
'heure_fin': '12:00:00',
|
||||
'personnes': 1,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-11',
|
||||
'duree': 30,
|
||||
'heure_debut': '11:30:00',
|
||||
'heure_fin': '12:00:00',
|
||||
'personnes': 2,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-11',
|
||||
'duree': 15,
|
||||
'heure_debut': '14:00:00',
|
||||
'heure_fin': '17:00:00',
|
||||
'personnes': 1,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-11',
|
||||
'duree': 30,
|
||||
'heure_debut': '14:00:00',
|
||||
'heure_fin': '17:00:00',
|
||||
'personnes': 2,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-11',
|
||||
'duree': 45,
|
||||
'heure_debut': '14:00:00',
|
||||
'heure_fin': '17:00:00',
|
||||
'personnes': 3,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-11',
|
||||
'duree': 60,
|
||||
'heure_debut': '14:00:00',
|
||||
'heure_fin': '17:00:00',
|
||||
'personnes': 4,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-12',
|
||||
'duree': 15,
|
||||
'heure_debut': '09:00:00',
|
||||
'heure_fin': '12:00:00',
|
||||
'personnes': 1,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-12',
|
||||
'duree': 30,
|
||||
'heure_debut': '09:00:00',
|
||||
'heure_fin': '12:00:00',
|
||||
'personnes': 2,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-12',
|
||||
'duree': 45,
|
||||
'heure_debut': '09:00:00',
|
||||
'heure_fin': '12:00:00',
|
||||
'personnes': 3,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-12',
|
||||
'duree': 60,
|
||||
'heure_debut': '09:00:00',
|
||||
'heure_fin': '12:00:00',
|
||||
'personnes': 4,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-12',
|
||||
'duree': 15,
|
||||
'heure_debut': '14:00:00',
|
||||
'heure_fin': '17:00:00',
|
||||
'personnes': 1,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-12',
|
||||
'duree': 30,
|
||||
'heure_debut': '14:00:00',
|
||||
'heure_fin': '17:00:00',
|
||||
'personnes': 2,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-12',
|
||||
'duree': 45,
|
||||
'heure_debut': '14:00:00',
|
||||
'heure_fin': '17:00:00',
|
||||
'personnes': 3,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-12',
|
||||
'duree': 60,
|
||||
'heure_debut': '14:00:00',
|
||||
'heure_fin': '17:00:00',
|
||||
'personnes': 4,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-13',
|
||||
'duree': 15,
|
||||
'heure_debut': '09:00:00',
|
||||
'heure_fin': '12:00:00',
|
||||
'personnes': 1,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-13',
|
||||
'duree': 30,
|
||||
'heure_debut': '09:00:00',
|
||||
'heure_fin': '12:00:00',
|
||||
'personnes': 2,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-13',
|
||||
'duree': 45,
|
||||
'heure_debut': '09:00:00',
|
||||
'heure_fin': '12:00:00',
|
||||
'personnes': 3,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-13',
|
||||
'duree': 60,
|
||||
'heure_debut': '09:00:00',
|
||||
'heure_fin': '12:00:00',
|
||||
'personnes': 4,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-13',
|
||||
'duree': 15,
|
||||
'heure_debut': '14:00:00',
|
||||
'heure_fin': '17:00:00',
|
||||
'personnes': 1,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-13',
|
||||
'duree': 30,
|
||||
'heure_debut': '14:00:00',
|
||||
'heure_fin': '17:00:00',
|
||||
'personnes': 2,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-13',
|
||||
'duree': 45,
|
||||
'heure_debut': '14:00:00',
|
||||
'heure_fin': '17:00:00',
|
||||
'personnes': 3,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-13',
|
||||
'duree': 60,
|
||||
'heure_debut': '14:00:00',
|
||||
'heure_fin': '17:00:00',
|
||||
'personnes': 4,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-14',
|
||||
'duree': 15,
|
||||
'heure_debut': '09:00:00',
|
||||
'heure_fin': '12:00:00',
|
||||
'personnes': 1,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-14',
|
||||
'duree': 30,
|
||||
'heure_debut': '09:00:00',
|
||||
'heure_fin': '12:00:00',
|
||||
'personnes': 2,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-14',
|
||||
'duree': 45,
|
||||
'heure_debut': '09:00:00',
|
||||
'heure_fin': '12:00:00',
|
||||
'personnes': 3,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-14',
|
||||
'duree': 60,
|
||||
'heure_debut': '09:00:00',
|
||||
'heure_fin': '12:00:00',
|
||||
'personnes': 4,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-14',
|
||||
'duree': 15,
|
||||
'heure_debut': '14:00:00',
|
||||
'heure_fin': '17:00:00',
|
||||
'personnes': 1,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-14',
|
||||
'duree': 30,
|
||||
'heure_debut': '14:00:00',
|
||||
'heure_fin': '17:00:00',
|
||||
'personnes': 2,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-14',
|
||||
'duree': 45,
|
||||
'heure_debut': '14:00:00',
|
||||
'heure_fin': '17:00:00',
|
||||
'personnes': 3,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-14',
|
||||
'duree': 60,
|
||||
'heure_debut': '14:00:00',
|
||||
'heure_fin': '17:00:00',
|
||||
'personnes': 4,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'full': True,
|
||||
'id': '2',
|
||||
'nom': 'Mairie annexe',
|
||||
'numero_rue': '3 rue du four',
|
||||
'code_postal': '99999',
|
||||
'ville': 'Saint-Didier',
|
||||
'latitude': 46.596,
|
||||
'longitude': 2.476,
|
||||
'url': '',
|
||||
'rdv_url': '',
|
||||
'gestion_url': '',
|
||||
'annulation_url': '',
|
||||
'rdvs': [
|
||||
{'annule': True, 'date': '2023-04-10T08:00:00+00:00', 'id': '1234ABCD'},
|
||||
{'date': '2023-04-10T09:00:00+00:00', 'id': 'XYZ12JKL'},
|
||||
{'date': '2023-04-11T09:00:00+00:00', 'id': 'ABCD1234'},
|
||||
],
|
||||
'plages': [
|
||||
{
|
||||
'date': '2023-04-10',
|
||||
'duree': 15,
|
||||
'heure_debut': '08:00:00',
|
||||
'heure_fin': '11:00:00',
|
||||
'personnes': 1,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-10',
|
||||
'duree': 30,
|
||||
'heure_debut': '08:00:00',
|
||||
'heure_fin': '11:00:00',
|
||||
'personnes': 2,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-10',
|
||||
'duree': 45,
|
||||
'heure_debut': '08:00:00',
|
||||
'heure_fin': '11:00:00',
|
||||
'personnes': 3,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-10',
|
||||
'duree': 60,
|
||||
'heure_debut': '08:00:00',
|
||||
'heure_fin': '11:00:00',
|
||||
'personnes': 4,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-10',
|
||||
'duree': 15,
|
||||
'heure_debut': '11:45:00',
|
||||
'heure_fin': '12:00:00',
|
||||
'personnes': 1,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-11',
|
||||
'duree': 15,
|
||||
'heure_debut': '08:00:00',
|
||||
'heure_fin': '12:00:00',
|
||||
'personnes': 1,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-11',
|
||||
'duree': 30,
|
||||
'heure_debut': '08:00:00',
|
||||
'heure_fin': '12:00:00',
|
||||
'personnes': 2,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-11',
|
||||
'duree': 45,
|
||||
'heure_debut': '08:00:00',
|
||||
'heure_fin': '12:00:00',
|
||||
'personnes': 3,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
{
|
||||
'date': '2023-04-11',
|
||||
'duree': 60,
|
||||
'heure_debut': '08:00:00',
|
||||
'heure_fin': '12:00:00',
|
||||
'personnes': 4,
|
||||
'types_rdv': ['CNI', 'PASSPORT'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
|
@ -321,7 +321,7 @@ def add_exception_source(target: typing.Union[Desk, Agenda], ics_filename: str,
|
|||
def add_meeting(
|
||||
target: typing.Union[Desk, Agenda],
|
||||
start_datetime: datetime.datetime,
|
||||
meeting_type: MeetingType = None,
|
||||
meeting_type: typing.Union[str, MeetingType] = None,
|
||||
**kwargs,
|
||||
):
|
||||
if isinstance(target, Desk):
|
||||
|
@ -332,6 +332,8 @@ def add_meeting(
|
|||
|
||||
if meeting_type is None:
|
||||
meeting_type = desk.agenda.meetingtype_set.get()
|
||||
elif isinstance(meeting_type, str):
|
||||
meeting_type = desk.agenda.meetingtype_set.get(slug=meeting_type)
|
||||
|
||||
event = Event.objects.create(
|
||||
agenda=desk.agenda,
|
||||
|
|
2
tox.ini
2
tox.ini
|
@ -35,6 +35,7 @@ deps =
|
|||
django32: psycopg2-binary>=2.9
|
||||
codestyle: pre-commit
|
||||
git+https://git.entrouvert.org/publik-django-templatetags.git
|
||||
responses
|
||||
commands =
|
||||
./getlasso3.sh
|
||||
python3 setup.py compile_translations
|
||||
|
@ -60,6 +61,7 @@ deps =
|
|||
pytest-freezegun
|
||||
psycopg2-binary<2.9
|
||||
git+https://git.entrouvert.org/publik-django-templatetags.git
|
||||
responses
|
||||
commands =
|
||||
./getlasso3.sh
|
||||
pylint: ./pylint.sh chrono/ tests/
|
||||
|
|
Loading…
Reference in New Issue