Events via URL importieren #354

This commit is contained in:
Daniel Grams 2022-01-12 16:12:37 +01:00
parent 8a25475fc0
commit f52d919069
37 changed files with 12322 additions and 112 deletions

View File

@ -1,14 +1,14 @@
# Translations template for PROJECT.
# Copyright (C) 2021 ORGANIZATION
# Copyright (C) 2022 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2021.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2022.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2021-12-30 15:28+0100\n"
"POT-Creation-Date: 2022-01-11 23:44+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -189,33 +189,33 @@ msgstr ""
msgid "."
msgstr ""
#: project/api/__init__.py:84
#: project/api/__init__.py:85
msgid "message"
msgstr ""
#: project/api/organization/resources.py:371
#: project/api/organization/resources.py:399
#: project/views/admin_unit_member_invitation.py:85
msgid "You have received an invitation"
msgstr ""
#: project/forms/admin.py:10 project/templates/layout.html:312
#: project/forms/admin.py:10 project/templates/layout.html:313
#: project/views/root.py:37
msgid "Terms of service"
msgstr ""
#: project/forms/admin.py:11 project/templates/layout.html:316
#: project/forms/admin.py:11 project/templates/layout.html:317
#: project/views/root.py:45
msgid "Legal notice"
msgstr ""
#: project/forms/admin.py:12 project/templates/_macros.html:1392
#: project/templates/layout.html:320
#: project/templates/layout.html:321
#: project/templates/widget/event_suggestion/create.html:204
#: project/views/admin_unit.py:73 project/views/root.py:53
msgid "Contact"
msgstr ""
#: project/forms/admin.py:13 project/templates/layout.html:324
#: project/forms/admin.py:13 project/templates/layout.html:325
#: project/views/root.py:61
msgid "Privacy"
msgstr ""
@ -1049,7 +1049,7 @@ msgstr ""
#: project/templates/_macros.html:489 project/templates/_macros.html:649
#: project/templates/admin_unit/create.html:28
#: project/templates/admin_unit/update.html:29
#: project/templates/layout.html:260
#: project/templates/layout.html:261
msgid "Organization"
msgstr ""
@ -1350,6 +1350,7 @@ msgstr ""
#: project/templates/layout.html:175 project/templates/layout.html:219
#: project/templates/manage/events.html:6
#: project/templates/manage/events.html:41
#: project/templates/manage/events_vue.html:4
msgid "Events"
msgstr ""
@ -1367,7 +1368,7 @@ msgstr ""
msgid "Planing"
msgstr ""
#: project/templates/layout.html:187 project/templates/layout.html:274
#: project/templates/layout.html:187 project/templates/layout.html:275
#: project/templates/oauth2_client/list.html:10
#: project/templates/oauth2_client/read.html:10
#: project/templates/oauth2_token/list.html:10 project/templates/profile.html:4
@ -1396,63 +1397,67 @@ msgstr ""
msgid "Create event"
msgstr ""
#: project/templates/layout.html:228
#: project/templates/layout.html:227
msgid "Import event"
msgstr ""
#: project/templates/layout.html:229
#: project/templates/manage/event_lists.html:4
msgid "Event lists"
msgstr ""
#: project/templates/layout.html:231
#: project/templates/layout.html:232
msgid "Review suggestions"
msgstr ""
#: project/templates/layout.html:241
#: project/templates/layout.html:242
#: project/templates/manage/references_incoming.html:5
#: project/templates/manage/references_outgoing.html:5
msgid "References"
msgstr ""
#: project/templates/layout.html:247
#: project/templates/layout.html:248
#: project/templates/manage/references_incoming.html:9
msgid "Incoming references"
msgstr ""
#: project/templates/layout.html:248
#: project/templates/layout.html:249
#: project/templates/manage/references_outgoing.html:9
msgid "Outgoing references"
msgstr ""
#: project/templates/layout.html:250
#: project/templates/layout.html:251
#: project/templates/manage/reference_requests_incoming.html:9
msgid "Incoming reference requests"
msgstr ""
#: project/templates/layout.html:255
#: project/templates/layout.html:256
#: project/templates/manage/reference_requests_outgoing.html:9
msgid "Outgoing reference requests"
msgstr ""
#: project/templates/layout.html:263 project/templates/manage/organizers.html:5
#: project/templates/layout.html:264 project/templates/manage/organizers.html:5
#: project/templates/manage/organizers.html:9
msgid "Organizers"
msgstr ""
#: project/templates/event_place/list.html:3
#: project/templates/event_place/list.html:7 project/templates/layout.html:264
#: project/templates/event_place/list.html:7 project/templates/layout.html:265
#: project/templates/manage/places.html:5
#: project/templates/manage/places.html:9
msgid "Places"
msgstr ""
#: project/templates/layout.html:266 project/templates/manage/members.html:5
#: project/templates/layout.html:267 project/templates/manage/members.html:5
#: project/templates/manage/members.html:28
msgid "Members"
msgstr ""
#: project/templates/layout.html:267 project/templates/manage/relations.html:4
#: project/templates/layout.html:268 project/templates/manage/relations.html:4
msgid "Relations"
msgstr ""
#: project/templates/layout.html:269
#: project/templates/layout.html:270
#: project/templates/manage/admin_units.html:17
#: project/templates/manage/organization_invitations.html:4
#: project/templates/user/organization_invitations.html:4
@ -1464,27 +1469,27 @@ msgstr ""
#: project/templates/admin/settings.html:8
#: project/templates/admin_unit/update.html:6
#: project/templates/admin_unit/update.html:23
#: project/templates/layout.html:271 project/templates/manage/widgets.html:11
#: project/templates/layout.html:272 project/templates/manage/widgets.html:11
#: project/templates/manage/widgets.html:15 project/templates/profile.html:19
msgid "Settings"
msgstr ""
#: project/templates/layout.html:272
#: project/templates/layout.html:273
#: project/templates/manage/custom_widgets.html:13
msgid "Custom widgets"
msgstr ""
#: project/templates/layout.html:273 project/templates/manage/reviews.html:10
#: project/templates/layout.html:274 project/templates/manage/reviews.html:10
#: project/templates/manage/widgets.html:5
#: project/templates/manage/widgets.html:9
msgid "Widgets"
msgstr ""
#: project/templates/layout.html:284
#: project/templates/layout.html:285
msgid "Switch organization"
msgstr ""
#: project/templates/developer/read.html:4 project/templates/layout.html:334
#: project/templates/developer/read.html:4 project/templates/layout.html:335
#: project/templates/profile.html:29
msgid "Developer"
msgstr ""
@ -1957,7 +1962,7 @@ msgstr ""
msgid "Organization successfully updated"
msgstr ""
#: project/views/admin.py:68 project/views/manage.py:346
#: project/views/admin.py:68 project/views/manage.py:360
msgid "Settings successfully updated"
msgstr ""
@ -2015,27 +2020,27 @@ msgstr ""
msgid "Invitation successfully deleted"
msgstr ""
#: project/views/event.py:178
#: project/views/event.py:180
msgid "Event successfully published"
msgstr ""
#: project/views/event.py:180
#: project/views/event.py:182
msgid "Draft successfully saved"
msgstr ""
#: project/views/event.py:223
#: project/views/event.py:225
msgid "Event successfully updated"
msgstr ""
#: project/views/event.py:249
#: project/views/event.py:251
msgid "Event successfully deleted"
msgstr ""
#: project/views/event.py:408
#: project/views/event.py:410
msgid "Referenced event changed"
msgstr ""
#: project/views/event.py:431
#: project/views/event.py:433
msgid "New event report"
msgstr ""

View File

@ -50,17 +50,18 @@ class RestApi(Api):
data["message"] = err.description
code = err.code
if (
isinstance(err, UnprocessableEntity)
and err.exc
and isinstance(err.exc, ValidationError)
):
if isinstance(err, UnprocessableEntity):
data["name"] = err.name
data["message"] = err.description
code = err.code
schema = UnprocessableEntityResponseSchema()
self.fill_validation_data(err.exc, data)
if (
hasattr(err, "exc")
and err.exc
and isinstance(err.exc, ValidationError)
):
self.fill_validation_data(err.exc, data)
else:
schema = ErrorResponseSchema()
elif isinstance(err, ValidationError):

View File

@ -333,3 +333,17 @@ class EventReportPostSchema(marshmallow.Schema):
required=True,
validate=validate.Length(min=20, max=1000),
)
class EventImportRequestSchema(marshmallow.Schema):
url = fields.URL(
required=True,
metadata={
"description": "A link to an external website containing more information about the event."
},
)
public_status = EnumField(
PublicStatus,
missing=PublicStatus.published,
metadata={"description": "Public status of the event."},
)

View File

@ -1,3 +1,4 @@
from flask import abort, request
from flask_apispec import doc, marshal_with, use_kwargs
from flask_babelex import gettext
from sqlalchemy import and_
@ -20,6 +21,7 @@ from project.api.custom_widget.schemas import (
from project.api.event.resources import api_can_read_private_events
from project.api.event.schemas import (
EventIdSchema,
EventImportRequestSchema,
EventListRequestSchema,
EventListResponseSchema,
EventPostRequestSchema,
@ -84,6 +86,7 @@ from project.services.admin_unit import (
)
from project.services.event import get_event_dates_query, get_events_query, insert_event
from project.services.event_search import EventSearchParams
from project.services.importer.event_importer import EventImporter
from project.services.reference import (
get_reference_incoming_query,
get_reference_outgoing_query,
@ -180,6 +183,30 @@ class OrganizationEventListResource(BaseResource):
return event, 201
class OrganizationEventImportResource(BaseResource):
@doc(summary="Import event for organization", tags=["Organizations", "Events"])
@use_kwargs(EventImportRequestSchema, location="json", apply=False)
@marshal_with(EventIdSchema, 201)
@require_api_access("event:write")
def post(self, id, **kwargs):
admin_unit = AdminUnit.query.get_or_404(id)
access_or_401(admin_unit, "event:create")
import_request = EventImportRequestSchema().load(request.json)
try:
importer = EventImporter(admin_unit.id)
event = importer.load_event_from_url(import_request["url"])
except Exception:
abort(422)
event.public_status = import_request["public_status"]
insert_event(event)
db.session.commit()
return event, 201
class OrganizationListResource(BaseResource):
@doc(summary="List organizations", tags=["Organizations"])
@use_kwargs(OrganizationListRequestSchema, location=("query"))
@ -481,6 +508,11 @@ add_api_resource(
"/organizations/<int:id>/events",
"api_v1_organization_event_list",
)
add_api_resource(
OrganizationEventImportResource,
"/organizations/<int:id>/events/import",
"api_v1_organization_event_import",
)
add_api_resource(
OrganizationEventListListResource,
"/organizations/<int:id>/event-lists",

View File

@ -2,6 +2,7 @@ from datetime import datetime, timedelta
import icalendar
import pytz
from dateutil.parser import isoparse
from dateutil.relativedelta import relativedelta
from dateutil.rrule import rrulestr
@ -79,6 +80,10 @@ def form_input_from_date(date):
return date.strftime("%Y-%m-%d") if date else ""
def parse_iso_string(input: str) -> datetime:
return isoparse(input)
def dates_from_recurrence_rule(start, recurrence_rule):
result = list()

View File

@ -1,4 +1,5 @@
import base64
import math
import re
from io import BytesIO
@ -45,6 +46,19 @@ def get_bytes_from_image(image: PIL.Image) -> bytes:
return imgByteArr.getvalue()
def resize_image_to_min(image: PIL.Image) -> PIL.Image:
if image.width >= min_image_size or image.height >= min_image_size:
return image
ratio = max(min_image_size / image.width, min_image_size / image.height)
width = int(math.ceil(image.width * ratio))
height = int(math.ceil(image.height * ratio))
format = image.format
result = image.resize((width, height), PIL.Image.LANCZOS)
result.format = format
return result
def resize_image_to_max(image: PIL.Image):
if image.width > max_image_size or image.height > max_image_size:
image.thumbnail((max_image_size, max_image_size), PIL.Image.ANTIALIAS)

View File

@ -0,0 +1,86 @@
from urllib.parse import urlparse
import requests
from sqlalchemy import and_
from project.models import Event, EventOrganizer, EventPlace
from project.services.importer.ld_json_importer import LdJsonImporter
class EventImporter:
def __init__(self, admin_unit_id: int):
self.admin_unit_id = admin_unit_id
def load_event_from_url(self, absolute_url: str):
sanitized_url = self._sanitize_url(absolute_url)
headers = dict()
if "eventim.de" in absolute_url:
headers[
"User-Agent"
] = "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)
return self.load_event_from_html(html, absolute_url)
def load_event_from_html(self, html: str, origin_url: str):
importer = LdJsonImporter(html, origin_url)
event = importer.load_event()
event.admin_unit_id = self.admin_unit_id
self._match_organizer(event)
self._match_place(event)
return event
def _match_organizer(self, event: Event):
organizer = EventOrganizer.query.filter(
and_(
EventOrganizer.admin_unit_id == self.admin_unit_id,
EventOrganizer.name == event.organizer.name,
)
).first()
if organizer:
event.organizer = organizer
else:
event.organizer.admin_unit_id = self.admin_unit_id
def _match_place(self, event: Event):
place = EventPlace.query.filter(
and_(
EventPlace.admin_unit_id == self.admin_unit_id,
EventPlace.name == event.event_place.name,
)
).first()
if place:
event.event_place = place
else:
event.event_place.admin_unit_id = self.admin_unit_id
def _sanitize_url(self, absolute_url: str) -> str:
result = absolute_url
if "reservix.de" in absolute_url or "facebook.com" in absolute_url:
try:
p = urlparse(absolute_url)
if p.hostname.endswith("reservix.de"):
result = p._replace(
netloc=p.netloc.replace(p.hostname, "www.reservix.de")
).geturl()
if p.hostname == "www.facebook.com":
result = p._replace(
netloc=p.netloc.replace("www.facebook.com", "m.facebook.com")
).geturl()
except Exception: # pragma: no cover
pass
return result

View File

@ -0,0 +1,479 @@
import json
import validators
from bs4 import BeautifulSoup
from project.dateutils import parse_iso_string
from project.imageutils import (
get_bytes_from_image,
get_image_from_url,
get_mime_type_from_image,
resize_image_to_max,
resize_image_to_min,
validate_image,
)
from project.models import (
Event,
EventAttendanceMode,
EventCategory,
EventDateDefinition,
EventOrganizer,
EventPlace,
EventStatus,
Image,
Location,
)
class LdJsonImporter:
def __init__(self, html: str, origin_url: str):
self.html = html
self.origin_url = origin_url
self.event_type_mapping = {
"ChildrensEvent": "Family",
"ComedyEvent": "Comedy",
"DanceEvent": "Dance",
"EducationEvent": "Lecture",
"ExhibitionEvent": "Exhibition",
"Festival": "Festival",
"FoodEvent": "Dining",
"LiteraryEvent": "Book",
"MusicEvent": "Music",
"SportsEvent": "Sports",
"TheaterEvent": "Theater",
}
self.event_status_mapping = {
"EventScheduled": EventStatus.scheduled,
"EventCancelled": EventStatus.cancelled,
"EventMovedOnline": EventStatus.movedOnline,
"EventPostponed": EventStatus.postponed,
"EventRescheduled": EventStatus.rescheduled,
}
self.event_attendance_mode_mapping = {
"OfflineEventAttendanceMode": EventAttendanceMode.offline,
"OnlineEventAttendanceMode": EventAttendanceMode.online,
"MixedEventAttendanceMode": EventAttendanceMode.mixed,
}
def load_event(self):
self.ld_json = self.load_ld_json_from_html()
return self.load_event_from_ld_json()
def load_ld_json_from_html(self):
self.soup = BeautifulSoup(self.html, features="html.parser")
ld_json_scripts = self.soup.find_all("script", {"type": "application/ld+json"})
for ld_json_script in ld_json_scripts:
try:
ld_json = self._load_ld_json_from_script(ld_json_script)
if ld_json:
break
except Exception:
pass
ld_json = self._strip_ld_json(ld_json)
if "description" in ld_json:
desc_soup = BeautifulSoup(ld_json["description"], features="html.parser")
for br in desc_soup.find_all("br"):
br.replace_with("\n" + br.text)
ld_json["description"] = desc_soup.text
if "harzinfo.de" in self.origin_url:
coordinate_div = self.soup.find("div", attrs={"data-position": True})
if coordinate_div:
ld_json["coordinate"] = coordinate_div["data-position"]
return ld_json
def load_event_from_ld_json(self):
item = self.ld_json
event = Event()
self.event = event
event.name = item["name"]
if "url" in item and validators.url(item["url"]):
event.external_link = item["url"]
else:
event.external_link = self.origin_url
self._load_organizer()
self._load_place()
self._load_event_status()
self._load_date_definition()
self._add_categories()
self._add_tags()
self._load_event_photo()
self._load_attendance_mode()
if "description" in item:
event.description = item["description"]
if "isAccessibleForFree" in item:
event.accessible_for_free = item["isAccessibleForFree"]
return event
def _load_date_definition(self):
event = self.event
item = self.ld_json
definition = EventDateDefinition()
definition.start = parse_iso_string(item["startDate"])
if "endDate" in item:
definition.end = parse_iso_string(item["endDate"])
event.date_definitions = [definition]
def _load_organizer(self):
event = self.event
item = self.ld_json
organizer_item = None
if "organizer" in item:
organizer_item = item["organizer"]
if not organizer_item and "author" in item:
organizer_item = item["author"]
if (
organizer_item
and isinstance(organizer_item, list)
and len(organizer_item) > 0
and organizer_item[0]
):
organizer_item = organizer_item[0]
if organizer_item:
event.organizer = self._load_organizer_from_ld_json(organizer_item)
else:
event.organizer = self._load_organizer_from_html()
if not event.organizer:
raise Exception("Organizer is missing")
def _load_organizer_from_ld_json(self, organizer_item: dict) -> EventOrganizer:
organizer = EventOrganizer()
organizer.name = organizer_item["name"]
if "url" in organizer_item and validators.url(organizer_item["url"]):
organizer.url = organizer_item["url"]
if "email" in organizer_item and validators.email(organizer_item["email"]):
organizer.email = organizer_item["email"]
if "telephone" in organizer_item:
organizer.phone = organizer_item["telephone"]
if "faxNumber" in organizer_item:
organizer.fax = organizer_item["faxNumber"]
if "address" in organizer_item:
organizer.location = self._load_location(organizer_item["address"])
return organizer
def _load_organizer_from_html(self) -> EventOrganizer:
if "reservix.de" in self.origin_url:
div = self.soup.find("div", attrs={"class": "c-organizer-info"})
if div:
prefix = "Veranstalter:"
text = div.text.strip()
if text.startswith(prefix):
organizer_text = text[len(prefix) :].strip()
organizer = self._load_organizer_from_text(organizer_text)
if organizer:
return organizer
if "eventim.de" in self.origin_url:
div = self.soup.find(
"div", attrs={"data-qa": "additional-info-promoter-content"}
)
if div:
header_div = div.find(
lambda tag: tag.name == "div" and "Veranstalter:" in tag.text
)
if header_div:
organizer_paragraph = header_div.findNext("p")
if organizer_paragraph:
organizer_text = organizer_paragraph.text.strip()
organizer = self._load_organizer_from_text(organizer_text)
if organizer:
return organizer
if "regiondo.de" in self.origin_url:
span = self.soup.find(
"span", attrs={"itemtype": "http://schema.org/Organization"}
)
if span:
organizer_text = span.text.strip()
organizer = self._load_organizer_from_text(organizer_text)
if organizer:
return organizer
if "facebook.com" in self.origin_url:
anchor = self.soup.find("a", attrs={"class": "cc"})
if anchor:
organizer_text = anchor.text.strip()
organizer = self._load_organizer_from_text(organizer_text)
if organizer:
return organizer
return None
def _load_organizer_from_text(self, organizer_text: str) -> EventOrganizer:
organizer_parts = organizer_text.split(",")
organizer = EventOrganizer()
organizer.name = organizer_parts[0].strip()
if len(organizer_parts) == 4:
location = Location()
location.street = organizer_parts[1].strip()
location.city = organizer_parts[2].strip()
location.country = organizer_parts[3].strip()
if " " in location.city:
city_parts = location.city.split(" ")
location.postalCode = city_parts[0]
location.city = city_parts[1]
organizer.location = location
return organizer
def _load_place(self):
event = self.event
item = self.ld_json
place_item = None
if "location" in item:
place_item = item["location"]
if isinstance(place_item, list) and len(place_item) > 0 and place_item[0]:
place_item = place_item[0]
location_type = place_item["@type"] if "@type" in place_item else None
place = EventPlace()
if "name" not in place_item and location_type == "VirtualLocation":
place.name = "Online"
else:
place.name = place_item["name"]
location = Location()
if "address" in place_item:
location = self._load_location(place_item["address"])
if "geo" in place_item:
geo_item = place_item["geo"]
if (
"@type" in geo_item
and geo_item["@type"] == "GeoCoordinates"
and "latitude" in geo_item
and "longitude" in geo_item
):
latitude = float(geo_item["latitude"])
longitude = float(geo_item["longitude"])
if latitude != 0 and longitude != 0:
location.latitude = latitude
location.longitude = longitude
if "coordinate" in item:
lat_str, lon_str = item["coordinate"].split(",")
latitude = float(lat_str)
longitude = float(lon_str)
if latitude != 0 and longitude != 0:
location.latitude = latitude
location.longitude = longitude
if "url" in place_item and validators.url(place_item["url"]):
place.url = place_item["url"]
place.location = location
event.event_place = place
def _load_location(self, address: dict) -> Location:
location = Location()
if "streetAddress" in address:
location.street = address["streetAddress"]
if "postalCode" in address:
location.postalCode = address["postalCode"]
if "addressLocality" in address:
location.city = address["addressLocality"]
if "addressCountry" in address:
location.country = address["addressCountry"]
return location
def _load_event_photo(self):
event = self.event
item = self.ld_json
if "image" not in item:
return
image = item["image"]
if isinstance(image, list) and len(image) == 0:
return
image_items = image if isinstance(image, list) else [image]
for image_item in image_items:
if isinstance(image_item, str):
image_item = {"url": image_item}
if "url" not in image_item:
continue
try:
pillow_image = get_image_from_url(image_item["url"])
encoding_format = get_mime_type_from_image(pillow_image)
if pillow_image.width > 200 or pillow_image.height > 200:
pillow_image = resize_image_to_min(pillow_image)
validate_image(pillow_image)
resize_image_to_max(pillow_image)
data = get_bytes_from_image(pillow_image)
except Exception:
continue
image = Image()
image.data = data
image.encoding_format = encoding_format
if "contributor" in image_item:
image.copyright_text = image_item["contributor"]
event.photo = image
return
def _load_event_status(self):
event = self.event
item = self.ld_json
if "eventStatus" in item:
eventStatus = self._get_item_enum_value("eventStatus")
if eventStatus in self.event_status_mapping:
event.status = self.event_status_mapping[eventStatus]
return
if eventStatus == "ausgebucht":
event.booked_up = True
event.status = EventStatus.scheduled
def _load_attendance_mode(self):
event = self.event
item = self.ld_json
if "eventAttendanceMode" in item:
eventAttendanceMode = self._get_item_enum_value("eventAttendanceMode")
if eventAttendanceMode in self.event_attendance_mode_mapping:
event.attendance_mode = self.event_attendance_mode_mapping[
eventAttendanceMode
]
def _add_categories(self):
event = self.event
item = self.ld_json
types_value = item["@type"]
types_list = types_value if isinstance(types_value, list) else [types_value]
event_categories = list()
for item_type in types_list:
if item_type in self.event_type_mapping:
category_name = self.event_type_mapping[item_type]
category = EventCategory.query.filter(
EventCategory.name == category_name
).first()
if category:
event_categories.append(category)
if len(event_categories) > 0:
event.categories = event_categories
def _add_tags(self):
event = self.event
item = self.ld_json
if "keywords" not in item:
return
tags = list()
for keyword in item["keywords"].split(","):
keyword = keyword.strip()
if not keyword[0].islower():
tags.append(keyword)
if len(tags) > 0:
event.tags = ",".join(tags)
def _load_ld_json_from_script(self, ld_json_script):
ld_json_object = json.loads(ld_json_script.string)
ld_json = (
ld_json_object[0] if isinstance(ld_json_object, list) else ld_json_object
)
types_value = ld_json["@type"]
types_list = types_value if isinstance(types_value, list) else [types_value]
for type in types_list:
if type.endswith("Event"):
return ld_json
return None
def _strip_ld_json(self, value: any) -> any:
if isinstance(value, str):
return value.strip()
if isinstance(value, dict):
result = dict()
for k, v in value.items():
result[k] = self._strip_ld_json(v)
return result
if isinstance(value, list):
result = list()
for elem in value:
result.append(self._strip_ld_json(elem))
return result
return value
def _get_item_enum_value(self, key: str) -> str:
return (
self.ld_json[key]
.replace("http://schema.org/", "")
.replace("https://schema.org/", "")
)

View File

@ -0,0 +1,96 @@
const EventImport = {
template: `
<div>
<h1>{{ $t("comp.title") }}</h1>
<ValidationObserver v-slot="{ handleSubmit }">
<b-form @submit.stop.prevent="handleSubmit(submitForm)">
<validated-input
:label="$t('comp.url')"
name="url"
v-model="form.url"
rules="required|url" />
<b-form-group v-slot="{ ariaDescribedby }">
<b-form-radio v-model="form.publicStatus" :aria-describedby="ariaDescribedby" value="draft">{{ $t("comp.draft") }}</b-form-radio>
<b-form-radio v-model="form.publicStatus" :aria-describedby="ariaDescribedby" value="published">{{ $t("comp.published") }}</b-form-radio>
</b-form-group>
<b-button variant="primary" type="submit" v-bind:disabled="isSubmitting">
<b-spinner small v-if="isSubmitting"></b-spinner>
{{ $t("shared.submit") }}
</b-button>
</b-form>
</ValidationObserver>
</div>
`,
i18n: {
messages: {
en: {
comp: {
title: "Import event",
url: "URL",
published: "Publish event",
draft: "Save as draft",
importError: "Unfortunately, no event could be imported from the URL.",
},
},
de: {
comp: {
title: "Veranstaltung importieren",
url: "URL",
published: "Veranstaltung veröffentlichen",
draft: "Als Entwurf speichern",
importError: "Von der URL konnte leider keine Veranstaltung importiert werden."
},
},
},
},
data: () => ({
isSubmitting: false,
form: {
name: null,
publicStatus: "draft",
},
}),
computed: {
adminUnitId() {
return this.$route.params.admin_unit_id
},
},
mounted() {
this.form = {
url: null,
publicStatus: "draft",
}
},
methods: {
submitForm() {
let data = {
'url': this.form.url,
'public_status': this.form.publicStatus,
};
axios
.post(`/api/v1/organizations/${this.adminUnitId}/events/import`,
data,
{
withCredentials: true,
handler: this,
handleLoading: this.handleSubmitting,
})
.then((response) => {
window.location.href = `/event/${response.data.id}`;
})
},
handleSubmitting(isLoading) {
this.isSubmitting = isLoading;
},
handleRequestError(error, message) {
let customMessage = message;
if (error.response.status == 422) {
customMessage = this.$t("comp.importError");
}
this.$root.makeErrorToast(customMessage);
}
}
};

View File

@ -224,6 +224,7 @@
<div class="dropdown-menu" aria-labelledby="navbarAdminUnitEventsDropdown">
<a class="dropdown-item" href="{{ url_for('manage_admin_unit_events', id=current_admin_unit.id) }}">{{ _('Show events') }}</a>
<a class="dropdown-item" href="{{ url_for('event_create_for_admin_unit_id', id=current_admin_unit.id) }}">{{ _('Create event') }}</a>
<a class="dropdown-item" href="{{ url_for('manage_admin_unit_events_import', id=current_admin_unit.id) }}">{{ _('Import event') }}</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="{{ url_for('manage_admin_unit_event_lists', id=current_admin_unit.id) }}">{{ _('Event lists') }}</a>
{% if current_admin_unit.suggestions_enabled %}

View File

@ -114,6 +114,8 @@
errors: {
uniqueViolation:
"An entry with the entered values already exists. Duplicate entries are not allowed.",
unprocessableEntity:
"The request was well-formed but was unable to be followed due to semantic errors.",
},
toast: {
errorTitle: "Error",
@ -155,6 +157,7 @@
size: "The {_field_} field size must be less than {size}KB",
double: "The {_field_} field must be a valid decimal",
uniqueOrganizationName: "Name is already taken",
url: "The {_field_} field must be a valid URL",
},
},
},
@ -222,6 +225,8 @@
errors: {
uniqueViolation:
"Ein Eintrag mit den eingegebenen Werten existiert bereits. Doppelte Einträge sind nicht erlaubt.",
unprocessableEntity:
"Die Anfrage konnte aufgrund von semantischen Fehlern nicht beantwortet werden.",
},
toast: {
errorTitle: "Fehler",
@ -261,6 +266,7 @@
size: "{_field_} muss kleiner als {size}KB sein",
double: "Das Feld {_field_} muss eine gültige Dezimalzahl sein",
uniqueOrganizationName: "Der Name ist bereits vergeben",
url: "Das Feld {_field_} muss eine gültige URL sein",
},
},
},
@ -298,6 +304,16 @@
immediate: false
});
VeeValidate.extend('url', {
validate: value => {
if (value) {
return /^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$/.test(value);
}
return false;
}
});
{% block vue_routes %}
const routes = [];
{% endblock %}
@ -384,7 +400,7 @@
const status = error && error.response && error.response.status;
let message = error.message || error;
if (status == 400) {
if (status == 400 || status == 422) {
message =
(error &&
error.response &&
@ -400,6 +416,9 @@
if (errorName == "Unique Violation") {
message = this.$t("shared.errors.uniqueViolation");
}
else if (errorName == "Unprocessable Entity") {
message = this.$t("shared.errors.unprocessableEntity");
}
}
if (

View File

@ -0,0 +1,22 @@
{% extends "layout_vue.html" %}
{%- block title -%}
{{ _('Events') }}
{%- endblock -%}
{% block component_scripts %}
<script src="{{ url_for('static', filename='vue/events/import.vue.js')}}"></script>
{% endblock %}
{% block component_definitions %}
Vue.component("EventImport", EventImport);
{% endblock %}
{% block vue_routes %}
const routes = [
{
path: "/manage/admin_unit/:admin_unit_id/events/import",
component: EventImport,
},
];
{% endblock %}

View File

@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2021-12-30 15:28+0100\n"
"POT-Creation-Date: 2022-01-11 23:44+0100\n"
"PO-Revision-Date: 2020-06-07 18:51+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: de\n"
@ -190,33 +190,33 @@ msgstr "Event_"
msgid "."
msgstr "."
#: project/api/__init__.py:84
#: project/api/__init__.py:85
msgid "message"
msgstr "message"
#: project/api/organization/resources.py:371
#: project/api/organization/resources.py:399
#: project/views/admin_unit_member_invitation.py:85
msgid "You have received an invitation"
msgstr "Du hast eine Einladung erhalten"
#: project/forms/admin.py:10 project/templates/layout.html:312
#: project/forms/admin.py:10 project/templates/layout.html:313
#: project/views/root.py:37
msgid "Terms of service"
msgstr "Nutzungsbedingungen"
#: project/forms/admin.py:11 project/templates/layout.html:316
#: project/forms/admin.py:11 project/templates/layout.html:317
#: project/views/root.py:45
msgid "Legal notice"
msgstr "Impressum"
#: project/forms/admin.py:12 project/templates/_macros.html:1392
#: project/templates/layout.html:320
#: project/templates/layout.html:321
#: project/templates/widget/event_suggestion/create.html:204
#: project/views/admin_unit.py:73 project/views/root.py:53
msgid "Contact"
msgstr "Kontakt"
#: project/forms/admin.py:13 project/templates/layout.html:324
#: project/forms/admin.py:13 project/templates/layout.html:325
#: project/views/root.py:61
msgid "Privacy"
msgstr "Datenschutz"
@ -1093,7 +1093,7 @@ msgstr "Wochentage"
#: project/templates/_macros.html:489 project/templates/_macros.html:649
#: project/templates/admin_unit/create.html:28
#: project/templates/admin_unit/update.html:29
#: project/templates/layout.html:260
#: project/templates/layout.html:261
msgid "Organization"
msgstr "Organisation"
@ -1396,6 +1396,7 @@ msgstr "Kostenlos registrieren"
#: project/templates/layout.html:175 project/templates/layout.html:219
#: project/templates/manage/events.html:6
#: project/templates/manage/events.html:41
#: project/templates/manage/events_vue.html:4
msgid "Events"
msgstr "Veranstaltungen"
@ -1413,7 +1414,7 @@ msgstr "Organisationen"
msgid "Planing"
msgstr "Planung"
#: project/templates/layout.html:187 project/templates/layout.html:274
#: project/templates/layout.html:187 project/templates/layout.html:275
#: project/templates/oauth2_client/list.html:10
#: project/templates/oauth2_client/read.html:10
#: project/templates/oauth2_token/list.html:10 project/templates/profile.html:4
@ -1442,63 +1443,67 @@ msgstr "Veranstaltungen anzeigen"
msgid "Create event"
msgstr "Veranstaltung erstellen"
#: project/templates/layout.html:228
#: project/templates/layout.html:227
msgid "Import event"
msgstr "Veranstaltung imporieren"
#: project/templates/layout.html:229
#: project/templates/manage/event_lists.html:4
msgid "Event lists"
msgstr "Veranstaltungslisten"
#: project/templates/layout.html:231
#: project/templates/layout.html:232
msgid "Review suggestions"
msgstr "Vorschläge prüfen"
#: project/templates/layout.html:241
#: project/templates/layout.html:242
#: project/templates/manage/references_incoming.html:5
#: project/templates/manage/references_outgoing.html:5
msgid "References"
msgstr "Empfehlungen"
#: project/templates/layout.html:247
#: project/templates/layout.html:248
#: project/templates/manage/references_incoming.html:9
msgid "Incoming references"
msgstr "Eingehende Empfehlungen"
#: project/templates/layout.html:248
#: project/templates/layout.html:249
#: project/templates/manage/references_outgoing.html:9
msgid "Outgoing references"
msgstr "Ausgehende Empfehlungen"
#: project/templates/layout.html:250
#: project/templates/layout.html:251
#: project/templates/manage/reference_requests_incoming.html:9
msgid "Incoming reference requests"
msgstr "Eingehende Empfehlungsanfragen"
#: project/templates/layout.html:255
#: project/templates/layout.html:256
#: project/templates/manage/reference_requests_outgoing.html:9
msgid "Outgoing reference requests"
msgstr "Ausgehende Empfehlungsanfragen"
#: project/templates/layout.html:263 project/templates/manage/organizers.html:5
#: project/templates/layout.html:264 project/templates/manage/organizers.html:5
#: project/templates/manage/organizers.html:9
msgid "Organizers"
msgstr "Veranstalter"
#: project/templates/event_place/list.html:3
#: project/templates/event_place/list.html:7 project/templates/layout.html:264
#: project/templates/event_place/list.html:7 project/templates/layout.html:265
#: project/templates/manage/places.html:5
#: project/templates/manage/places.html:9
msgid "Places"
msgstr "Orte"
#: project/templates/layout.html:266 project/templates/manage/members.html:5
#: project/templates/layout.html:267 project/templates/manage/members.html:5
#: project/templates/manage/members.html:28
msgid "Members"
msgstr "Mitglieder"
#: project/templates/layout.html:267 project/templates/manage/relations.html:4
#: project/templates/layout.html:268 project/templates/manage/relations.html:4
msgid "Relations"
msgstr "Beziehungen"
#: project/templates/layout.html:269
#: project/templates/layout.html:270
#: project/templates/manage/admin_units.html:17
#: project/templates/manage/organization_invitations.html:4
#: project/templates/user/organization_invitations.html:4
@ -1510,27 +1515,27 @@ msgstr "Organisationseinladungen"
#: project/templates/admin/settings.html:8
#: project/templates/admin_unit/update.html:6
#: project/templates/admin_unit/update.html:23
#: project/templates/layout.html:271 project/templates/manage/widgets.html:11
#: project/templates/layout.html:272 project/templates/manage/widgets.html:11
#: project/templates/manage/widgets.html:15 project/templates/profile.html:19
msgid "Settings"
msgstr "Einstellungen"
#: project/templates/layout.html:272
#: project/templates/layout.html:273
#: project/templates/manage/custom_widgets.html:13
msgid "Custom widgets"
msgstr "Custom widgets"
#: project/templates/layout.html:273 project/templates/manage/reviews.html:10
#: project/templates/layout.html:274 project/templates/manage/reviews.html:10
#: project/templates/manage/widgets.html:5
#: project/templates/manage/widgets.html:9
msgid "Widgets"
msgstr "Widgets"
#: project/templates/layout.html:284
#: project/templates/layout.html:285
msgid "Switch organization"
msgstr "Organisation wechseln"
#: project/templates/developer/read.html:4 project/templates/layout.html:334
#: project/templates/developer/read.html:4 project/templates/layout.html:335
#: project/templates/profile.html:29
msgid "Developer"
msgstr "Entwickler"
@ -1762,7 +1767,10 @@ msgstr "Zielgruppe"
msgid ""
"You are about to delete an event with multiple dates. If you want to "
"delete a single date instead, you have to edit the event to do so."
msgstr "Du bist im Begriff, eine Veranstaltung mit mehreren Terminen zu löschen. Wenn du stattdessen einen einzelnen Termin löschen möchtest, musst du dazu die Veranstaltung bearbeiten."
msgstr ""
"Du bist im Begriff, eine Veranstaltung mit mehreren Terminen zu löschen. "
"Wenn du stattdessen einen einzelnen Termin löschen möchtest, musst du "
"dazu die Veranstaltung bearbeiten."
#: project/templates/event/reference.html:8
#, python-format
@ -2009,7 +2017,7 @@ msgstr "Vorschau"
msgid "Organization successfully updated"
msgstr "Organisation erfolgreich aktualisiert"
#: project/views/admin.py:68 project/views/manage.py:346
#: project/views/admin.py:68 project/views/manage.py:360
msgid "Settings successfully updated"
msgstr "Einstellungen erfolgreich aktualisiert"
@ -2070,27 +2078,27 @@ msgstr "Die eingegebene Email passt nicht zur Email der Einladung"
msgid "Invitation successfully deleted"
msgstr "Einladung erfolgreich gelöscht"
#: project/views/event.py:178
#: project/views/event.py:180
msgid "Event successfully published"
msgstr "Veranstaltung erfolgreich veröffentlicht"
#: project/views/event.py:180
#: project/views/event.py:182
msgid "Draft successfully saved"
msgstr "Entwurf erfolgreich gespeichert"
#: project/views/event.py:223
#: project/views/event.py:225
msgid "Event successfully updated"
msgstr "Veranstaltung erfolgreich aktualisiert"
#: project/views/event.py:249
#: project/views/event.py:251
msgid "Event successfully deleted"
msgstr "Veranstaltung erfolgreich gelöscht"
#: project/views/event.py:408
#: project/views/event.py:410
msgid "Referenced event changed"
msgstr "Empfohlene Veranstaltung wurde geändert"
#: project/views/event.py:431
#: project/views/event.py:433
msgid "New event report"
msgstr "Neue Meldung zu einer Veranstaltung"

View File

@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2021-12-30 15:28+0100\n"
"POT-Creation-Date: 2022-01-11 23:44+0100\n"
"PO-Revision-Date: 2021-04-30 15:04+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: en\n"
@ -190,33 +190,33 @@ msgstr ""
msgid "."
msgstr ""
#: project/api/__init__.py:84
#: project/api/__init__.py:85
msgid "message"
msgstr ""
#: project/api/organization/resources.py:371
#: project/api/organization/resources.py:399
#: project/views/admin_unit_member_invitation.py:85
msgid "You have received an invitation"
msgstr ""
#: project/forms/admin.py:10 project/templates/layout.html:312
#: project/forms/admin.py:10 project/templates/layout.html:313
#: project/views/root.py:37
msgid "Terms of service"
msgstr ""
#: project/forms/admin.py:11 project/templates/layout.html:316
#: project/forms/admin.py:11 project/templates/layout.html:317
#: project/views/root.py:45
msgid "Legal notice"
msgstr ""
#: project/forms/admin.py:12 project/templates/_macros.html:1392
#: project/templates/layout.html:320
#: project/templates/layout.html:321
#: project/templates/widget/event_suggestion/create.html:204
#: project/views/admin_unit.py:73 project/views/root.py:53
msgid "Contact"
msgstr ""
#: project/forms/admin.py:13 project/templates/layout.html:324
#: project/forms/admin.py:13 project/templates/layout.html:325
#: project/views/root.py:61
msgid "Privacy"
msgstr ""
@ -1050,7 +1050,7 @@ msgstr ""
#: project/templates/_macros.html:489 project/templates/_macros.html:649
#: project/templates/admin_unit/create.html:28
#: project/templates/admin_unit/update.html:29
#: project/templates/layout.html:260
#: project/templates/layout.html:261
msgid "Organization"
msgstr ""
@ -1351,6 +1351,7 @@ msgstr ""
#: project/templates/layout.html:175 project/templates/layout.html:219
#: project/templates/manage/events.html:6
#: project/templates/manage/events.html:41
#: project/templates/manage/events_vue.html:4
msgid "Events"
msgstr ""
@ -1368,7 +1369,7 @@ msgstr ""
msgid "Planing"
msgstr ""
#: project/templates/layout.html:187 project/templates/layout.html:274
#: project/templates/layout.html:187 project/templates/layout.html:275
#: project/templates/oauth2_client/list.html:10
#: project/templates/oauth2_client/read.html:10
#: project/templates/oauth2_token/list.html:10 project/templates/profile.html:4
@ -1397,63 +1398,67 @@ msgstr ""
msgid "Create event"
msgstr ""
#: project/templates/layout.html:228
#: project/templates/layout.html:227
msgid "Import event"
msgstr ""
#: project/templates/layout.html:229
#: project/templates/manage/event_lists.html:4
msgid "Event lists"
msgstr ""
#: project/templates/layout.html:231
#: project/templates/layout.html:232
msgid "Review suggestions"
msgstr ""
#: project/templates/layout.html:241
#: project/templates/layout.html:242
#: project/templates/manage/references_incoming.html:5
#: project/templates/manage/references_outgoing.html:5
msgid "References"
msgstr ""
#: project/templates/layout.html:247
#: project/templates/layout.html:248
#: project/templates/manage/references_incoming.html:9
msgid "Incoming references"
msgstr ""
#: project/templates/layout.html:248
#: project/templates/layout.html:249
#: project/templates/manage/references_outgoing.html:9
msgid "Outgoing references"
msgstr ""
#: project/templates/layout.html:250
#: project/templates/layout.html:251
#: project/templates/manage/reference_requests_incoming.html:9
msgid "Incoming reference requests"
msgstr ""
#: project/templates/layout.html:255
#: project/templates/layout.html:256
#: project/templates/manage/reference_requests_outgoing.html:9
msgid "Outgoing reference requests"
msgstr ""
#: project/templates/layout.html:263 project/templates/manage/organizers.html:5
#: project/templates/layout.html:264 project/templates/manage/organizers.html:5
#: project/templates/manage/organizers.html:9
msgid "Organizers"
msgstr ""
#: project/templates/event_place/list.html:3
#: project/templates/event_place/list.html:7 project/templates/layout.html:264
#: project/templates/event_place/list.html:7 project/templates/layout.html:265
#: project/templates/manage/places.html:5
#: project/templates/manage/places.html:9
msgid "Places"
msgstr ""
#: project/templates/layout.html:266 project/templates/manage/members.html:5
#: project/templates/layout.html:267 project/templates/manage/members.html:5
#: project/templates/manage/members.html:28
msgid "Members"
msgstr ""
#: project/templates/layout.html:267 project/templates/manage/relations.html:4
#: project/templates/layout.html:268 project/templates/manage/relations.html:4
msgid "Relations"
msgstr ""
#: project/templates/layout.html:269
#: project/templates/layout.html:270
#: project/templates/manage/admin_units.html:17
#: project/templates/manage/organization_invitations.html:4
#: project/templates/user/organization_invitations.html:4
@ -1465,27 +1470,27 @@ msgstr ""
#: project/templates/admin/settings.html:8
#: project/templates/admin_unit/update.html:6
#: project/templates/admin_unit/update.html:23
#: project/templates/layout.html:271 project/templates/manage/widgets.html:11
#: project/templates/layout.html:272 project/templates/manage/widgets.html:11
#: project/templates/manage/widgets.html:15 project/templates/profile.html:19
msgid "Settings"
msgstr ""
#: project/templates/layout.html:272
#: project/templates/layout.html:273
#: project/templates/manage/custom_widgets.html:13
msgid "Custom widgets"
msgstr ""
#: project/templates/layout.html:273 project/templates/manage/reviews.html:10
#: project/templates/layout.html:274 project/templates/manage/reviews.html:10
#: project/templates/manage/widgets.html:5
#: project/templates/manage/widgets.html:9
msgid "Widgets"
msgstr ""
#: project/templates/layout.html:284
#: project/templates/layout.html:285
msgid "Switch organization"
msgstr ""
#: project/templates/developer/read.html:4 project/templates/layout.html:334
#: project/templates/developer/read.html:4 project/templates/layout.html:335
#: project/templates/profile.html:29
msgid "Developer"
msgstr ""
@ -1958,7 +1963,7 @@ msgstr ""
msgid "Organization successfully updated"
msgstr ""
#: project/views/admin.py:68 project/views/manage.py:346
#: project/views/admin.py:68 project/views/manage.py:360
msgid "Settings successfully updated"
msgstr ""
@ -2016,27 +2021,27 @@ msgstr ""
msgid "Invitation successfully deleted"
msgstr ""
#: project/views/event.py:178
#: project/views/event.py:180
msgid "Event successfully published"
msgstr ""
#: project/views/event.py:180
#: project/views/event.py:182
msgid "Draft successfully saved"
msgstr ""
#: project/views/event.py:223
#: project/views/event.py:225
msgid "Event successfully updated"
msgstr ""
#: project/views/event.py:249
#: project/views/event.py:251
msgid "Event successfully deleted"
msgstr ""
#: project/views/event.py:408
#: project/views/event.py:410
msgid "Referenced event changed"
msgstr ""
#: project/views/event.py:431
#: project/views/event.py:433
msgid "New event report"
msgstr ""

View File

@ -46,6 +46,7 @@ from project.views.utils import (
get_share_links,
handleSqlError,
send_mails,
set_current_admin_unit,
)
@ -106,6 +107,7 @@ def event_report(event_id):
def event_create_for_admin_unit_id(id):
admin_unit = AdminUnit.query.get_or_404(id)
access_or_401(admin_unit, "event:create")
set_current_admin_unit(admin_unit)
form = CreateEventForm(
admin_unit_id=admin_unit.id, category_ids=[upsert_event_category("Other").id]

View File

@ -6,6 +6,7 @@ from sqlalchemy.sql import desc, func
from project import app, db
from project.access import (
access_or_401,
admin_unit_suggestions_enabled_or_404,
get_admin_unit_for_manage_or_404,
get_admin_units_for_manage,
@ -306,6 +307,19 @@ def manage_admin_unit_custom_widgets(id, path=None):
)
@app.route("/manage/admin_unit/<int:id>/events/import")
@auth_required()
def manage_admin_unit_events_import(id):
admin_unit = get_admin_unit_for_manage_or_404(id)
access_or_401(admin_unit, "event:create")
set_current_admin_unit(admin_unit)
return render_template(
"manage/events_vue.html",
admin_unit=admin_unit,
)
@app.route("/manage/admin_unit/<int:id>/widgets", methods=("GET", "POST"))
@auth_required()
def manage_admin_unit_widgets(id):

View File

@ -21,6 +21,7 @@ colour==0.1.5
coverage==5.5
coveralls==2.2.0
cryptography==3.3.2
decorator==5.1.0
distlib==0.3.1
dnspython==2.0.0
docopt==0.6.2
@ -83,6 +84,7 @@ pyparsing==2.4.7
pyrsistent==0.17.3
pytest==6.1.2
pytest-cov==2.10.1
pytest-datadir==1.3.1
pytest-mock==3.3.1
python-dateutil==2.8.1
python-dotenv==0.15.0
@ -107,6 +109,7 @@ typed-ast==1.4.1
typing-extensions==3.7.4.3
urllib3==1.26.5
URLObject==2.4.3
validators==0.18.2
virtualenv==20.2.2
visitor==0.1.3
webargs==7.0.1

File diff suppressed because one or more lines are too long

View File

@ -267,6 +267,33 @@ def test_events_post_photo_too_small(client, seeder, utils, app):
assert error["message"] == "Image is too small (1x1px). At least 320x320px."
def test_events_import(client, seeder, utils, app, shared_datadir):
external_url = "https://www.harzinfo.de/event/xy"
utils.mock_get_request_with_file(
external_url, shared_datadir, "harzinfo_biathlon.html"
)
_, admin_unit_id = seeder.setup_base()
url = utils.get_url("api_v1_organization_event_import", id=admin_unit_id)
response = utils.post_json(url, {"url": external_url})
utils.assert_response_created(response)
assert "id" in response.json
event_id = int(response.json["id"])
with app.app_context():
from project.models import Event
event = Event.query.get(event_id)
assert "lädt" in event.description
# 422
external_url = "https://www.harzinfo.de/event/abc"
utils.mock_get_request_with_text(external_url, "")
response = utils.post_json(url, {"url": external_url})
utils.assert_response_unprocessable_entity(response)
def test_places(client, seeder, utils):
user_id, admin_unit_id = seeder.setup_base()
seeder.upsert_default_event_place(admin_unit_id)

View File

@ -58,8 +58,8 @@ def client(app, db):
@pytest.fixture
def utils(client, app, mocker):
return UtilActions(client, app, mocker)
def utils(client, app, mocker, requests_mock):
return UtilActions(client, app, mocker, requests_mock)
@pytest.fixture

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,88 @@
import pytest
# Load more urls:
# curl -o tests/services/importer/test_event_importer/<filename>.html <URL>
def test_import(client, seeder, utils, app, shared_datadir, requests_mock):
_, admin_unit_id = seeder.setup_base()
seeder.upsert_event_place(admin_unit_id, "MINER'S ROCK")
seeder.upsert_event_organizer(admin_unit_id, "MINER'S ROCK")
params = (utils, admin_unit_id, shared_datadir)
with app.app_context():
_assert_import_event(
params,
"facebook.html",
"https://www.facebook.com/events/413124792931487",
"https://m.facebook.com/events/413124792931487",
)
_assert_import_event(
params,
"regiondo.html",
"https://goslar.regiondo.de/unterwegs-mit-der-frau-des-nachtwachters",
)
_assert_import_event(
params,
"eventim.html",
"https://www.eventim.de/event/kaisermania-2022-roland-kaiser-live-mit-band-filmnaechte-am-elbufer-14339901/",
)
_assert_import_event(
params,
"meetup.html",
"https://www.meetup.com/de-DE/hi-new-work/events/282743665/",
)
_assert_import_event(
params,
"reservix.html",
"https://kultur-kraftwerk.reservix.de/tickets-christoph-kuch-ich-weiss-in-goslar-kulturkraftwerk-harzenergie-am-14-1-2022/e1739715",
"https://www.reservix.de/tickets-christoph-kuch-ich-weiss-in-goslar-kulturkraftwerk-harzenergie-am-14-1-2022/e1739715",
)
_assert_import_event(
params,
"eventbrite.html",
"https://www.eventbrite.de/e/fr110322-wanderdate-sagenumwobene-brockentour-fur-singles-fur-40-65j-tickets-224906520457",
)
# None JSON
with pytest.raises(Exception):
_assert_import_event(
params,
"harzinfo_json_none.html",
"https://www.harzinfo.de",
)
# With image
utils.mock_image_request_with_file(
"https://dam.destination.one/762032/f243062990392ce94607e37db3458303daedb6ce8b1aa03b80381f9b10aabc6b/210710-facebook-biathlon-challenge-2400x1256px-png.png",
shared_datadir,
"image500.png",
)
event = _assert_import_event(
params,
"harzinfo_biathlon.html",
"https://www.harzinfo.de",
)
assert event is not None
assert "lädt" in event.description
assert event.photo is not None
def _assert_import_event(params, filename, url, sanitized_url=None):
from project.services.importer.event_importer import EventImporter
utils, admin_unit_id, datadir = params
mock_url = sanitized_url if sanitized_url else url
utils.mock_get_request_with_file(mock_url, datadir, filename)
importer = EventImporter(admin_unit_id)
event = importer.load_event_from_url(url)
assert event is not None
return event

View File

@ -0,0 +1,133 @@
from tests.sub_tests import SubTests
class TestLdJsonImporter(SubTests):
def test_load_event_from_ld_json(self, client, seeder, utils, app, shared_datadir):
self.utils = utils
self.shared_datadir = shared_datadir
with app.app_context():
self.call_sub_tests()
def _test_category(self):
def manipulate(ld_json):
ld_json["@type"] = "MusicEvent"
event = self._load_event_from_ld_json(manipulate)
assert event.categories[0].name == "Music"
def _test_booked_up_ausgebucht(self):
def manipulate(ld_json):
ld_json["eventStatus"] = "ausgebucht"
event = self._load_event_from_ld_json(manipulate)
assert event.booked_up
def _test_default(self):
event = self._load_event_from_ld_json()
assert event is not None
def _test_accessible_for_free(self):
def manipulate(ld_json):
ld_json["isAccessibleForFree"] = True
event = self._load_event_from_ld_json(manipulate)
assert event.accessible_for_free
def _test_no_organizer_but_author(self):
def manipulate(ld_json):
ld_json["author"] = ld_json["organizer"]
del ld_json["organizer"]
event = self._load_event_from_ld_json(manipulate)
assert event is not None
def _test_no_organizer(self):
import pytest
def manipulate(ld_json):
del ld_json["organizer"]
with pytest.raises(Exception):
self._load_event_from_ld_json(manipulate)
def _test_photo_empty_list(self):
def manipulate(ld_json):
ld_json["image"] = []
event = self._load_event_from_ld_json(manipulate)
assert event is not None
def _test_photo_no_url(self):
def manipulate(ld_json):
ld_json["image"] = {}
event = self._load_event_from_ld_json(manipulate)
assert event is not None
def _test_photo_contributor(self):
self.utils.mock_image_request_with_file(
"https://example.org/image",
self.shared_datadir,
"image500.png",
)
def manipulate(ld_json):
ld_json["image"] = {
"url": "https://example.org/image",
"contributor": "Contributor",
}
event = self._load_event_from_ld_json(manipulate)
assert event.photo.copyright_text == "Contributor"
def _load_event_from_ld_json(self, manipulate_json=None):
from project.services.importer.ld_json_importer import LdJsonImporter
ld_json = self._create_ld_json()
if manipulate_json:
manipulate_json(ld_json)
importer = LdJsonImporter("", "")
importer.ld_json = ld_json
event = importer.load_event_from_ld_json()
return event
def _create_ld_json(self):
return {
"@type": "Event",
"location": [
{
"@type": "Place",
"address": {
"@type": "PostalAddress",
"addressCountry": "Deutschland",
"addressLocality": "Wernigerode OT Schierke",
"postalCode": "38879",
"streetAddress": "Am Winterbergtor 2",
},
"name": "Schierker Feuerstein Arena",
}
],
"name": "Name",
"organizer": [
{
"@type": ["Organization"],
"address": {
"@type": "PostalAddress",
"addressLocality": "Wernigerode OT Schierke",
"postalCode": "38879",
"streetAddress": "Am Winterbergtor 2",
},
"email": "sfa@wernigerode.de",
"name": " Schierker Feuerstein Arena",
"telephone": "03943 - 654 777",
"faxNumber": "03943 - 654 778",
"url": "http://www.schierker-feuerstein-arena.de",
}
],
"startDate": "2021-07-10T11:00:00+02:00",
"url": "https://www.harzinfo.de/veranstaltungen/event/3-schierker-biathlon-challenge-mit-michael-roesch",
}

10
tests/sub_tests.py Normal file
View File

@ -0,0 +1,10 @@
class SubTests:
def call_sub_tests(self):
sub_tests = [
getattr(self, method_name)
for method_name in dir(self)
if callable(getattr(self, method_name)) and method_name.startswith("_test")
]
for sub_test in sub_tests:
sub_test()

View File

@ -13,6 +13,18 @@ def test_resize_image_to_max():
assert image.height == max_image_size
def test_resize_image_to_min():
import PIL
from project.imageutils import min_image_size, resize_image_to_min
image = PIL.Image.new("RGB", (min_image_size - 1, min_image_size - 1))
image = resize_image_to_min(image)
assert image.width == min_image_size
assert image.height == min_image_size
def test_validate_image_too_small():
import PIL

View File

@ -1,3 +1,4 @@
import pathlib
import re
from urllib.parse import parse_qs, urlsplit
@ -8,10 +9,11 @@ from sqlalchemy.exc import IntegrityError
class UtilActions(object):
def __init__(self, client, app, mocker):
def __init__(self, client, app, mocker, requests_mock):
self._client = client
self._app = app
self._mocker = mocker
self._requests_mock = requests_mock
self._access_token = None
self._refresh_token = None
self._client_id = None
@ -446,3 +448,17 @@ class UtilActions(object):
def get_oauth_userinfo(self):
url = self.get_url("oauth_userinfo")
return self.get_json(url)
def mock_get_request_with_text(self, url: str, text: str):
self._requests_mock.get(url, text=text)
def mock_get_request_with_content(self, url: str, content):
self._requests_mock.get(url, content=content)
def mock_get_request_with_file(self, url: str, path: pathlib.Path, filename: str):
text = (path / filename).read_text()
self.mock_get_request_with_text(url, text)
def mock_image_request_with_file(self, url: str, path: pathlib.Path, filename: str):
content = (path / filename).read_bytes()
self.mock_get_request_with_content(url, content)

View File

@ -209,3 +209,10 @@ def test_admin_unit_custom_widgets(client, seeder, utils):
url = utils.get_url("manage_admin_unit_custom_widgets", id=admin_unit_id)
utils.get_ok(url)
def test_admin_unit_events_import(client, seeder, utils):
_, admin_unit_id = seeder.setup_base()
url = utils.get_url("manage_admin_unit_events_import", id=admin_unit_id)
utils.get_ok(url)