Planning external calendars #561

This commit is contained in:
Daniel Grams 2023-11-25 15:18:16 +01:00
parent d3375cecb1
commit 39131aded9
19 changed files with 3003 additions and 22 deletions

View File

@ -1,14 +1,8 @@
{ {
"editor.formatOnSave": true, "editor.formatOnSave": true,
"python.pythonPath": "./env/bin/python3",
"python.formatting.provider": "black",
"isort.args": ["-sp", ".isort.cfg"], "isort.args": ["-sp", ".isort.cfg"],
"python.linting.enabled": true,
"python.linting.pylintEnabled": false,
"python.linting.flake8Enabled": true,
"python.testing.pytestArgs": ["tests", "--capture=sys"], "python.testing.pytestArgs": ["tests", "--capture=sys"],
"python.testing.unittestEnabled": false, "python.testing.unittestEnabled": false,
"python.testing.nosetestsEnabled": false,
"python.testing.pytestEnabled": true, "python.testing.pytestEnabled": true,
"[python]": { "[python]": {
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {

View File

@ -0,0 +1,33 @@
"""empty message
Revision ID: ef547963d7f0
Revises: 182a83a49c1f
Create Date: 2023-11-25 10:12:48.763298
"""
import sqlalchemy as sa
import sqlalchemy_utils
from alembic import op
from project import dbtypes
# revision identifiers, used by Alembic.
revision = "ef547963d7f0"
down_revision = "182a83a49c1f"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
"settings",
sa.Column("planning_external_calendars", sa.UnicodeText(), nullable=True),
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("settings", "planning_external_calendars")
# ### end Alembic commands ###

View File

@ -1,3 +1,5 @@
import json
from flask_babel import lazy_gettext from flask_babel import lazy_gettext
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import BooleanField, RadioField, StringField, SubmitField, TextAreaField from wtforms import BooleanField, RadioField, StringField, SubmitField, TextAreaField
@ -18,6 +20,30 @@ class AdminSettingsForm(FlaskForm):
submit = SubmitField(lazy_gettext("Save")) submit = SubmitField(lazy_gettext("Save"))
class AdminPlanningForm(FlaskForm):
planning_external_calendars = TextAreaField(
lazy_gettext("External calendars"), validators=[Optional()]
)
submit = SubmitField(lazy_gettext("Save"))
def validate(self, extra_validators=None):
result = super().validate(extra_validators)
if self.planning_external_calendars.data:
try:
json_object = json.loads(self.planning_external_calendars.data)
self.planning_external_calendars.data = json.dumps(
json_object, indent=2
)
except Exception as e: # pragma: no cover
msg = str(e)
self.planning_external_calendars.errors.append(msg)
result = False
return result
class ResetTosAceptedForm(FlaskForm): class ResetTosAceptedForm(FlaskForm):
reset_for_users = BooleanField( reset_for_users = BooleanField(
lazy_gettext("Reset for all users"), validators=[DataRequired()] lazy_gettext("Reset for all users"), validators=[DataRequired()]

View File

@ -14,3 +14,4 @@ class Settings(db.Model, TrackableMixin):
privacy = deferred(Column(UnicodeText())) privacy = deferred(Column(UnicodeText()))
start_page = deferred(Column(UnicodeText())) start_page = deferred(Column(UnicodeText()))
announcement = deferred(Column(UnicodeText())) announcement = deferred(Column(UnicodeText()))
planning_external_calendars = deferred(Column(UnicodeText()))

View File

@ -5,6 +5,7 @@ from sqlalchemy import and_
from project.models import Event, EventOrganizer, EventPlace from project.models import Event, EventOrganizer, EventPlace
from project.services.importer.ld_json_importer import LdJsonImporter from project.services.importer.ld_json_importer import LdJsonImporter
from project.utils import decode_response_content
class EventImporter: class EventImporter:
@ -21,12 +22,7 @@ class EventImporter:
] = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36" ] = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36"
response = requests.get(sanitized_url, headers=headers) response = requests.get(sanitized_url, headers=headers)
html = decode_response_content(response)
try:
html = response.content.decode("UTF-8")
except Exception: # pragma: no cover
html = response.content.decode(response.apparent_encoding)
return self.load_event_from_html(html, absolute_url) return self.load_event_from_html(html, absolute_url)
def load_event_from_html(self, html: str, origin_url: str): def load_event_from_html(self, html: str, origin_url: str):

View File

@ -39,6 +39,64 @@ const PlanningList = {
</v-icon> </v-icon>
{{ $t("comp.filter") }} {{ $t("comp.filter") }}
</v-btn> </v-btn>
<v-dialog
v-model="externalMenu"
persistent
max-width="600px"
>
<template v-slot:activator="{ on, attrs }">
<v-btn color="primary"
v-if="externalCalMenuVisible"
depressed outlined class="mr-4" v-bind="attrs"
v-on="on">
<v-icon small>
mdi-calendar-multiple
</v-icon>
</v-btn>
</template>
<v-card>
<v-card-title>{{ $t("comp.externalCalTitle") }}</v-card-title>
<v-card-text>
<v-list two-line flat>
<v-list-item-group>
<v-list-item v-for="(externalCal, i) in externalCals" :value="externalCal" :key="i">
<template>
<v-list-item-action>
<v-checkbox v-model="externalCal.active"></v-checkbox>
</v-list-item-action>
<v-list-item-content>
<v-list-item-title v-text="externalCal.title"></v-list-item-title>
<v-list-item-subtitle v-text="externalCal.url"></v-list-item-subtitle>
</v-list-item-content>
</template>
</v-list-item>
</v-list-item-group>
</v-list>
<!--<v-text-field
v-model="externalNewUrl"
@keyup.enter="addExternalUrl"
:label="$t('comp.externalCalAddUrl')"
required
></v-text-field>-->
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
text
@click="closeExternalMenu"
>
{{ $t("shared.close") }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn-toggle <v-btn-toggle
@ -74,7 +132,7 @@ const PlanningList = {
locale="de" locale="de"
:weekdays="[1, 2, 3, 4, 5, 6, 0]" :weekdays="[1, 2, 3, 4, 5, 6, 0]"
:type="type" :type="type"
:events="events" :events="allEvents"
:event-color="getEventColor" :event-color="getEventColor"
event-more-text="{0} weitere" event-more-text="{0} weitere"
@click:event="showEvent" @click:event="showEvent"
@ -92,12 +150,19 @@ const PlanningList = {
<v-card-title>{{ selectedEvent.name }}</v-card-title> <v-card-title>{{ selectedEvent.name }}</v-card-title>
<v-card-subtitle> <v-card-subtitle>
<v-icon small>mdi-calendar</v-icon> {{ $root.render_event_date(selectedEvent.date.start, selectedEvent.date.end, selectedEvent.date.allday) }} <v-icon small>mdi-calendar</v-icon> {{ $root.render_event_date(selectedEvent.date.start, selectedEvent.date.end, selectedEvent.date.allday) }}
<event-warning-pills :event="selectedEvent.date.event"></event-warning-pills> <event-warning-pills v-if="selectedEvent.date.event" :event="selectedEvent.date.event"></event-warning-pills>
</v-card-subtitle> </v-card-subtitle>
<v-card-text> <v-card-text>
<div><v-icon small>mdi-database</v-icon> {{ selectedEvent.date.event.organization.name }}</div> <template v-if="selectedEvent.date.event">
<div v-if="selectedEvent.date.event.organizer.name != selectedEvent.date.event.organization.name"><v-icon small>mdi-server</v-icon> {{ selectedEvent.date.event.organizer.name }}</div> <div><v-icon small>mdi-database</v-icon> {{ selectedEvent.date.event.organization.name }}</div>
<div><v-icon small>mdi-map-marker</v-icon> {{ selectedEvent.date.event.place.name }}</div> <div v-if="selectedEvent.date.event.organizer.name != selectedEvent.date.event.organization.name"><v-icon small>mdi-server</v-icon> {{ selectedEvent.date.event.organizer.name }}</div>
<div><v-icon small>mdi-map-marker</v-icon> {{ selectedEvent.date.event.place.name }}</div>
</template>
<template v-if="selectedEvent.date.vevent">
<div v-if="selectedEvent.date.vevent.description">{{ selectedEvent.date.vevent.description }}</div>
<div v-if="selectedEvent.date.vevent.location"><v-icon small>mdi-map-marker</v-icon> {{ selectedEvent.date.vevent.location }}</div>
<div><v-icon small>mdi-database</v-icon> {{ selectedEvent.date.vevent.url }}</div>
</template>
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
@ -110,6 +175,7 @@ const PlanningList = {
{{ $t("shared.close") }} {{ $t("shared.close") }}
</v-btn> </v-btn>
<v-btn <v-btn
v-if="selectedEvent.date.event"
text text
color="primary" color="primary"
@click="openEventDate(selectedEvent.date)" @click="openEventDate(selectedEvent.date)"
@ -137,6 +203,8 @@ const PlanningList = {
week: "Week", week: "Week",
month: "Month", month: "Month",
filter: "Filter", filter: "Filter",
externalCalTitle: "External Calendars",
externalCalAddUrl: "Add link to iCal calendar",
}, },
}, },
de: { de: {
@ -148,6 +216,8 @@ const PlanningList = {
week: "Woche", week: "Woche",
month: "Monat", month: "Monat",
filter: "Filter", filter: "Filter",
externalCalTitle: "Externe Kalender",
externalCalAddUrl: "Link zu iCal-Kalendar hinzufügen",
}, },
}, },
}, },
@ -164,8 +234,21 @@ const PlanningList = {
isLoading: false, isLoading: false,
maxDates: 200, maxDates: 200,
warning: null, warning: null,
externalMenu: false,
externalNewUrl: "",
externalEvents: [],
externalCals: [],
}), }),
computed: {
allEvents() {
return [...this.events,...this.externalEvents];
},
externalCalMenuVisible() {
return this.externalCals.length > 0;
},
},
mounted () { mounted () {
this.externalCals = this.$root.externalCals;
this.$refs.calendar.checkChange(); this.$refs.calendar.checkChange();
}, },
methods: { methods: {
@ -177,7 +260,11 @@ const PlanningList = {
calendarChanged({ start, end }) { calendarChanged({ start, end }) {
$('#date_from').val(start.date); $('#date_from').val(start.date);
$('#date_to').val(end.date); $('#date_to').val(end.date);
this.load();
},
load() {
this.loadEvents(); this.loadEvents();
this.loadExternalCalendars();
}, },
loadEvents(page = 1) { loadEvents(page = 1) {
$('#page').val(page); $('#page').val(page);
@ -220,6 +307,39 @@ const PlanningList = {
} }
}); });
}, },
loadExternalCalendars() {
this.externalEvents = [];
this.externalCals.forEach(externalCal => {
if (externalCal.active) {
this.loadExternalCalendar(externalCal);
}
});
},
loadExternalCalendar(externalCal) {
var bodyFormData = new FormData();
bodyFormData.append('date_from', $('#date_from').val());
bodyFormData.append('date_to', $('#date_to').val());
bodyFormData.append('url', externalCal.url);
const vm = this;
axios
.post(`/js/icalevents`, bodyFormData, {
withCredentials: true,
})
.then((response) => {
for (const item of response.data.items) {
vm.externalEvents.push({
name: item.name,
start: moment(item.start).toDate(),
end: item.end != null ? moment(item.end).toDate() : null,
timed: !item.allday,
date: item,
color: '#aa7bff'
});
}
});
},
viewDay ({ date }) { viewDay ({ date }) {
this.focus = date; this.focus = date;
this.type = 'day'; this.type = 'day';
@ -274,5 +394,17 @@ const PlanningList = {
handleLoading(isLoading) { handleLoading(isLoading) {
this.isLoading = isLoading; this.isLoading = isLoading;
}, },
addExternalUrl() {
this.externalCals.push({
title: this.externalNewUrl,
url: this.externalNewUrl,
active: true,
});
this.externalNewUrl = "";
},
closeExternalMenu() {
this.externalMenu = false;
this.loadExternalCalendars();
}
}, },
}; };

View File

@ -31,6 +31,10 @@
{{ _('Newsletter') }} {{ _('Newsletter') }}
<i class="fa fa-caret-right"></i> <i class="fa fa-caret-right"></i>
</a> </a>
<a href="{{ url_for('admin_planning') }}" class="list-group-item">
{{ _('Planning') }}
<i class="fa fa-caret-right"></i>
</a>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,38 @@
{% extends "layout.html" %}
{% from "_macros.html" import render_field, render_field_with_errors %}
{%- block title -%}
{{ _('Planning') }}
{%- endblock -%}
{% block content %}
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('admin') }}">{{ _('Admin') }}</a></li>
<li class="breadcrumb-item active" aria-current="page">{{ _('Planning') }}</li>
</ol>
</nav>
<form action="" method="POST">
{{ form.hidden_tag() }}
<textarea class="form-control text-monospace" style="font-size: 0.7rem;" disabled rows="12">
[
{
"title": "Feiertage Deutschland",
"url": "https://www.feiertage-deutschland.de/kalender-download/ics/feiertage-deutschland.ics",
"active": false
},
{
"title": "Schulferien Niedersachsen",
"url": "https://www.feiertage-deutschland.de/kalender-download/ics/schulferien-niedersachsen.ics",
"active": false
}
]
</textarea>
{{ render_field_with_errors(form.planning_external_calendars) }}
{{ render_field(form.submit) }}
</form>
{% endblock %}

View File

@ -108,6 +108,7 @@
}, },
cancel: "Cancel", cancel: "Cancel",
close: "Close", close: "Close",
refresh: "Refresh",
decline: "Decline", decline: "Decline",
docs: "Docs", docs: "Docs",
save: "Save", save: "Save",
@ -230,6 +231,7 @@
close: "Schließen", close: "Schließen",
decline: "Ablehnen", decline: "Ablehnen",
docs: "Doku", docs: "Doku",
refresh: "Aktualisieren",
save: "Speichern", save: "Speichern",
submit: "Senden", submit: "Senden",
view: "Anzeigen", view: "Anzeigen",
@ -382,6 +384,9 @@
var vue_app_data = {}; var vue_app_data = {};
{% endblock %} {% endblock %}
{% block vue_app_data_fill %}
{% endblock %}
{% if config["DOCS_URL"] %} {% if config["DOCS_URL"] %}
vue_app_data["docsUrl"] = "{{ config["DOCS_URL"] }}"; vue_app_data["docsUrl"] = "{{ config["DOCS_URL"] }}";
{% endif %} {% endif %}

View File

@ -43,6 +43,10 @@ Vue.component("PlanningList", PlanningList);
vue_init_data.vuetify = new Vuetify(); vue_init_data.vuetify = new Vuetify();
{% endblock %} {% endblock %}
{% block vue_app_data_fill %}
vue_app_data["externalCals"] = {{ initial_external_calendars | safe }};
{% endblock %}
{% block vue_container %} {% block vue_container %}
{% endblock %} {% endblock %}

View File

@ -1,6 +1,7 @@
import os import os
import pathlib import pathlib
import requests
from flask_babel import lazy_gettext from flask_babel import lazy_gettext
from psycopg2.errorcodes import CHECK_VIOLATION, UNIQUE_VIOLATION from psycopg2.errorcodes import CHECK_VIOLATION, UNIQUE_VIOLATION
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
@ -123,3 +124,10 @@ def get_pending_changes(
result[attr.key] = [new_value, old_value] result[attr.key] = [new_value, old_value]
return result return result
def decode_response_content(response: requests.Response) -> str:
try:
return response.content.decode("UTF-8")
except Exception: # pragma: no cover
return response.content.decode(response.apparent_encoding)

View File

@ -7,6 +7,7 @@ from sqlalchemy.sql import func
from project import app, db from project import app, db
from project.forms.admin import ( from project.forms.admin import (
AdminNewsletterForm, AdminNewsletterForm,
AdminPlanningForm,
AdminSettingsForm, AdminSettingsForm,
AdminTestEmailForm, AdminTestEmailForm,
DeleteAdminUnitForm, DeleteAdminUnitForm,
@ -263,3 +264,25 @@ def admin_user_delete(id):
flash_errors(form) flash_errors(form)
return render_template("admin/delete_user.html", form=form, user=user) return render_template("admin/delete_user.html", form=form, user=user)
@app.route("/admin/planning", methods=("GET", "POST"))
@roles_required("admin")
def admin_planning():
settings = upsert_settings()
form = AdminPlanningForm(obj=settings)
if form.validate_on_submit():
form.populate_obj(settings)
try:
db.session.commit()
flash(gettext("Settings successfully updated"), "success")
return redirect(url_for("admin"))
except SQLAlchemyError as e:
db.session.rollback()
flash(handleSqlError(e), "danger")
else:
flash_errors(form)
return render_template("admin/planning.html", form=form)

View File

@ -1,18 +1,24 @@
from datetime import datetime
import icalendar
import recurring_ical_events
import requests
from flask import request from flask import request
from flask.json import jsonify from flask.json import jsonify
from flask_babel import gettext from flask_babel import gettext
from flask_cors import cross_origin from flask_cors import cross_origin
from flask_security import url_for_security from flask_security import auth_required, url_for_security
from flask_security.utils import localize_callback from flask_security.utils import localize_callback
from sqlalchemy import func from sqlalchemy import func
from project import app, csrf from project import app, csrf
from project.api.custom_widget.schemas import CustomWidgetSchema from project.api.custom_widget.schemas import CustomWidgetSchema
from project.dateutils import form_input_to_date
from project.maputils import find_gmaps_places, get_gmaps_place from project.maputils import find_gmaps_places, get_gmaps_place
from project.models import AdminUnit, CustomWidget, EventOrganizer, EventPlace from project.models import AdminUnit, CustomWidget, EventOrganizer, EventPlace
from project.services.place import get_event_places from project.services.place import get_event_places
from project.services.user import find_user_by_email from project.services.user import find_user_by_email
from project.utils import get_place_str from project.utils import decode_response_content, get_place_str
@app.route("/js/check/organization/short_name", methods=["POST"]) @app.route("/js/check/organization/short_name", methods=["POST"])
@ -181,6 +187,70 @@ def js_autocomplete_gmaps_place():
return jsonify(place) return jsonify(place)
@app.route("/js/icalevents", methods=["POST"])
@auth_required()
def js_icalevents():
csrf.protect()
try:
url = request.form["url"]
date_from = request.form["date_from"]
date_to = request.form["date_to"]
start_date = form_input_to_date(date_from).date()
end_date = form_input_to_date(date_to).date()
response = requests.get(url)
ical_string = decode_response_content(response)
calendar = icalendar.Calendar.from_ical(ical_string)
events = recurring_ical_events.of(calendar).between(start_date, end_date)
items = list()
for event in events:
summary = event.get("SUMMARY")
dt_start = event.get("DTSTART")
dt_end = event.get("DTEND")
location = event.get("LOCATION")
description = event.get("DESCRIPTION")
if not summary or not dt_start: # pragma: no cover
continue
start = dt_start.dt
item = {
"name": summary,
"start": start,
"allday": not isinstance(start, datetime),
}
if dt_end:
item["end"] = dt_end.dt
vevent = {
"url": url,
}
if location:
vevent["location"] = location
if dt_end:
vevent["description"] = description
item["vevent"] = vevent
items.append(item)
result = {
"url": url,
"items": items,
}
return jsonify(result)
except Exception as e: # pragma: no cover
app.logger.exception(url)
return getattr(e, "message", "Unknown error"), 400
@app.route("/js/wlcw/<int:id>") @app.route("/js/wlcw/<int:id>")
@cross_origin() @cross_origin()
def js_widget_loader_custom_widget(id: int): def js_widget_loader_custom_widget(id: int):

View File

@ -4,6 +4,7 @@ from flask_security import auth_required
from project import app from project import app
from project.access import can_use_planning from project.access import can_use_planning
from project.forms.planning import PlanningForm from project.forms.planning import PlanningForm
from project.services.admin import upsert_settings
from project.services.search_params import EventSearchParams from project.services.search_params import EventSearchParams
from project.views.event import get_event_category_choices from project.views.event import get_event_category_choices
from project.views.utils import permission_missing from project.views.utils import permission_missing
@ -24,4 +25,16 @@ def planning():
form.weekday.data = [c[0] for c in form.weekday.choices] form.weekday.data = [c[0] for c in form.weekday.choices]
form.exclude_recurring.data = True form.exclude_recurring.data = True
return render_template("planning/list.html", form=form, params=params) settings = upsert_settings()
initial_external_calendars = (
settings.planning_external_calendars
if settings.planning_external_calendars
else "[]"
)
return render_template(
"planning/list.html",
form=form,
params=params,
initial_external_calendars=initial_external_calendars,
)

View File

@ -60,7 +60,7 @@ GeoAlchemy2==0.13.1
googlemaps==4.10.0 googlemaps==4.10.0
greenlet==2.0.2 greenlet==2.0.2
gunicorn==20.1.0 gunicorn==20.1.0
icalendar==5.0.5 icalendar==5.0.11
identify==1.5.10 identify==1.5.10
idna==2.10 idna==2.10
importlib-metadata==6.3.0 importlib-metadata==6.3.0
@ -109,9 +109,10 @@ python-dateutil==2.8.1
python-dotenv==0.15.0 python-dotenv==0.15.0
python-editor==1.0.4 python-editor==1.0.4
pytoolconfig==1.2.5 pytoolconfig==1.2.5
pytz==2022.7.1 pytz==2023.3
PyYAML==6.0.1 PyYAML==6.0.1
qrcode==6.1 qrcode==6.1
recurring-ical-events==2.1.0
redis==4.5.4 redis==4.5.4
regex==2023.3.23 regex==2023.3.23
requests==2.31.0 requests==2.31.0

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,105 @@
BEGIN:VCALENDAR
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
VERSION:2.0
BEGIN:VTIMEZONE
TZID:Europe/Berlin
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
TZNAME:CEST
DTSTART:19700329T020000
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
TZNAME:CET
DTSTART:19701025T030000
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
CREATED:20190303T154052Z
LAST-MODIFIED:20190303T154956Z
DTSTAMP:20190303T154956Z
UID:5d4c6843-9300-4f91-8d88-6094d4b0b840
SUMMARY:test7
RRULE:FREQ=DAILY;UNTIL=20190320T030000Z
DTSTART;TZID=Europe/Berlin:20190318T040000
DTEND;TZID=Europe/Berlin:20190318T050000
TRANSP:OPAQUE
X-MOZ-GENERATION:4
SEQUENCE:1
DESCRIPTION:description should be the same
END:VEVENT
BEGIN:VEVENT
CREATED:20190303T154131Z
LAST-MODIFIED:20190303T154145Z
DTSTAMP:20190303T154145Z
UID:5d4c6843-9300-4f91-8d88-6094d4b0b840
SUMMARY:test7 - edited
RECURRENCE-ID;TZID=Europe/Berlin:20190319T040000
DTSTART;TZID=Europe/Berlin:20190319T040000
DTEND;TZID=Europe/Berlin:20190319T050000
TRANSP:OPAQUE
X-MOZ-GENERATION:3
SEQUENCE:2
LOCATION:location
X-LIC-ERROR:No value for CLASS property. Removing entire property:
END:VEVENT
BEGIN:VEVENT
CREATED:20190307T194152Z
LAST-MODIFIED:20190307T195005Z
DTSTAMP:20190307T195005Z
UID:a0c78729-30b1-4ba3-a86e-6aedd995d788
SUMMARY:New Event
RRULE:FREQ=DAILY;UNTIL=20190310T010000Z
DTSTART;TZID=Europe/Berlin:20190307T020000
DTEND;TZID=Europe/Berlin:20190307T030000
TRANSP:OPAQUE
SEQUENCE:1
X-MOZ-GENERATION:6
END:VEVENT
BEGIN:VEVENT
CREATED:20190307T194207Z
LAST-MODIFIED:20190307T194945Z
DTSTAMP:20190307T194945Z
UID:a0c78729-30b1-4ba3-a86e-6aedd995d788
SUMMARY:New Event
RECURRENCE-ID;TZID=Europe/Berlin:20190308T020000
DTSTART;TZID=Europe/Berlin:20190308T010000
DTEND;TZID=Europe/Berlin:20190308T030000
TRANSP:OPAQUE
SEQUENCE:3
X-MOZ-GENERATION:2
END:VEVENT
BEGIN:VEVENT
CREATED:20190307T194214Z
LAST-MODIFIED:20190307T194952Z
DTSTAMP:20190307T194952Z
UID:a0c78729-30b1-4ba3-a86e-6aedd995d788
SUMMARY:New Event
RECURRENCE-ID;TZID=Europe/Berlin:20190309T020000
DTSTART;TZID=Europe/Berlin:20190309T030000
DTEND;TZID=Europe/Berlin:20190309T033000
TRANSP:OPAQUE
SEQUENCE:3
X-MOZ-GENERATION:3
END:VEVENT
BEGIN:VEVENT
CREATED:20190307T194955Z
LAST-MODIFIED:20190307T195005Z
DTSTAMP:20190307T195005Z
UID:a0c78729-30b1-4ba3-a86e-6aedd995d788
SUMMARY:New Event
RECURRENCE-ID;TZID=Europe/Berlin:20190310T020000
DTSTART;VALUE=DATE:20190310
DTEND;VALUE=DATE:20190311
TRANSP:TRANSPARENT
SEQUENCE:2
X-MOZ-GENERATION:6
X-LIC-ERROR;X-LIC-ERRORTYPE=VALUE-PARSE-ERROR:No value for CLASS property.
Removing entire property:
DURATION:PT0S
END:VEVENT
END:VCALENDAR

View File

@ -309,3 +309,34 @@ def test_admin_reset_tos_accepted(client, app, db, seeder: Seeder, utils: UtilAc
from project.models.user import User from project.models.user import User
assert len(User.query.filter(User.tos_accepted_at.isnot(None)).all()) == 0 assert len(User.query.filter(User.tos_accepted_at.isnot(None)).all()) == 0
@pytest.mark.parametrize("db_error", [True, False])
def test_admin_planning(client, seeder, utils, app, mocker, db_error):
user_id, admin_unit_id = seeder.setup_base(True)
url = utils.get_url("admin_planning")
response = utils.get_ok(url)
if db_error:
utils.mock_db_commit(mocker)
response = utils.post_form(
url,
response,
{
"planning_external_calendars": "[]",
},
)
if db_error:
utils.assert_response_db_error(response)
return
utils.assert_response_redirect(response, "admin")
with app.app_context():
from project.services.admin import upsert_settings
settings = upsert_settings()
assert settings.planning_external_calendars == "[]"

View File

@ -298,3 +298,36 @@ def test_js_widget_loader_custom_widget(client, seeder: Seeder, utils: UtilActio
url = utils.get_url("js_widget_loader_custom_widget", id=custom_widget_id) url = utils.get_url("js_widget_loader_custom_widget", id=custom_widget_id)
utils.get_ok(url) utils.get_ok(url)
def test_js_icalevents(
client, seeder: Seeder, utils: UtilActions, shared_datadir, requests_mock
):
user_id, admin_unit_id = seeder.setup_base()
url = utils.get_url("planning")
utils.get(url)
params = (client, utils, shared_datadir)
_assert_icalevents(params, "feiertage-deutschland.ics")
_assert_icalevents(params, "recurring-events-changed-duration.ics")
def _assert_icalevents(params, filename):
client, utils, datadir = params
utils.mock_get_request_with_file("http://test.de", datadir, filename)
with client:
url = utils.get_url("js_icalevents")
response = utils.post_form_data(
url,
{
"date_from": "2019-03-05",
"date_to": "2019-04-01",
"url": "http://test.de",
},
)
utils.assert_response_ok(response)
json = response.json
first = json["items"][0]
assert first["name"]