mirror of
https://github.com/lucaspalomodevelop/eventcally.git
synced 2026-03-13 00:07:22 +00:00
Add sharing options #184
This commit is contained in:
parent
739ca190e2
commit
4419ea1b93
@ -1,10 +1,12 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
import icalendar
|
||||||
import pytz
|
import pytz
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
from dateutil.rrule import rrulestr
|
from dateutil.rrule import rrulestr
|
||||||
|
|
||||||
berlin_tz = pytz.timezone("Europe/Berlin")
|
berlin_tz = pytz.timezone("Europe/Berlin")
|
||||||
|
gmt_tz = pytz.timezone("GMT")
|
||||||
|
|
||||||
|
|
||||||
def get_now():
|
def get_now():
|
||||||
@ -195,3 +197,34 @@ def calculate_occurrences(start_date, date_format, rrule_str, start, batch_size)
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {"occurrences": occurrences, "batch": batch_data}
|
return {"occurrences": occurrences, "batch": batch_data}
|
||||||
|
|
||||||
|
|
||||||
|
def create_icalendar() -> icalendar.Calendar:
|
||||||
|
cal = icalendar.Calendar()
|
||||||
|
cal.add("prodid", "-//Oveda//oveda.de//")
|
||||||
|
cal.add("version", "2.0")
|
||||||
|
cal.add("x-wr-timezone", berlin_tz.zone)
|
||||||
|
|
||||||
|
tzc = icalendar.Timezone()
|
||||||
|
tzc.add("tzid", berlin_tz.zone)
|
||||||
|
tzc.add("x-lic-location", berlin_tz.zone)
|
||||||
|
|
||||||
|
tzs = icalendar.TimezoneStandard()
|
||||||
|
tzs.add("tzname", "CET")
|
||||||
|
tzs.add("dtstart", datetime(1970, 10, 25, 3, 0, 0))
|
||||||
|
tzs.add("rrule", {"freq": "yearly", "bymonth": 10, "byday": "-1su"})
|
||||||
|
tzs.add("TZOFFSETFROM", timedelta(hours=2))
|
||||||
|
tzs.add("TZOFFSETTO", timedelta(hours=1))
|
||||||
|
|
||||||
|
tzd = icalendar.TimezoneDaylight()
|
||||||
|
tzd.add("tzname", "CEST")
|
||||||
|
tzd.add("dtstart", datetime(1970, 3, 29, 2, 0, 0))
|
||||||
|
tzd.add("rrule", {"freq": "yearly", "bymonth": 3, "byday": "-1su"})
|
||||||
|
tzd.add("TZOFFSETFROM", timedelta(hours=1))
|
||||||
|
tzd.add("TZOFFSETTO", timedelta(hours=2))
|
||||||
|
|
||||||
|
tzc.add_component(tzs)
|
||||||
|
tzc.add_component(tzd)
|
||||||
|
cal.add_component(tzc)
|
||||||
|
|
||||||
|
return cal
|
||||||
|
|||||||
@ -6,6 +6,8 @@ from project.utils import (
|
|||||||
get_event_category_name,
|
get_event_category_name,
|
||||||
get_localized_enum_name,
|
get_localized_enum_name,
|
||||||
get_localized_scope,
|
get_localized_scope,
|
||||||
|
get_location_str,
|
||||||
|
get_place_str,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -36,6 +38,8 @@ app.jinja_env.filters["quote_plus"] = lambda u: quote_plus(u)
|
|||||||
app.jinja_env.filters["is_list"] = is_list
|
app.jinja_env.filters["is_list"] = is_list
|
||||||
app.jinja_env.filters["any_dict_value_true"] = any_dict_value_true
|
app.jinja_env.filters["any_dict_value_true"] = any_dict_value_true
|
||||||
app.jinja_env.filters["ensure_link_scheme"] = lambda s: ensure_link_scheme(s)
|
app.jinja_env.filters["ensure_link_scheme"] = lambda s: ensure_link_scheme(s)
|
||||||
|
app.jinja_env.filters["place_str"] = lambda p: get_place_str(p)
|
||||||
|
app.jinja_env.filters["location_str"] = lambda l: get_location_str(l)
|
||||||
|
|
||||||
|
|
||||||
@app.context_processor
|
@app.context_processor
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
import icalendar
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
from flask import url_for
|
from flask import url_for
|
||||||
from flask_babelex import format_date, format_time
|
from flask_babelex import format_date, format_time
|
||||||
@ -8,7 +9,12 @@ from sqlalchemy.orm import contains_eager, defaultload, joinedload, lazyload
|
|||||||
from sqlalchemy.sql import extract
|
from sqlalchemy.sql import extract
|
||||||
|
|
||||||
from project import db
|
from project import db
|
||||||
from project.dateutils import date_add_time, dates_from_recurrence_rule, get_today
|
from project.dateutils import (
|
||||||
|
berlin_tz,
|
||||||
|
date_add_time,
|
||||||
|
dates_from_recurrence_rule,
|
||||||
|
get_today,
|
||||||
|
)
|
||||||
from project.models import (
|
from project.models import (
|
||||||
AdminUnit,
|
AdminUnit,
|
||||||
Event,
|
Event,
|
||||||
@ -22,7 +28,7 @@ from project.models import (
|
|||||||
Image,
|
Image,
|
||||||
Location,
|
Location,
|
||||||
)
|
)
|
||||||
from project.utils import get_pending_changes
|
from project.utils import get_pending_changes, get_place_str
|
||||||
from project.views.utils import truncate
|
from project.views.utils import truncate
|
||||||
|
|
||||||
|
|
||||||
@ -350,3 +356,31 @@ def get_meta_data(event: Event, event_date: EventDate = None) -> dict:
|
|||||||
meta["image"] = url_for("image", id=event.photo_id, _external=True)
|
meta["image"] = url_for("image", id=event.photo_id, _external=True)
|
||||||
|
|
||||||
return meta
|
return meta
|
||||||
|
|
||||||
|
|
||||||
|
def create_ical_event_for_date(event_date: EventDate) -> icalendar.Event:
|
||||||
|
url = url_for("event_date", id=event_date.id, _external=True)
|
||||||
|
|
||||||
|
event = icalendar.Event()
|
||||||
|
event.add("summary", event_date.event.name)
|
||||||
|
event.add("url", url)
|
||||||
|
event.add("description", url)
|
||||||
|
event.add("uid", url)
|
||||||
|
event.add("dtstart", event_date.start.astimezone(berlin_tz))
|
||||||
|
|
||||||
|
if event_date.end:
|
||||||
|
event.add("dtend", event_date.end.astimezone(berlin_tz))
|
||||||
|
|
||||||
|
if event_date.event.created_at:
|
||||||
|
event.add("dtstamp", event_date.event.created_at)
|
||||||
|
|
||||||
|
if event_date.event.updated_at:
|
||||||
|
event.add("last-modified", event_date.event.updated_at)
|
||||||
|
|
||||||
|
if (
|
||||||
|
event_date.event.attendance_mode
|
||||||
|
and event_date.event.attendance_mode != EventAttendanceMode.online
|
||||||
|
):
|
||||||
|
event.add("location", get_place_str(event_date.event.event_place))
|
||||||
|
|
||||||
|
return event
|
||||||
|
|||||||
@ -194,6 +194,16 @@ $( function() {
|
|||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#copy_input_button").click(function () {
|
||||||
|
$("#copy_input").select();
|
||||||
|
document.execCommand("copy");
|
||||||
|
$(this).tooltip('show');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#copy_input_button').mouseleave(function () {
|
||||||
|
$(this).tooltip('hide');
|
||||||
|
});
|
||||||
|
|
||||||
$("#geolocation_btn").click(function () {
|
$("#geolocation_btn").click(function () {
|
||||||
if ("geolocation" in navigator){
|
if ("geolocation" in navigator){
|
||||||
navigator.geolocation.getCurrentPosition(function(position){
|
navigator.geolocation.getCurrentPosition(function(position){
|
||||||
|
|||||||
@ -89,21 +89,8 @@
|
|||||||
{{ organizer.name }}
|
{{ organizer.name }}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro render_location(location) %}
|
{% macro render_location(location) %}{{ location | location_str }}{% endmacro %}
|
||||||
{%- if location.street -%}
|
{% macro render_place(place) %}{{ place | place_str }}{% endmacro %}
|
||||||
{{ location.street }}, {{ location.postalCode }} {{ location.city }}
|
|
||||||
{%- elif location.postalCode or location.city -%}
|
|
||||||
{{ location.postalCode }} {{ location.city }}
|
|
||||||
{%- endif -%}
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro render_place(place) %}
|
|
||||||
{%- if place.location -%}
|
|
||||||
{{ place.name }}, {{render_location(place.location)}}
|
|
||||||
{%- else -%}
|
|
||||||
{{ place.name }}
|
|
||||||
{%- endif -%}
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro render_events_sub_menu() %}
|
{% macro render_events_sub_menu() %}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
@ -511,7 +498,7 @@
|
|||||||
|
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro render_event_props_seo(event, start, end, dates = None, show_rating = False, show_admin_unit = True, user_rights=None) %}
|
{% macro render_event_props_seo(event, start, end, dates = None, show_rating = False, show_admin_unit = True, user_rights=None, share_links=None, calendar_links=None) %}
|
||||||
<div class="w-normal mx-auto">
|
<div class="w-normal mx-auto">
|
||||||
|
|
||||||
{% if event.photo_id %}
|
{% if event.photo_id %}
|
||||||
@ -563,7 +550,20 @@
|
|||||||
<div class="mt-4" style="white-space:pre-wrap;">{{ event.description|urlize(nofollow=True, target='_blank', rel="nofollow") }}</div>
|
<div class="mt-4" style="white-space:pre-wrap;">{{ event.description|urlize(nofollow=True, target='_blank', rel="nofollow") }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="small mt-4">
|
{% if share_links or calendar_links %}
|
||||||
|
<div class="mt-4">
|
||||||
|
{% if share_links %}
|
||||||
|
<button type="button" class="btn btn-outline-secondary mr-1 mb-1" data-toggle="modal" data-target="#shareModal"><i class="fa fa-share-alt"></i> {{ _('Share') }}</button>
|
||||||
|
{{ render_share_modal(share_links) }}
|
||||||
|
{% endif %}
|
||||||
|
{% if calendar_links %}
|
||||||
|
<button type="button" class="btn btn-outline-secondary mb-1" data-toggle="modal" data-target="#calendarExportModal"><i class="fa fa-calendar"></i> {{ _('Add to calendar') }}</button>
|
||||||
|
{{ render_calendar_export_modal(calendar_links) }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="small mt-2">
|
||||||
{{ render_audit(event, show_rating) }}
|
{{ render_audit(event, show_rating) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -588,7 +588,7 @@
|
|||||||
|
|
||||||
{% if event.attendance_mode and event.attendance_mode.value != 2 %}
|
{% if event.attendance_mode and event.attendance_mode.value != 2 %}
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<a href="http://www.google.com/maps?q={{ render_place(event.event_place) | quote_plus }}" class="btn btn-secondary" target="_blank" rel="noopener noreferrer">{{ _('Show directions') }}</a>
|
<a href="http://www.google.com/maps?q={{ render_place(event.event_place) | quote_plus }}" class="btn btn-outline-secondary" target="_blank" rel="noopener noreferrer"><i class="fa fa-directions"></i> {{ _('Show directions') }}</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@ -1248,4 +1248,58 @@ if (URL) {
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro render_share_modal(share_links) %}
|
||||||
|
<div class="modal fade" id="shareModal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">{{ _('Share event') }}</h5>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input id="copy_input" type="text" class="form-control" value="{{ share_links["url"] }}" />
|
||||||
|
<div class="input-group-append">
|
||||||
|
<button id="copy_input_button" class="btn btn-outline-secondary" type="button" data-toggle="tooltip" data-trigger="manual" data-title="{{ _('Link copied') }}">{{ _('Copy link') }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="list-group">
|
||||||
|
<a class="list-group-item list-group-item-action" href="{{ share_links["facebook"] }}" target="_blank" rel="noopener noreferrer"><i class="fab fa-facebook"></i> Facebook</a>
|
||||||
|
<a class="list-group-item list-group-item-action" href="{{ share_links["twitter"] }}" target="_blank" rel="noopener noreferrer"><i class="fab fa-twitter"></i> Twitter</a>
|
||||||
|
<a class="list-group-item list-group-item-action" href="{{ share_links["email"] }}" target="_blank" rel="noopener noreferrer"><i class="fa fa-envelope"></i> {{ _('Email') }}</a>
|
||||||
|
<a class="list-group-item list-group-item-action" href="{{ share_links["whatsapp"] }}" target="_blank" rel="noopener noreferrer" data-action="share/whatsapp/share"><i class="fab fa-whatsapp"></i> Whatsapp</a>
|
||||||
|
<a class="list-group-item list-group-item-action" href="{{ share_links["telegram"] }}" target="_blank" rel="noopener noreferrer"><i class="fab fa-telegram"></i> Telegram</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro render_calendar_export_modal(calendar_links) %}
|
||||||
|
<div class="modal fade" id="calendarExportModal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">{{ _('Add to calendar') }}</h5>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="list-group">
|
||||||
|
<a class="list-group-item list-group-item-action" href="{{ calendar_links["ics"] }}"><i class="fab fa-microsoft"></i> Outlook</a>
|
||||||
|
<a class="list-group-item list-group-item-action" href="{{ calendar_links["google"] }}" target="_blank" rel="noopener noreferrer"><i class="fab fa-google"></i> {{ _('Google calendar') }}</a>
|
||||||
|
<a class="list-group-item list-group-item-action" href="{{ calendar_links["ics"] }}"><i class="fab fa-apple"></i> {{ _('Apple calendar') }}</a>
|
||||||
|
<a class="list-group-item list-group-item-action" href="{{ calendar_links["ics"] }}"><i class="fab fa-yahoo"></i> {{ _('Yahoo calendar') }}</a>
|
||||||
|
<a class="list-group-item list-group-item-action" href="{{ calendar_links["ics"] }}"><i class="fa fa-calendar"></i> {{ _('Other calendar') }}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{% extends "layout.html" %}
|
{% extends "layout.html" %}
|
||||||
{% from "_macros.html" import render_event_props %}
|
{% from "_macros.html" import render_share_modal %}
|
||||||
{%- block title -%}
|
{%- block title -%}
|
||||||
{{ _('Actions for event') }}
|
{{ _('Actions for event') }}
|
||||||
{%- endblock -%}
|
{%- endblock -%}
|
||||||
@ -28,6 +28,12 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="list-group mt-4">
|
||||||
|
<button type="button" class="list-group-item list-group-item-action" data-toggle="modal" data-target="#shareModal"><i class="fa fa-share-alt"></i> {{ _('Share event') }}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ render_share_modal(share_links) }}
|
||||||
|
|
||||||
<div class="list-group mt-4">
|
<div class="list-group mt-4">
|
||||||
{% if user_rights['can_duplicate_event'] %}
|
{% if user_rights['can_duplicate_event'] %}
|
||||||
<a class="list-group-item list-group-item-action" href="{{ url_for('event_create_for_admin_unit_id', id=event.admin_unit_id, template_id=event.id) }}"><i class="fa fa-copy"></i> {{ _('Duplicate event') }}</a>
|
<a class="list-group-item list-group-item-action" href="{{ url_for('event_create_for_admin_unit_id', id=event.admin_unit_id, template_id=event.id) }}"><i class="fa fa-copy"></i> {{ _('Duplicate event') }}</a>
|
||||||
@ -42,7 +48,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -6,6 +6,6 @@
|
|||||||
{% block content_container_attribs %}{% endblock %}
|
{% block content_container_attribs %}{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
{{ render_event_props_seo(event, event.start, event.end, dates, user_rights['can_update_event'], user_rights=user_rights) }}
|
{{ render_event_props_seo(event, event.start, event.end, dates, user_rights['can_update_event'], user_rights=user_rights, share_links=share_links) }}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -7,6 +7,6 @@
|
|||||||
{% block content_container_attribs %}{% endblock %}
|
{% block content_container_attribs %}{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
{{ render_event_props_seo(event, event_date.start, event_date.end, dates, user_rights=user_rights) }}
|
{{ render_event_props_seo(event, event_date.start, event_date.end, dates, user_rights=user_rights, share_links=share_links, calendar_links=calendar_links) }}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -20,6 +20,29 @@ def get_localized_scope(scope: str) -> str:
|
|||||||
return lazy_gettext(loc_key)
|
return lazy_gettext(loc_key)
|
||||||
|
|
||||||
|
|
||||||
|
def get_location_str(location) -> str:
|
||||||
|
if not location:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
if location.street and not location.street.isspace():
|
||||||
|
return f"{location.street}, {location.postalCode} {location.city}"
|
||||||
|
|
||||||
|
if location.postalCode or location.city:
|
||||||
|
return f"{location.postalCode} {location.city}".strip()
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def get_place_str(place) -> str:
|
||||||
|
if not place:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
if place.location:
|
||||||
|
return f"{place.name}, {get_location_str(place.location)}"
|
||||||
|
|
||||||
|
return place.name
|
||||||
|
|
||||||
|
|
||||||
def make_dir(path):
|
def make_dir(path):
|
||||||
try:
|
try:
|
||||||
original_umask = os.umask(0)
|
original_umask = os.umask(0)
|
||||||
|
|||||||
@ -43,6 +43,7 @@ from project.views.event_suggestion import send_event_suggestion_review_status_m
|
|||||||
from project.views.utils import (
|
from project.views.utils import (
|
||||||
flash_errors,
|
flash_errors,
|
||||||
flash_message,
|
flash_message,
|
||||||
|
get_share_links,
|
||||||
handleSqlError,
|
handleSqlError,
|
||||||
non_match_for_deletion,
|
non_match_for_deletion,
|
||||||
send_mail,
|
send_mail,
|
||||||
@ -54,6 +55,8 @@ def event(event_id):
|
|||||||
event = get_event_with_details_or_404(event_id)
|
event = get_event_with_details_or_404(event_id)
|
||||||
user_rights = get_menu_user_rights(event)
|
user_rights = get_menu_user_rights(event)
|
||||||
dates = get_upcoming_event_dates(event.id)
|
dates = get_upcoming_event_dates(event.id)
|
||||||
|
url = url_for("event", event_id=event_id, _external=True)
|
||||||
|
share_links = get_share_links(url, event.name)
|
||||||
|
|
||||||
structured_datas = list()
|
structured_datas = list()
|
||||||
for event_date in dates:
|
for event_date in dates:
|
||||||
@ -70,6 +73,7 @@ def event(event_id):
|
|||||||
meta=get_meta_data(event),
|
meta=get_meta_data(event),
|
||||||
user_rights=user_rights,
|
user_rights=user_rights,
|
||||||
canonical_url=url_for("event", event_id=event_id, _external=True),
|
canonical_url=url_for("event", event_id=event_id, _external=True),
|
||||||
|
share_links=share_links,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -77,8 +81,15 @@ def event(event_id):
|
|||||||
def event_actions(event_id):
|
def event_actions(event_id):
|
||||||
event = Event.query.get_or_404(event_id)
|
event = Event.query.get_or_404(event_id)
|
||||||
user_rights = get_user_rights(event)
|
user_rights = get_user_rights(event)
|
||||||
|
url = url_for("event", event_id=event_id, _external=True)
|
||||||
|
share_links = get_share_links(url, event.name)
|
||||||
|
|
||||||
return render_template("event/actions.html", event=event, user_rights=user_rights)
|
return render_template(
|
||||||
|
"event/actions.html",
|
||||||
|
event=event,
|
||||||
|
user_rights=user_rights,
|
||||||
|
share_links=share_links,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/admin_unit/<int:id>/events/create", methods=("GET", "POST"))
|
@app.route("/admin_unit/<int:id>/events/create", methods=("GET", "POST"))
|
||||||
|
|||||||
@ -1,18 +1,26 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from flask import redirect, render_template, request, url_for
|
from flask import redirect, render_template, request, url_for
|
||||||
|
from flask.wrappers import Response
|
||||||
|
|
||||||
from project import app
|
from project import app
|
||||||
|
from project.dateutils import create_icalendar
|
||||||
from project.forms.event_date import FindEventDateForm
|
from project.forms.event_date import FindEventDateForm
|
||||||
from project.jsonld import DateTimeEncoder, get_sd_for_event_date
|
from project.jsonld import DateTimeEncoder, get_sd_for_event_date
|
||||||
from project.services.event import (
|
from project.services.event import (
|
||||||
|
create_ical_event_for_date,
|
||||||
get_event_date_with_details_or_404,
|
get_event_date_with_details_or_404,
|
||||||
get_meta_data,
|
get_meta_data,
|
||||||
get_upcoming_event_dates,
|
get_upcoming_event_dates,
|
||||||
)
|
)
|
||||||
from project.services.event_search import EventSearchParams
|
from project.services.event_search import EventSearchParams
|
||||||
from project.views.event import get_event_category_choices, get_menu_user_rights
|
from project.views.event import get_event_category_choices, get_menu_user_rights
|
||||||
from project.views.utils import flash_errors, track_analytics
|
from project.views.utils import (
|
||||||
|
flash_errors,
|
||||||
|
get_calendar_links,
|
||||||
|
get_share_links,
|
||||||
|
track_analytics,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def prepare_event_date_form(form):
|
def prepare_event_date_form(form):
|
||||||
@ -53,6 +61,10 @@ def event_date(id):
|
|||||||
get_sd_for_event_date(event_date), indent=2, cls=DateTimeEncoder
|
get_sd_for_event_date(event_date), indent=2, cls=DateTimeEncoder
|
||||||
)
|
)
|
||||||
|
|
||||||
|
url = url_for("event_date", id=id, _external=True)
|
||||||
|
share_links = get_share_links(url, event_date.event.name)
|
||||||
|
calendar_links = get_calendar_links(event_date)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"event_date/read.html",
|
"event_date/read.html",
|
||||||
event_date=event_date,
|
event_date=event_date,
|
||||||
@ -61,4 +73,21 @@ def event_date(id):
|
|||||||
canonical_url=url_for("event_date", id=id, _external=True),
|
canonical_url=url_for("event_date", id=id, _external=True),
|
||||||
user_rights=get_menu_user_rights(event_date.event),
|
user_rights=get_menu_user_rights(event_date.event),
|
||||||
dates=get_upcoming_event_dates(event_date.event_id),
|
dates=get_upcoming_event_dates(event_date.event_id),
|
||||||
|
share_links=share_links,
|
||||||
|
calendar_links=calendar_links,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/eventdate/<int:id>/ical")
|
||||||
|
def event_date_ical(id):
|
||||||
|
event_date = get_event_date_with_details_or_404(id)
|
||||||
|
event = create_ical_event_for_date(event_date)
|
||||||
|
|
||||||
|
cal = create_icalendar()
|
||||||
|
cal.add_component(event)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
cal.to_ical(),
|
||||||
|
mimetype="text/calendar",
|
||||||
|
headers={"Content-disposition": f"attachment; filename=eventdate_{id}.ics"},
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
from urllib.parse import quote_plus
|
||||||
|
|
||||||
from flask import Markup, flash, redirect, render_template, request, url_for
|
from flask import Markup, flash, redirect, render_template, request, url_for
|
||||||
from flask_babelex import gettext
|
from flask_babelex import gettext
|
||||||
from flask_mail import Message
|
from flask_mail import Message
|
||||||
@ -5,7 +7,9 @@ from psycopg2.errorcodes import UNIQUE_VIOLATION
|
|||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
|
||||||
from project import app, db, mail
|
from project import app, db, mail
|
||||||
from project.models import Analytics
|
from project.dateutils import gmt_tz
|
||||||
|
from project.models import Analytics, EventAttendanceMode, EventDate
|
||||||
|
from project.utils import get_place_str
|
||||||
|
|
||||||
|
|
||||||
def track_analytics(key, value1, value2):
|
def track_analytics(key, value1, value2):
|
||||||
@ -112,3 +116,52 @@ def truncate(data: str, length: int) -> str:
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
return (data[: length - 2] + "..") if len(data) > length else data
|
return (data[: length - 2] + "..") if len(data) > length else data
|
||||||
|
|
||||||
|
|
||||||
|
def get_share_links(url: str, title: str) -> dict:
|
||||||
|
share_links = dict()
|
||||||
|
encoded_url = quote_plus(url)
|
||||||
|
encoded_title = quote_plus(title)
|
||||||
|
|
||||||
|
share_links[
|
||||||
|
"facebook"
|
||||||
|
] = f"https://www.facebook.com/sharer/sharer.php?u={encoded_url}"
|
||||||
|
share_links[
|
||||||
|
"twitter"
|
||||||
|
] = f"https://twitter.com/intent/tweet?url={encoded_url}&text={encoded_title}"
|
||||||
|
share_links["email"] = f"mailto:?subject={encoded_title}&body={encoded_url}"
|
||||||
|
share_links["whatsapp"] = f"whatsapp://send?text={encoded_url}"
|
||||||
|
share_links["telegram"] = f"https://t.me/share/url?url={encoded_url}"
|
||||||
|
share_links["url"] = url
|
||||||
|
|
||||||
|
return share_links
|
||||||
|
|
||||||
|
|
||||||
|
def get_calendar_links(event_date: EventDate) -> dict:
|
||||||
|
calendar_links = dict()
|
||||||
|
|
||||||
|
url = url_for("event_date", id=event_date.id, _external=True)
|
||||||
|
encoded_url = quote_plus(url)
|
||||||
|
encoded_title = quote_plus(event_date.event.name)
|
||||||
|
start = event_date.start.astimezone(gmt_tz).strftime("%Y%m%dT%H%M%SZ")
|
||||||
|
if event_date.end:
|
||||||
|
end = event_date.end.astimezone(gmt_tz).strftime("%Y%m%dT%H%M%SZ")
|
||||||
|
else:
|
||||||
|
end = start
|
||||||
|
|
||||||
|
if (
|
||||||
|
event_date.event.attendance_mode
|
||||||
|
and event_date.event.attendance_mode != EventAttendanceMode.online
|
||||||
|
):
|
||||||
|
location = get_place_str(event_date.event.event_place)
|
||||||
|
locationParam = f"&location={quote_plus(location)}"
|
||||||
|
else:
|
||||||
|
locationParam = ""
|
||||||
|
|
||||||
|
calendar_links[
|
||||||
|
"google"
|
||||||
|
] = f"http://www.google.com/calendar/event?action=TEMPLATE&text={encoded_title}&dates={start}/{end}&details={encoded_url}{locationParam}"
|
||||||
|
|
||||||
|
calendar_links["ics"] = url_for("event_date_ical", id=event_date.id, _external=True)
|
||||||
|
|
||||||
|
return calendar_links
|
||||||
|
|||||||
@ -4,6 +4,7 @@ apispec==4.0.0
|
|||||||
apispec-webframeworks==0.5.2
|
apispec-webframeworks==0.5.2
|
||||||
appdirs==1.4.4
|
appdirs==1.4.4
|
||||||
argh==0.26.2
|
argh==0.26.2
|
||||||
|
arrow==0.14.7
|
||||||
attrs==20.3.0
|
attrs==20.3.0
|
||||||
Authlib==0.15.3
|
Authlib==0.15.3
|
||||||
Babel==2.9.0
|
Babel==2.9.0
|
||||||
@ -46,6 +47,7 @@ Flask-SQLAlchemy==2.4.4
|
|||||||
Flask-WTF==0.14.3
|
Flask-WTF==0.14.3
|
||||||
GeoAlchemy2==0.8.4
|
GeoAlchemy2==0.8.4
|
||||||
gunicorn==20.0.4
|
gunicorn==20.0.4
|
||||||
|
icalendar==4.0.7
|
||||||
identify==1.5.10
|
identify==1.5.10
|
||||||
idna==2.10
|
idna==2.10
|
||||||
importlib-metadata==3.1.1
|
importlib-metadata==3.1.1
|
||||||
@ -97,6 +99,7 @@ speaklater==1.3
|
|||||||
SQLAlchemy==1.3.20
|
SQLAlchemy==1.3.20
|
||||||
SQLAlchemy-Utils==0.36.8
|
SQLAlchemy-Utils==0.36.8
|
||||||
swagger-spec-validator==2.7.3
|
swagger-spec-validator==2.7.3
|
||||||
|
TatSu==4.4.0
|
||||||
toml==0.10.2
|
toml==0.10.2
|
||||||
typed-ast==1.4.1
|
typed-ast==1.4.1
|
||||||
typing-extensions==3.7.4.3
|
typing-extensions==3.7.4.3
|
||||||
|
|||||||
@ -176,8 +176,10 @@ class Seeder(object):
|
|||||||
category = get_event_category(category_name)
|
category = get_event_category(category_name)
|
||||||
return category.id
|
return category.id
|
||||||
|
|
||||||
def create_event(self, admin_unit_id, recurrence_rule="", external_link=""):
|
def create_event(
|
||||||
from project.models import Event
|
self, admin_unit_id, recurrence_rule="", external_link="", end=None
|
||||||
|
):
|
||||||
|
from project.models import Event, EventAttendanceMode
|
||||||
from project.services.event import insert_event, upsert_event_category
|
from project.services.event import insert_event, upsert_event_category
|
||||||
|
|
||||||
with self._app.app_context():
|
with self._app.app_context():
|
||||||
@ -187,6 +189,7 @@ class Seeder(object):
|
|||||||
event.name = "Name"
|
event.name = "Name"
|
||||||
event.description = "Beschreibung"
|
event.description = "Beschreibung"
|
||||||
event.start = self.get_now_by_minute()
|
event.start = self.get_now_by_minute()
|
||||||
|
event.end = end
|
||||||
event.event_place_id = self.upsert_default_event_place(admin_unit_id)
|
event.event_place_id = self.upsert_default_event_place(admin_unit_id)
|
||||||
event.organizer_id = self.upsert_default_event_organizer(admin_unit_id)
|
event.organizer_id = self.upsert_default_event_organizer(admin_unit_id)
|
||||||
event.recurrence_rule = recurrence_rule
|
event.recurrence_rule = recurrence_rule
|
||||||
@ -194,6 +197,7 @@ class Seeder(object):
|
|||||||
event.ticket_link = ""
|
event.ticket_link = ""
|
||||||
event.tags = ""
|
event.tags = ""
|
||||||
event.price_info = ""
|
event.price_info = ""
|
||||||
|
event.attendance_mode = EventAttendanceMode.offline
|
||||||
insert_event(event)
|
insert_event(event)
|
||||||
self._db.session.commit()
|
self._db.session.commit()
|
||||||
event_id = event.id
|
event_id = event.id
|
||||||
|
|||||||
73
tests/test_utils.py
Normal file
73
tests/test_utils.py
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
def test_get_location_str_none(client, seeder, app, utils):
|
||||||
|
from project.utils import get_location_str
|
||||||
|
|
||||||
|
location_str = get_location_str(None)
|
||||||
|
assert location_str == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_location_str_empty(client, seeder, app, utils):
|
||||||
|
from project.models import Location
|
||||||
|
from project.utils import get_location_str
|
||||||
|
|
||||||
|
location = Location()
|
||||||
|
|
||||||
|
location_str = get_location_str(location)
|
||||||
|
assert location_str == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_location_str_full(client, seeder, app, utils):
|
||||||
|
from project.models import Location
|
||||||
|
from project.utils import get_location_str
|
||||||
|
|
||||||
|
location = Location()
|
||||||
|
location.street = "Strasse"
|
||||||
|
location.postalCode = "PLZ"
|
||||||
|
location.city = "Ort"
|
||||||
|
|
||||||
|
location_str = get_location_str(location)
|
||||||
|
assert location_str == "Strasse, PLZ Ort"
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_location_str_no_street(client, seeder, app, utils):
|
||||||
|
from project.models import Location
|
||||||
|
from project.utils import get_location_str
|
||||||
|
|
||||||
|
location = Location()
|
||||||
|
location.postalCode = "PLZ"
|
||||||
|
location.city = "Ort"
|
||||||
|
|
||||||
|
location_str = get_location_str(location)
|
||||||
|
assert location_str == "PLZ Ort"
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_place_str_full(client, seeder, app, utils):
|
||||||
|
from project.models import EventPlace, Location
|
||||||
|
from project.utils import get_place_str
|
||||||
|
|
||||||
|
place = EventPlace()
|
||||||
|
place.name = "Name"
|
||||||
|
place.location = Location()
|
||||||
|
place.location.street = "Strasse"
|
||||||
|
place.location.postalCode = "PLZ"
|
||||||
|
place.location.city = "Ort"
|
||||||
|
|
||||||
|
place_str = get_place_str(place)
|
||||||
|
assert place_str == "Name, Strasse, PLZ Ort"
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_place_str_none(client, seeder, app, utils):
|
||||||
|
from project.utils import get_place_str
|
||||||
|
|
||||||
|
place_str = get_place_str(None)
|
||||||
|
assert place_str == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_place_str_no_location(client, seeder, app, utils):
|
||||||
|
from project.models import EventPlace
|
||||||
|
from project.utils import get_place_str
|
||||||
|
|
||||||
|
place = EventPlace()
|
||||||
|
place.name = "Name"
|
||||||
|
|
||||||
|
place_str = get_place_str(place)
|
||||||
|
assert place_str == "Name"
|
||||||
@ -1,6 +1,6 @@
|
|||||||
def test_read(client, seeder, utils):
|
def test_read(client, seeder, utils):
|
||||||
user_id, admin_unit_id = seeder.setup_base()
|
user_id, admin_unit_id = seeder.setup_base()
|
||||||
seeder.create_event(admin_unit_id)
|
seeder.create_event(admin_unit_id, end=seeder.get_now_by_minute())
|
||||||
|
|
||||||
url = utils.get_url("event_date", id=1)
|
url = utils.get_url("event_date", id=1)
|
||||||
utils.get_ok(url)
|
utils.get_ok(url)
|
||||||
@ -10,6 +10,14 @@ def test_read(client, seeder, utils):
|
|||||||
utils.assert_response_redirect(response, "event_date", id=1)
|
utils.assert_response_redirect(response, "event_date", id=1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_ical(client, seeder, utils):
|
||||||
|
user_id, admin_unit_id = seeder.setup_base()
|
||||||
|
seeder.create_event(admin_unit_id, end=seeder.get_now_by_minute())
|
||||||
|
|
||||||
|
url = utils.get_url("event_date_ical", id=1)
|
||||||
|
utils.get_ok(url)
|
||||||
|
|
||||||
|
|
||||||
def test_list(client, seeder, utils):
|
def test_list(client, seeder, utils):
|
||||||
user_id, admin_unit_id = seeder.setup_base()
|
user_id, admin_unit_id = seeder.setup_base()
|
||||||
seeder.create_event(admin_unit_id)
|
seeder.create_event(admin_unit_id)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user