Compare commits
10 Commits
8504dc18dd
...
77f3373820
Author | SHA1 | Date |
---|---|---|
Lauréline Guérin | 77f3373820 | |
Valentin Deniaud | 169dc0a69a | |
Lauréline Guérin | 88d8feacd8 | |
Thomas NOËL | 6ee8fbf78d | |
Lauréline Guérin | 3403295d3d | |
Yann Weber | 0563e0642d | |
Yann Weber | 5a90c4851b | |
Yann Weber | d03e1e7940 | |
Yann Weber | b0f956c223 | |
Yann Weber | 570cf81c8e |
|
@ -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'),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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')),
|
||||
]
|
||||
|
|
|
@ -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'),
|
||||
]
|
|
@ -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)
|
|
@ -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
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -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)
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -63,6 +63,7 @@ INSTALLED_APPS = (
|
|||
'chrono.apps.ants_hub',
|
||||
'chrono.apps.export_import',
|
||||
'chrono.apps.snapshot',
|
||||
'chrono.apps.user_preferences',
|
||||
)
|
||||
|
||||
MIDDLEWARE = (
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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)
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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={
|
||||
|
|
2
tox.ini
2
tox.ini
|
@ -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]
|
||||
|
|
Loading…
Reference in New Issue