mirror of
https://github.com/lucaspalomodevelop/eventcally.git
synced 2026-03-13 00:07:22 +00:00
Merge pull request #562 from eventcally/issues/561
Planning external calendars #561
This commit is contained in:
commit
3b03d362d5
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
@ -1,14 +1,8 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"python.pythonPath": "./env/bin/python3",
|
||||
"python.formatting.provider": "black",
|
||||
"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.unittestEnabled": false,
|
||||
"python.testing.nosetestsEnabled": false,
|
||||
"python.testing.pytestEnabled": true,
|
||||
"[python]": {
|
||||
"editor.codeActionsOnSave": {
|
||||
|
||||
33
migrations/versions/ef547963d7f0_.py
Normal file
33
migrations/versions/ef547963d7f0_.py
Normal 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 ###
|
||||
@ -1,3 +1,5 @@
|
||||
import json
|
||||
|
||||
from flask_babel import lazy_gettext
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import BooleanField, RadioField, StringField, SubmitField, TextAreaField
|
||||
@ -18,6 +20,30 @@ class AdminSettingsForm(FlaskForm):
|
||||
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):
|
||||
reset_for_users = BooleanField(
|
||||
lazy_gettext("Reset for all users"), validators=[DataRequired()]
|
||||
|
||||
@ -14,3 +14,4 @@ class Settings(db.Model, TrackableMixin):
|
||||
privacy = deferred(Column(UnicodeText()))
|
||||
start_page = deferred(Column(UnicodeText()))
|
||||
announcement = deferred(Column(UnicodeText()))
|
||||
planning_external_calendars = deferred(Column(UnicodeText()))
|
||||
|
||||
@ -5,6 +5,7 @@ from sqlalchemy import and_
|
||||
|
||||
from project.models import Event, EventOrganizer, EventPlace
|
||||
from project.services.importer.ld_json_importer import LdJsonImporter
|
||||
from project.utils import decode_response_content
|
||||
|
||||
|
||||
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"
|
||||
|
||||
response = requests.get(sanitized_url, headers=headers)
|
||||
|
||||
try:
|
||||
html = response.content.decode("UTF-8")
|
||||
except Exception: # pragma: no cover
|
||||
html = response.content.decode(response.apparent_encoding)
|
||||
|
||||
html = decode_response_content(response)
|
||||
return self.load_event_from_html(html, absolute_url)
|
||||
|
||||
def load_event_from_html(self, html: str, origin_url: str):
|
||||
|
||||
@ -39,6 +39,64 @@ const PlanningList = {
|
||||
</v-icon>
|
||||
{{ $t("comp.filter") }}
|
||||
</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-btn-toggle
|
||||
@ -74,7 +132,7 @@ const PlanningList = {
|
||||
locale="de"
|
||||
:weekdays="[1, 2, 3, 4, 5, 6, 0]"
|
||||
:type="type"
|
||||
:events="events"
|
||||
:events="allEvents"
|
||||
:event-color="getEventColor"
|
||||
event-more-text="{0} weitere"
|
||||
@click:event="showEvent"
|
||||
@ -92,12 +150,19 @@ const PlanningList = {
|
||||
<v-card-title>{{ selectedEvent.name }}</v-card-title>
|
||||
<v-card-subtitle>
|
||||
<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-text>
|
||||
<div><v-icon small>mdi-database</v-icon> {{ selectedEvent.date.event.organization.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 v-if="selectedEvent.date.event">
|
||||
<div><v-icon small>mdi-database</v-icon> {{ selectedEvent.date.event.organization.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-actions>
|
||||
@ -110,6 +175,7 @@ const PlanningList = {
|
||||
{{ $t("shared.close") }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="selectedEvent.date.event"
|
||||
text
|
||||
color="primary"
|
||||
@click="openEventDate(selectedEvent.date)"
|
||||
@ -137,6 +203,8 @@ const PlanningList = {
|
||||
week: "Week",
|
||||
month: "Month",
|
||||
filter: "Filter",
|
||||
externalCalTitle: "External Calendars",
|
||||
externalCalAddUrl: "Add link to iCal calendar",
|
||||
},
|
||||
},
|
||||
de: {
|
||||
@ -148,6 +216,8 @@ const PlanningList = {
|
||||
week: "Woche",
|
||||
month: "Monat",
|
||||
filter: "Filter",
|
||||
externalCalTitle: "Externe Kalender",
|
||||
externalCalAddUrl: "Link zu iCal-Kalendar hinzufügen",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -164,8 +234,21 @@ const PlanningList = {
|
||||
isLoading: false,
|
||||
maxDates: 200,
|
||||
warning: null,
|
||||
externalMenu: false,
|
||||
externalNewUrl: "",
|
||||
externalEvents: [],
|
||||
externalCals: [],
|
||||
}),
|
||||
computed: {
|
||||
allEvents() {
|
||||
return [...this.events,...this.externalEvents];
|
||||
},
|
||||
externalCalMenuVisible() {
|
||||
return this.externalCals.length > 0;
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.externalCals = this.$root.externalCals;
|
||||
this.$refs.calendar.checkChange();
|
||||
},
|
||||
methods: {
|
||||
@ -177,7 +260,11 @@ const PlanningList = {
|
||||
calendarChanged({ start, end }) {
|
||||
$('#date_from').val(start.date);
|
||||
$('#date_to').val(end.date);
|
||||
this.load();
|
||||
},
|
||||
load() {
|
||||
this.loadEvents();
|
||||
this.loadExternalCalendars();
|
||||
},
|
||||
loadEvents(page = 1) {
|
||||
$('#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 }) {
|
||||
this.focus = date;
|
||||
this.type = 'day';
|
||||
@ -274,5 +394,17 @@ const PlanningList = {
|
||||
handleLoading(isLoading) {
|
||||
this.isLoading = isLoading;
|
||||
},
|
||||
addExternalUrl() {
|
||||
this.externalCals.push({
|
||||
title: this.externalNewUrl,
|
||||
url: this.externalNewUrl,
|
||||
active: true,
|
||||
});
|
||||
this.externalNewUrl = "";
|
||||
},
|
||||
closeExternalMenu() {
|
||||
this.externalMenu = false;
|
||||
this.loadExternalCalendars();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@ -31,6 +31,10 @@
|
||||
{{ _('Newsletter') }}
|
||||
<i class="fa fa-caret-right"></i>
|
||||
</a>
|
||||
<a href="{{ url_for('admin_planning') }}" class="list-group-item">
|
||||
{{ _('Planning') }}
|
||||
<i class="fa fa-caret-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
38
project/templates/admin/planning.html
Normal file
38
project/templates/admin/planning.html
Normal 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 %}
|
||||
@ -108,6 +108,7 @@
|
||||
},
|
||||
cancel: "Cancel",
|
||||
close: "Close",
|
||||
refresh: "Refresh",
|
||||
decline: "Decline",
|
||||
docs: "Docs",
|
||||
save: "Save",
|
||||
@ -230,6 +231,7 @@
|
||||
close: "Schließen",
|
||||
decline: "Ablehnen",
|
||||
docs: "Doku",
|
||||
refresh: "Aktualisieren",
|
||||
save: "Speichern",
|
||||
submit: "Senden",
|
||||
view: "Anzeigen",
|
||||
@ -382,6 +384,9 @@
|
||||
var vue_app_data = {};
|
||||
{% endblock %}
|
||||
|
||||
{% block vue_app_data_fill %}
|
||||
{% endblock %}
|
||||
|
||||
{% if config["DOCS_URL"] %}
|
||||
vue_app_data["docsUrl"] = "{{ config["DOCS_URL"] }}";
|
||||
{% endif %}
|
||||
|
||||
@ -43,6 +43,10 @@ Vue.component("PlanningList", PlanningList);
|
||||
vue_init_data.vuetify = new Vuetify();
|
||||
{% endblock %}
|
||||
|
||||
{% block vue_app_data_fill %}
|
||||
vue_app_data["externalCals"] = {{ initial_external_calendars | safe }};
|
||||
{% endblock %}
|
||||
|
||||
{% block vue_container %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import os
|
||||
import pathlib
|
||||
|
||||
import requests
|
||||
from flask_babel import lazy_gettext
|
||||
from psycopg2.errorcodes import CHECK_VIOLATION, UNIQUE_VIOLATION
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
@ -123,3 +124,10 @@ def get_pending_changes(
|
||||
result[attr.key] = [new_value, old_value]
|
||||
|
||||
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)
|
||||
|
||||
@ -7,6 +7,7 @@ from sqlalchemy.sql import func
|
||||
from project import app, db
|
||||
from project.forms.admin import (
|
||||
AdminNewsletterForm,
|
||||
AdminPlanningForm,
|
||||
AdminSettingsForm,
|
||||
AdminTestEmailForm,
|
||||
DeleteAdminUnitForm,
|
||||
@ -263,3 +264,25 @@ def admin_user_delete(id):
|
||||
flash_errors(form)
|
||||
|
||||
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)
|
||||
|
||||
@ -1,18 +1,26 @@
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
import icalendar
|
||||
import recurring_ical_events
|
||||
import requests
|
||||
from flask import request
|
||||
from flask.json import jsonify
|
||||
from flask_babel import gettext
|
||||
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 sqlalchemy import func
|
||||
|
||||
from project import app, csrf
|
||||
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.models import AdminUnit, CustomWidget, EventOrganizer, EventPlace
|
||||
from project.services.admin import upsert_settings
|
||||
from project.services.place import get_event_places
|
||||
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"])
|
||||
@ -181,6 +189,79 @@ def js_autocomplete_gmaps_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()
|
||||
|
||||
settings = upsert_settings()
|
||||
planning_external_calendars_str = (
|
||||
settings.planning_external_calendars
|
||||
if settings.planning_external_calendars
|
||||
else "[]"
|
||||
)
|
||||
external_calendars = json.loads(planning_external_calendars_str)
|
||||
external_calendar = next((c for c in external_calendars if c["url"] == url))
|
||||
|
||||
response = requests.get(external_calendar["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>")
|
||||
@cross_origin()
|
||||
def js_widget_loader_custom_widget(id: int):
|
||||
|
||||
@ -4,6 +4,7 @@ from flask_security import auth_required
|
||||
from project import app
|
||||
from project.access import can_use_planning
|
||||
from project.forms.planning import PlanningForm
|
||||
from project.services.admin import upsert_settings
|
||||
from project.services.search_params import EventSearchParams
|
||||
from project.views.event import get_event_category_choices
|
||||
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.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,
|
||||
)
|
||||
|
||||
@ -60,7 +60,7 @@ GeoAlchemy2==0.13.1
|
||||
googlemaps==4.10.0
|
||||
greenlet==2.0.2
|
||||
gunicorn==20.1.0
|
||||
icalendar==5.0.5
|
||||
icalendar==5.0.11
|
||||
identify==1.5.10
|
||||
idna==2.10
|
||||
importlib-metadata==6.3.0
|
||||
@ -109,9 +109,10 @@ python-dateutil==2.8.1
|
||||
python-dotenv==0.15.0
|
||||
python-editor==1.0.4
|
||||
pytoolconfig==1.2.5
|
||||
pytz==2022.7.1
|
||||
pytz==2023.3
|
||||
PyYAML==6.0.1
|
||||
qrcode==6.1
|
||||
recurring-ical-events==2.1.0
|
||||
redis==4.5.4
|
||||
regex==2023.3.23
|
||||
requests==2.31.0
|
||||
|
||||
2464
tests/views/data/feiertage-deutschland.ics
Normal file
2464
tests/views/data/feiertage-deutschland.ics
Normal file
File diff suppressed because it is too large
Load Diff
105
tests/views/data/recurring-events-changed-duration.ics
Normal file
105
tests/views/data/recurring-events-changed-duration.ics
Normal 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
|
||||
@ -309,3 +309,34 @@ def test_admin_reset_tos_accepted(client, app, db, seeder: Seeder, utils: UtilAc
|
||||
from project.models.user import User
|
||||
|
||||
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 == "[]"
|
||||
|
||||
@ -298,3 +298,51 @@ 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)
|
||||
utils.get_ok(url)
|
||||
|
||||
|
||||
def test_js_icalevents(
|
||||
client, app, db, seeder: Seeder, utils: UtilActions, shared_datadir, requests_mock
|
||||
):
|
||||
user_id, admin_unit_id = seeder.setup_base()
|
||||
url = utils.get_url("planning")
|
||||
utils.get(url)
|
||||
|
||||
with app.app_context():
|
||||
import json
|
||||
|
||||
from project.services.admin import upsert_settings
|
||||
|
||||
settings = upsert_settings()
|
||||
settings.planning_external_calendars = json.dumps(
|
||||
[
|
||||
{
|
||||
"url": "http://test.de",
|
||||
}
|
||||
]
|
||||
)
|
||||
db.session.commit()
|
||||
|
||||
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"]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user