Compare commits

...

10 Commits

Author SHA1 Message Date
Lauréline Guérin 77f3373820
agendas: fix custom fields export/import and display in inspect (#89485)
gitea/chrono/pipeline/head This commit looks good Details
2024-04-26 14:41:51 +02:00
Valentin Deniaud 169dc0a69a agendas: ignore exception source on import if file is missing (#89873)
gitea/chrono/pipeline/head This commit looks good Details
2024-04-23 13:42:32 +02:00
Lauréline Guérin 88d8feacd8
translation update
gitea/chrono/pipeline/head This commit looks good Details
2024-04-18 09:37:51 +02:00
Thomas NOËL 6ee8fbf78d agendas: use URLField for event url (#89447)
gitea/chrono/pipeline/head This commit looks good Details
2024-04-16 18:01:42 +02:00
Lauréline Guérin 3403295d3d snapshots: json diff, use gadjo to collapse lines between changes (#89484)
gitea/chrono/pipeline/head This commit looks good Details
2024-04-16 10:14:25 +02:00
Yann Weber 0563e0642d tests: fix event order in api fillslot tests (#89598)
gitea/chrono/pipeline/head This commit looks good Details
2024-04-15 16:24:51 +02:00
Yann Weber 5a90c4851b tests: add callback to clear timezone cache on settings update (#89097)
gitea/chrono/pipeline/head This commit looks good Details
2024-04-15 15:08:00 +02:00
Yann Weber d03e1e7940 tests: replace legacy Brazil/East timezone in fixture (#89097)
Replacing Brazil/East (legacy, not present in defaults zoneinfo anymore)
with America/Sao_Paulo
2024-04-15 15:08:00 +02:00
Yann Weber b0f956c223 tests: remove --dist loadfile option introducing a bug (#89097) 2024-04-15 15:08:00 +02:00
Yann Weber 570cf81c8e manager: make agenda's groups foldable (#85616)
gitea/chrono/pipeline/head This commit looks good Details
2024-04-15 14:44:09 +02:00
31 changed files with 504 additions and 118 deletions

View File

@ -17,6 +17,6 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='event',
name='url',
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='URL'),
field=models.URLField(blank=True, null=True, verbose_name='URL'),
),
]

View File

@ -601,7 +601,7 @@ class Agenda(WithSnapshotMixin, WithApplicationMixin, WithInspectMixin, models.M
AgendaNotificationsSettings.objects.filter(agenda=agenda).delete()
for event_data in events:
event_data['agenda'] = agenda
Event.import_json(event_data)
Event.import_json(event_data, snapshot=snapshot)
if notifications_settings:
notifications_settings['agenda'] = agenda
AgendaNotificationsSettings.import_json(notifications_settings)
@ -2234,7 +2234,7 @@ class Event(WithInspectMixin, models.Model):
_('Description'), null=True, blank=True, help_text=_('Optional event description.')
)
pricing = models.CharField(_('Pricing'), max_length=150, null=True, blank=True)
url = models.CharField(_('URL'), max_length=200, null=True, blank=True)
url = models.URLField(_('URL'), max_length=200, null=True, blank=True)
booked_places = models.PositiveSmallIntegerField(default=0)
booked_waiting_list_places = models.PositiveSmallIntegerField(default=0)
almost_full = models.BooleanField(default=False)
@ -2695,7 +2695,7 @@ class Event(WithInspectMixin, models.Model):
return
@classmethod
def import_json(cls, data):
def import_json(cls, data, snapshot=None):
try:
data['start_datetime'] = make_aware(
datetime.datetime.strptime(data['start_datetime'], '%Y-%m-%d %H:%M:%S')
@ -2721,7 +2721,7 @@ class Event(WithInspectMixin, models.Model):
else:
event = cls(**data)
event.save()
if event.recurrence_days:
if snapshot is None and event.recurrence_days:
event.refresh_from_db()
if event.recurrence_end_date:
event.recurrences.filter(start_datetime__gt=event.recurrence_end_date).delete()
@ -2737,6 +2737,7 @@ class Event(WithInspectMixin, models.Model):
'description',
'pricing',
'url',
'custom_fields',
]
}
event.recurrences.update(**update_fields)
@ -2769,6 +2770,7 @@ class Event(WithInspectMixin, models.Model):
'url': self.url,
'pricing': self.pricing,
'duration': self.duration,
'custom_fields': self.get_custom_fields(),
}
def get_inspect_keys(self):
@ -3992,6 +3994,9 @@ class TimePeriodExceptionSource(WithInspectMixin, models.Model):
data['ics_file'] = ContentFile(base64.b64decode(data['ics_file']), name=data['ics_filename'])
except base64.binascii.Error:
raise AgendaImportError(_('Bad ics file'))
elif data.get('ics_filename'):
# filename but no file content, skip this source
return
desk = data.pop('desk')
settings_slug = data.pop('settings_slug')

View File

@ -156,4 +156,5 @@ urlpatterns = [
path('statistics/', views.statistics_list, name='api-statistics-list'),
path('statistics/bookings/', views.bookings_statistics, name='api-statistics-bookings'),
path('ants/', include('chrono.apps.ants_hub.api_urls')),
path('user-preferences/', include('chrono.apps.user_preferences.api_urls')),
]

View File

View File

@ -0,0 +1,23 @@
# chrono - agendas system
# Copyright (C) 2024 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.urls import path
from . import api_views
urlpatterns = [
path('save', api_views.save_preference, name='api-user-preferences'),
]

View File

@ -0,0 +1,43 @@
# chrono - agendas system
# Copyright (C) 2024 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
from django.contrib.auth.decorators import login_required
from django.http import HttpResponse, HttpResponseBadRequest
from django.utils.translation import gettext_lazy as _
from django.views.decorators.csrf import csrf_exempt
from . import models
@csrf_exempt
@login_required
def save_preference(request):
user_pref, dummy = models.UserPreferences.objects.get_or_create(user=request.user)
if len(request.body) > 1000:
return HttpResponseBadRequest(_('Payload is too large'))
try:
prefs = json.loads(request.body)
except json.JSONDecodeError:
return HttpResponseBadRequest(_('Bad format'))
if not isinstance(prefs, dict) or len(prefs) != 1:
return HttpResponseBadRequest(_('Bad format'))
user_pref.preferences.update(prefs)
user_pref.save()
return HttpResponse('', status=204)

View File

@ -0,0 +1,32 @@
# Generated by Django 3.2.18 on 2024-04-11 15:30
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='UserPreferences',
fields=[
(
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('preferences', models.JSONField(default=dict, verbose_name='Preferences')),
(
'user',
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
),
],
),
]

View File

@ -0,0 +1,24 @@
# chrono - agendas system
# Copyright (C) 2024 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.conf import settings
from django.db import models
from django.utils.translation import gettext_lazy as _
class UserPreferences(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
preferences = models.JSONField(_('Preferences'), default=dict)

View File

@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: chrono 0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-04-04 15:40+0200\n"
"POT-Creation-Date: 2024-04-18 09:37+0200\n"
"PO-Revision-Date: 2024-02-01 09:50+0100\n"
"Last-Translator: Frederic Peters <fpeters@entrouvert.com>\n"
"Language: French\n"
@ -1986,6 +1986,18 @@ msgstr "Version %s"
msgid "Snapshot"
msgstr "Sauvegarde"
#: apps/user_preferences/api_views.py
msgid "Payload is too large"
msgstr "Le contenu de requête est trop grand"
#: apps/user_preferences/api_views.py
msgid "Bad format"
msgstr "Mauvais format"
#: apps/user_preferences/models.py
msgid "Preferences"
msgstr "Préférences"
#: manager/forms.py
msgid "Desk 1"
msgstr "Guichet 1"

View File

@ -1013,77 +1013,3 @@ a.button.button-paragraph {
.snapshots-list .collapsed {
display: none;
}
p.snapshot-description {
font-size: 80%;
margin: 0;
}
div.diff {
margin: 1em 0;
h3 {
del, ins {
font-weight: bold;
background-color: transparent;
}
del {
color: #fbb6c2 !important;
}
ins {
color: #d4fcbc !important;
}
}
}
ins {
text-decoration: none;
background-color: #d4fcbc;
}
del {
text-decoration: line-through;
background-color: #fbb6c2;
color: #555;
}
table.diff {
background: white;
border: 1px solid #f3f3f3;
border-collapse: collapse;
width: 100%;
colgroup, thead, tbody, td {
border: 1px solid #f3f3f3;
}
tbody tr:nth-child(even) {
background: #fdfdfd;
}
th, td {
max-width: 30vw;
/* it will not actually limit width as the table is set to
* expand to 100% but it will prevent one side getting wider
*/
overflow: hidden;
text-overflow: ellipsis;
vertical-align: top;
}
.diff_header {
background: #f7f7f7;
}
td.diff_header {
text-align: right;
padding-right: 10px;
color: #606060;
}
.diff_next {
display: none;
}
.diff_add {
background-color: #aaffaa;
}
.diff_chg {
background-color: #ffff77;
}
.diff_sub {
background-color: #ffaaaa;
}
}

View File

@ -1,4 +1,23 @@
$(function() {
const foldableClassObserver = new MutationObserver((mutations) => {
mutations.forEach(mu => {
const old_folded = (mu.oldValue.indexOf('folded') != -1);
const new_folded = mu.target.classList.contains('folded')
if (old_folded == new_folded) { return; }
var pref_message = Object();
pref_message[mu.target.dataset.sectionFoldedPrefName] = new_folded;
fetch('/api/user-preferences/save', {
method: 'POST',
credentials: 'include',
headers: {'Accept': 'application/json', 'Content-Type': 'application/json'},
body: JSON.stringify(pref_message)
});
});
});
document.querySelectorAll('[data-section-folded-pref-name]').forEach(
elt => foldableClassObserver.observe(elt, {attributes: true, attributeFilter: ['class'], attributeOldValue: true})
);
$('[data-total]').each(function() {
var total = $(this).data('total');
var booked = $(this).data('booked');

View File

@ -1,5 +1,5 @@
<p class="snapshot-description">{{ fromdesc|safe }} ➔ {{ todesc|safe }}</p>
<div class="diff">
<div class="snapshot-diff">
{% if mode == 'json' %}
{{ diff_serialization|safe }}
{% else %}

View File

@ -1,5 +1,15 @@
{% extends "chrono/manager_agenda_history.html" %}
{% load i18n %}
{% load gadjo static i18n %}
{% block css %}
{{ block.super }}
<link rel="stylesheet" type="text/css" media="all" href="{% static "css/gadjo.snapshotdiff.css" %}?{% start_timestamp %}"/>
{% endblock %}
{% block extrascripts %}
{{ block.super }}
<script src="{% static "js/gadjo.snapshotdiff.js" %}?{% start_timestamp %}"></script>
{% endblock %}
{% block appbar %}
<h2>{% trans 'Compare snapshots' %} ({% if mode == 'json' %}{% trans "JSON" %}{% else %}{% trans "Inspect" %}{% endif %})</h2>

View File

@ -132,6 +132,19 @@
{{ value }}
</li>
{% endfor %}
{% if object.events_type %}
<li class="parameter-custom-fields">
<span class="parameter">{% trans "Custom fields:" %}</span>
<ul>
{% for value in object.events_type.get_custom_fields %}
<li class="parameter-custom-field-{{ value.varname }}">
<span class="parameter">{% blocktrans with label=value.label %}{{ label }}:{% endblocktrans %}</span>
{{ event.get_custom_fields|get:value.varname|default:"" }}
</li>
{% endfor %}
</ul>
</li>
{% endif %}
</ul>
{% endfor %}
</div>

View File

@ -1,5 +1,15 @@
{% extends "chrono/manager_category_history.html" %}
{% load i18n %}
{% load gadjo static i18n %}
{% block css %}
{{ block.super }}
<link rel="stylesheet" type="text/css" media="all" href="{% static "css/gadjo.snapshotdiff.css" %}?{% start_timestamp %}"/>
{% endblock %}
{% block extrascripts %}
{{ block.super }}
<script src="{% static "js/gadjo.snapshotdiff.js" %}?{% start_timestamp %}"></script>
{% endblock %}
{% block appbar %}
<h2>{% trans 'Compare snapshots' %} ({% if mode == 'json' %}{% trans "JSON" %}{% else %}{% trans "Inspect" %}{% endif %})</h2>

View File

@ -1,5 +1,15 @@
{% extends "chrono/manager_events_type_history.html" %}
{% load i18n %}
{% load gadjo static i18n %}
{% block css %}
{{ block.super }}
<link rel="stylesheet" type="text/css" media="all" href="{% static "css/gadjo.snapshotdiff.css" %}?{% start_timestamp %}"/>
{% endblock %}
{% block extrascripts %}
{{ block.super }}
<script src="{% static "js/gadjo.snapshotdiff.js" %}?{% start_timestamp %}"></script>
{% endblock %}
{% block appbar %}
<h2>{% trans 'Compare snapshots' %} ({% if mode == 'json' %}{% trans "JSON" %}{% else %}{% trans "Inspect" %}{% endif %})</h2>

View File

@ -1,5 +1,5 @@
{% extends "chrono/manager_base.html" %}
{% load i18n thumbnail %}
{% load i18n thumbnail chrono %}
{% block appbar %}
{% include 'chrono/includes/application_appbar_fragment.html' with title_no_application=_('Agendas outside applications') title_object_list=_('Agendas') %}
@ -16,19 +16,23 @@
{% if object_list %}
{% regroup object_list by category as agenda_groups %}
{% for group in agenda_groups %}
<div class="section">
{% if group.grouper %}<h3>{{ group.grouper }}</h3>{% elif not forloop.first %}<h3>{% trans "Misc" %}</h3>{% endif %}
<ul class="objects-list single-links">
{% for object in group.list %}
<li>
<a href="{% url 'chrono-manager-agenda-view' pk=object.id %}">
<span class="badge">{{ object.get_real_kind_display }}</span>
{% include 'chrono/includes/application_icon_fragment.html' %}
{{ object.label }}{% if user.is_staff %} <span class="identifier">[{% trans "identifier:" %} {{ object.slug }}]{% endif %}</span>
</a>
</li>
{% endfor %}
</ul>
{% with i=group.grouper.id|stringformat:"s" %}
{% with foldname='foldable-manager-category-group-'|add:i %}
<div class="section foldable {% if user|get_preference:foldname %}folded{% endif %}" data-section-folded-pref-name="{{foldname}}">
{% endwith %}
{% endwith %}
{% if group.grouper %}<h3>{{ group.grouper }}</h3>{% elif not forloop.first %}<h3>{% trans "Misc" %}</h3>{% endif %}
<ul class="objects-list single-links">
{% for object in group.list %}
<li>
<a href="{% url 'chrono-manager-agenda-view' pk=object.id %}">
<span class="badge">{{ object.get_real_kind_display }}</span>
{% include 'chrono/includes/application_icon_fragment.html' %}
{{ object.label }}{% if user.is_staff %} <span class="identifier">[{% trans "identifier:" %} {{ object.slug }}]{% endif %}</span>
</a>
</li>
{% endfor %}
</ul>
</div>
{% endfor %}
{% elif not no_application %}

View File

@ -1,5 +1,15 @@
{% extends "chrono/manager_resource_history.html" %}
{% load i18n %}
{% load gadjo static i18n %}
{% block css %}
{{ block.super }}
<link rel="stylesheet" type="text/css" media="all" href="{% static "css/gadjo.snapshotdiff.css" %}?{% start_timestamp %}"/>
{% endblock %}
{% block extrascripts %}
{{ block.super }}
<script src="{% static "js/gadjo.snapshotdiff.js" %}?{% start_timestamp %}"></script>
{% endblock %}
{% block appbar %}
<h2>{% trans 'Compare snapshots' %} ({% if mode == 'json' %}{% trans "JSON" %}{% else %}{% trans "Inspect" %}{% endif %})</h2>

View File

@ -1,5 +1,15 @@
{% extends "chrono/manager_unavailability_calendar_history.html" %}
{% load i18n %}
{% load gadjo static i18n %}
{% block css %}
{{ block.super }}
<link rel="stylesheet" type="text/css" media="all" href="{% static "css/gadjo.snapshotdiff.css" %}?{% start_timestamp %}"/>
{% endblock %}
{% block extrascripts %}
{{ block.super }}
<script src="{% static "js/gadjo.snapshotdiff.js" %}?{% start_timestamp %}"></script>
{% endblock %}
{% block appbar %}
<h2>{% trans 'Compare snapshots' %} ({% if mode == 'json' %}{% trans "JSON" %}{% else %}{% trans "Inspect" %}{% endif %})</h2>

View File

@ -17,6 +17,8 @@
from django import template
from django.utils.formats import date_format
from chrono.apps.user_preferences.models import UserPreferences
register = template.Library()
@ -31,3 +33,9 @@ def human_date_range(date_start, date_end):
date_start_format = 'd'
return '%s %s' % (date_format(date_start, date_start_format), date_format(date_end, date_end_format))
@register.filter
def get_preference(user, pref_name):
user_preferences, dummy = UserPreferences.objects.get_or_create(user=user)
return user_preferences.preferences.get(pref_name) or False

View File

@ -63,6 +63,7 @@ INSTALLED_APPS = (
'chrono.apps.ants_hub',
'chrono.apps.export_import',
'chrono.apps.snapshot',
'chrono.apps.user_preferences',
)
MIDDLEWARE = (

View File

@ -28,7 +28,7 @@ def admin_user():
return user
@pytest.fixture(params=['Europe/Brussels', 'Asia/Kolkata', 'Brazil/East'])
@pytest.fixture(params=['Europe/Brussels', 'Asia/Kolkata', 'America/Sao_Paulo'])
def time_zone(request, settings):
settings.TIME_ZONE = request.param

View File

@ -711,7 +711,7 @@ def test_api_events_fillslots_with_lock_code(app, user, freezer):
'waiting_list_places': 1,
},
'Event 2': {
'start_datetime': now() + datetime.timedelta(days=1),
'start_datetime': now() + datetime.timedelta(days=2),
'places': 2,
'waiting_list_places': 1,
},
@ -807,7 +807,7 @@ def test_api_events_fillslots_with_lock_code_expiration(app, user, freezer):
'waiting_list_places': 1,
},
'Event 2': {
'start_datetime': now() + datetime.timedelta(days=1),
'start_datetime': now() + datetime.timedelta(days=2),
'places': 2,
'waiting_list_places': 1,
},

View File

@ -0,0 +1,71 @@
import json
import pytest
from django.urls import reverse
from chrono.apps.user_preferences.models import UserPreferences
from tests.utils import login
pytestmark = pytest.mark.django_db
def test_user_preferences_api_ok(app, admin_user):
login(app)
fake_id = 'fake-id-1'
url = reverse('api-user-preferences')
app.post_json(url, params={fake_id: True}, status=204)
user_pref = UserPreferences.objects.get(user=admin_user)
assert user_pref.preferences[fake_id] is True
app.post_json(url, params={fake_id: False}, status=204)
user_pref = UserPreferences.objects.get(user=admin_user)
assert user_pref.preferences[fake_id] is False
fake_id2 = 'fake-id-2'
app.post_json(url, params={fake_id2: False}, status=204)
user_pref = UserPreferences.objects.get(user=admin_user)
assert user_pref.preferences[fake_id] is False
assert user_pref.preferences[fake_id2] is False
app.post_json(url, params={fake_id2: False}, status=204)
user_pref = UserPreferences.objects.get(user=admin_user)
assert user_pref.preferences[fake_id] is False
assert user_pref.preferences[fake_id2] is False
app.post_json(url, params={fake_id2: True}, status=204)
user_pref = UserPreferences.objects.get(user=admin_user)
assert user_pref.preferences[fake_id] is False
assert user_pref.preferences[fake_id2] is True
@pytest.mark.parametrize(
'bad_body',
(
json.dumps({'fake-id-1': True, 'fake-id-2': False}),
'"not a dict"',
'[1,2,3]',
'{\'fake-id-1\': true',
),
)
def test_user_preferences_api_invalid(app, admin_user, bad_body):
login(app)
url = reverse('api-user-preferences')
app.post(url, params=bad_body, status=400)
def test_user_preferences_api_large_payload(app, admin_user):
login(app)
url = reverse('api-user-preferences')
app.post(url, params='a' * 1024, status=400)
app.post_json(url, params={'b' * 1024: True}, status=400)
def test_user_preferences_api_unauthorized(app):
url = reverse('api-user-preferences')
app.post(url, params={'toto': True}, status=302)

View File

@ -1,5 +1,9 @@
import django_webtest
import pytest
from django.core.signals import setting_changed
from django.dispatch import receiver
from chrono.utils.timezone import get_default_timezone
@pytest.fixture
@ -23,3 +27,10 @@ def nocache(settings):
'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
}
}
@receiver(setting_changed)
def update_connections_time_zone(**kwargs):
if kwargs['setting'] == 'TIME_ZONE':
# Reset local time zone lru cache
get_default_timezone.cache_clear()

View File

@ -4,11 +4,17 @@ from django.test.utils import CaptureQueriesContext
from chrono.agendas.models import Agenda, Category
from chrono.apps.snapshot.models import CategorySnapshot
from chrono.apps.user_preferences.models import UserPreferences
from tests.utils import login
pytestmark = pytest.mark.django_db
def update_preference(user_preference, name, value):
user_preference.preferences.update({name: value})
user_preference.save()
def test_list_categories_as_manager(app, manager_user):
agenda = Agenda(label='Foo Bar')
agenda.view_role = manager_user.groups.all()[0]
@ -97,3 +103,80 @@ def test_inspect_category(app, admin_user):
with CaptureQueriesContext(connection) as ctx:
resp = resp.click('Inspect')
assert len(ctx.captured_queries) == 3
def test_category_fold_preferences(app, admin_user):
category1 = Category.objects.create(label='Foo bar')
category2 = Category.objects.create(label='Toto')
pref_name1 = f'foldable-manager-category-group-{category1.id}'
pref_name2 = f'foldable-manager-category-group-{category2.id}'
Agenda.objects.create(label='Foo bar', category=category1)
agenda2 = Agenda.objects.create(label='Titi', category=category2)
app = login(app)
resp = app.get('/manage/')
elt = resp.pyquery.find(f'div[data-section-folded-pref-name={pref_name1}]')
assert len(elt) == 1
assert 'foldable' in elt[0].classes
assert 'folded' not in elt[0].classes
elt = resp.pyquery.find(f'div[data-section-folded-pref-name={pref_name2}]')
assert len(elt) == 1
assert 'foldable' in elt[0].classes
assert 'folded' not in elt[0].classes
user_prefs = UserPreferences.objects.get(user=admin_user)
update_preference(user_prefs, pref_name1, True)
resp = app.get('/manage/')
elt = resp.pyquery.find(f'div[data-section-folded-pref-name={pref_name1}]')
assert len(elt) == 1
assert 'foldable' in elt[0].classes
assert 'folded' in elt[0].classes
elt = resp.pyquery.find(f'div[data-section-folded-pref-name={pref_name2}]')
assert len(elt) == 1
assert 'foldable' in elt[0].classes
assert 'folded' not in elt[0].classes
# Order is preserved when adding a new category : preferences are preserved
category_temp = Category.objects.create(label='Tata0')
category3 = Category.objects.create(label='Tata')
pref_name3 = f'foldable-manager-category-group-{category3.id}'
category_temp.delete()
Agenda.objects.create(label='Titi', category=category3)
update_preference(user_prefs, pref_name1, False)
update_preference(user_prefs, pref_name2, True)
resp = app.get('/manage/')
elt = resp.pyquery.find(f'div[data-section-folded-pref-name={pref_name1}]')
assert len(elt) == 1
assert 'foldable' in elt[0].classes
assert 'folded' not in elt[0].classes
elt = resp.pyquery.find(f'div[data-section-folded-pref-name={pref_name2}]')
assert len(elt) == 1
assert 'foldable' in elt[0].classes
assert 'folded' in elt[0].classes
elt = resp.pyquery.find(f'div[data-section-folded-pref-name={pref_name3}]')
assert len(elt) == 1
assert 'foldable' in elt[0].classes
assert 'folded' not in elt[0].classes
# Preferences are not "shifted" when a category is deleted
agenda2.delete()
resp = app.get('/manage/')
elt = resp.pyquery.find(f'div[data-section-folded-pref-name={pref_name1}]')
assert len(elt) == 1
assert 'foldable' in elt[0].classes
assert 'folded' not in elt[0].classes
elt = resp.pyquery.find(f'div[data-section-folded-pref-name={pref_name3}]')
assert len(elt) == 1
assert 'foldable' in elt[0].classes
assert 'folded' not in elt[0].classes

View File

@ -764,7 +764,7 @@ def test_export_events(app, admin_user):
resp = app.get('/manage/agendas/%s/import-events' % agenda.id)
resp.form['events_csv_file'] = Upload(
't.csv',
b'2016-09-16,23:30,10,5,label,slug,"description\nfoobar",pricing,url,2016-10-16 00:00,90',
b'2016-09-16,23:30,10,5,label,slug,"description\nfoobar",pricing,https://example.net/event,2016-10-16 00:00,90',
'text/csv',
)
resp.form.submit(status=302)
@ -774,7 +774,7 @@ def test_export_events(app, admin_user):
csv_export
== 'date,time,number of places,number of places in waiting list,label,identifier,description,pricing,URL,publication date/time,duration\r\n'
'2016-09-16,00:30,10,0,,foo-bar-event,,,,,\r\n'
'2016-09-16,23:30,10,5,label,slug,"description\nfoobar",pricing,url,2016-10-16 00:00,90\r\n'
'2016-09-16,23:30,10,5,label,slug,"description\nfoobar",pricing,https://example.net/event,2016-10-16 00:00,90\r\n'
)
@ -967,7 +967,7 @@ def test_import_events(app, admin_user):
resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200)
resp.form['events_csv_file'] = Upload(
't.csv',
b'2016-09-16,18:00,10,5,label,slug,"description\nfoobar",pricing,url,2016-10-16,90',
b'2016-09-16,18:00,10,5,label,slug,"description\nfoobar",pricing,https://example.net/event,2016-10-16,90',
'text/csv',
)
resp = resp.form.submit(status=302)
@ -975,7 +975,7 @@ def test_import_events(app, admin_user):
event = Event.objects.get()
assert event.description == 'description\nfoobar'
assert event.pricing == 'pricing'
assert event.url == 'url'
assert event.url == 'https://example.net/event'
assert str(event.publication_datetime) == '2016-10-15 22:00:00+00:00'
assert str(event.publication_datetime.tzinfo) == 'UTC'
assert event.duration == 90
@ -983,7 +983,7 @@ def test_import_events(app, admin_user):
resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200)
resp.form['events_csv_file'] = Upload(
't.csv',
b'2016-09-16,18:00,10,5,label,slug,"description\nfoobar",pricing,url,2016-10-16 10:00,90',
b'2016-09-16,18:00,10,5,label,slug,"description\nfoobar",pricing,https://example.net/event,2016-10-16 10:00,90',
'text/csv',
)
resp = resp.form.submit(status=302)
@ -991,7 +991,7 @@ def test_import_events(app, admin_user):
event = Event.objects.get()
assert event.description == 'description\nfoobar'
assert event.pricing == 'pricing'
assert event.url == 'url'
assert event.url == 'https://example.net/event'
assert str(event.publication_datetime) == '2016-10-16 08:00:00+00:00'
assert str(event.publication_datetime.tzinfo) == 'UTC'
assert event.duration == 90
@ -999,7 +999,9 @@ def test_import_events(app, admin_user):
# publication date/time bad format
resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200)
resp.form['events_csv_file'] = Upload(
't.csv', b'2016-09-16,18:00,10,5,label,slug,description,pricing,url,foobar', 'text/csv'
't.csv',
b'2016-09-16,18:00,10,5,label,slug,description,pricing,https://example.net/event,foobar',
'text/csv',
)
resp = resp.form.submit(status=200)
assert 'Wrong publication date/time format. (1st event)' in resp.text
@ -1007,7 +1009,9 @@ def test_import_events(app, admin_user):
# duration bad format
resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200)
resp.form['events_csv_file'] = Upload(
't.csv', b'2016-09-16,18:00,10,5,label,slug,description,pricing,url,2016-09-16,foobar', 'text/csv'
't.csv',
b'2016-09-16,18:00,10,5,label,slug,description,pricing,https://example.net/event,2016-09-16,foobar',
'text/csv',
)
resp = resp.form.submit(status=200)
assert 'Duration must be an integer. (1st event)' in resp.text
@ -1202,7 +1206,9 @@ def test_import_events_partial_bookings(app, admin_user):
# no end time
resp = app.get('/manage/agendas/%s/import-events' % agenda.pk)
resp.form['events_csv_file'] = Upload(
't.csv', b'2016-09-16,18:00,10,5,label,slug,description,pricing,url,2016-09-16', 'text/csv'
't.csv',
b'2016-09-16,18:00,10,5,label,slug,description,pricing,https://example.net/event,2016-09-16',
'text/csv',
)
resp = resp.form.submit(status=200)
assert 'Missing end_time.' in resp.text
@ -1210,7 +1216,9 @@ def test_import_events_partial_bookings(app, admin_user):
# invalid end time
resp = app.get('/manage/agendas/%s/import-events' % agenda.pk)
resp.form['events_csv_file'] = Upload(
't.csv', b'2016-09-16,18:00,10,5,label,slug,description,pricing,url,2016-09-16,xxx', 'text/csv'
't.csv',
b'2016-09-16,18:00,10,5,label,slug,description,pricing,https://example.net/event,2016-09-16,xxx',
'text/csv',
)
resp = resp.form.submit(status=200)
assert '“xxx” value has an invalid format' in resp.text

View File

@ -57,7 +57,7 @@ def test_agenda_history(settings, app, admin_user):
assert resp.text.count('<del>') == 0
else:
assert resp.text.count('diff_sub') == 1
assert resp.text.count('diff_add') == 17
assert resp.text.count('diff_add') == 18
assert resp.text.count('diff_chg') == 0
resp = app.get(
'/manage/agendas/%s/history/compare/?version1=%s&version2=%s'
@ -66,7 +66,7 @@ def test_agenda_history(settings, app, admin_user):
assert 'Snapshot (%s)' % (snapshot1.pk) in resp
assert 'Snapshot (%s) - (Version 42.0)' % (snapshot2.pk) in resp
assert resp.text.count('diff_sub') == 1
assert resp.text.count('diff_add') == 17
assert resp.text.count('diff_add') == 18
assert resp.text.count('diff_chg') == 0
# check compare on application version number

View File

@ -264,7 +264,15 @@ def test_import_export_events_agenda_options(app):
def test_import_export_event_details(app):
agenda = Agenda.objects.create(label='Foo Bar', kind='events')
events_type = EventsType.objects.create(
label='Foo',
custom_fields=[
{'varname': 'text', 'label': 'Text', 'field_type': 'text'},
{'varname': 'textarea', 'label': 'TextArea', 'field_type': 'textarea'},
{'varname': 'bool', 'label': 'Bool', 'field_type': 'bool'},
],
)
agenda = Agenda.objects.create(label='Foo Bar', kind='events', events_type=events_type)
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
Event.objects.create(
slug='event',
@ -277,6 +285,11 @@ def test_import_export_event_details(app):
start_datetime=now(),
end_time=datetime.time(20, 00),
duration=30,
custom_fields={
'text': 'foo',
'textarea': 'foo bar',
'bool': True,
},
)
# check event (agenda, slug) unicity
agenda2 = Agenda.objects.create(label='Foo Bar 2', kind='events')
@ -308,12 +321,25 @@ def test_import_export_event_details(app):
assert str(first_imported_event.publication_datetime.tzinfo) == 'UTC'
assert first_imported_event.duration == 30
assert first_imported_event.end_time == datetime.time(20, 00)
assert first_imported_event.custom_fields == {
'text': 'foo',
'textarea': 'foo bar',
'bool': True,
}
assert Agenda.objects.get(label='Foo Bar 2').event_set.first().slug == 'event'
def test_import_export_recurring_event(app, freezer):
freezer.move_to('2021-01-12 12:10')
agenda = Agenda.objects.create(label='Foo Bar', kind='events')
events_type = EventsType.objects.create(
label='Foo',
custom_fields=[
{'varname': 'text', 'label': 'Text', 'field_type': 'text'},
{'varname': 'textarea', 'label': 'TextArea', 'field_type': 'textarea'},
{'varname': 'bool', 'label': 'Bool', 'field_type': 'bool'},
],
)
agenda = Agenda.objects.create(label='Foo Bar', kind='events', events_type=events_type)
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
event = Event.objects.create(
agenda=agenda,
@ -323,6 +349,11 @@ def test_import_export_recurring_event(app, freezer):
recurrence_week_interval=2,
places=10,
slug='test',
custom_fields={
'text': 'foo',
'textarea': 'foo bar',
'bool': True,
},
)
assert Event.objects.count() == 1
@ -355,6 +386,14 @@ def test_import_export_recurring_event(app, freezer):
event = Event.objects.get(slug='test')
assert Event.objects.filter(primary_event=event).count() == 1
first_event = event.recurrences.first()
assert first_event.custom_fields == {
'text': 'foo',
'textarea': 'foo bar',
'bool': True,
}
first_event.custom_fields = {}
first_event.save()
# import again
with tempfile.NamedTemporaryFile() as f:
@ -364,6 +403,13 @@ def test_import_export_recurring_event(app, freezer):
event = Event.objects.get(slug='test')
assert Event.objects.filter(primary_event=event).count() == 1
first_event2 = event.recurrences.first()
assert first_event.pk == first_event2.pk
assert first_event2.custom_fields == {
'text': 'foo',
'textarea': 'foo bar',
'bool': True,
}
# import again but change places
payload = json.loads(output)
@ -927,6 +973,12 @@ def test_import_export_time_period_exception_source_ics_file(mocked_get):
import_site(payload)
assert '%s' % excinfo.value == 'Bad ics file'
Agenda.objects.all().delete()
payload['agendas'][0]['desks'][0]['exception_sources'][0]['ics_file'] = None
import_site(payload)
assert TimePeriodExceptionSource.objects.count() == 0
assert TimePeriodException.objects.count() == 0
@override_settings(
EXCEPTIONS_SOURCES={

View File

@ -45,7 +45,7 @@ allowlist_externals =
commands =
./getlasso3.sh
python3 setup.py compile_translations
py.test {posargs:-v --dist loadfile {env:COVERAGE:} --numprocesses={env:NUMPROCESSES:1} tests/}
py.test {posargs:-v {env:COVERAGE:} --numprocesses={env:NUMPROCESSES:1} tests/}
codestyle: pre-commit run --all-files --show-diff-on-failure
[testenv:pylint]