mirror of
https://github.com/lucaspalomodevelop/eventcally.git
synced 2026-03-13 08:09:37 +00:00
Events via URL importieren #354
This commit is contained in:
parent
8a25475fc0
commit
f52d919069
75
messages.pot
75
messages.pot
@ -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 ""
|
||||
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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."},
|
||||
)
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
86
project/services/importer/event_importer.py
Normal file
86
project/services/importer/event_importer.py
Normal 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
|
||||
479
project/services/importer/ld_json_importer.py
Normal file
479
project/services/importer/ld_json_importer.py
Normal 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/", "")
|
||||
)
|
||||
96
project/static/vue/events/import.vue.js
Normal file
96
project/static/vue/events/import.vue.js
Normal 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);
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -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 %}
|
||||
|
||||
@ -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 (
|
||||
|
||||
22
project/templates/manage/events_vue.html
Normal file
22
project/templates/manage/events_vue.html
Normal 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 %}
|
||||
Binary file not shown.
@ -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"
|
||||
|
||||
|
||||
Binary file not shown.
@ -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 ""
|
||||
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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
|
||||
|
||||
3
tests/api/data/harzinfo_biathlon.html
Normal file
3
tests/api/data/harzinfo_biathlon.html
Normal file
File diff suppressed because one or more lines are too long
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
2177
tests/services/importer/data/eventbrite.html
Normal file
2177
tests/services/importer/data/eventbrite.html
Normal file
File diff suppressed because one or more lines are too long
6122
tests/services/importer/data/eventim.html
Normal file
6122
tests/services/importer/data/eventim.html
Normal file
File diff suppressed because one or more lines are too long
4
tests/services/importer/data/facebook.html
Normal file
4
tests/services/importer/data/facebook.html
Normal file
File diff suppressed because one or more lines are too long
3
tests/services/importer/data/harzinfo_biathlon.html
Normal file
3
tests/services/importer/data/harzinfo_biathlon.html
Normal file
File diff suppressed because one or more lines are too long
3
tests/services/importer/data/harzinfo_json_none.html
Normal file
3
tests/services/importer/data/harzinfo_json_none.html
Normal file
File diff suppressed because one or more lines are too long
BIN
tests/services/importer/data/image500.png
Normal file
BIN
tests/services/importer/data/image500.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
53
tests/services/importer/data/meetup.html
Normal file
53
tests/services/importer/data/meetup.html
Normal file
File diff suppressed because one or more lines are too long
1338
tests/services/importer/data/regiondo.html
Normal file
1338
tests/services/importer/data/regiondo.html
Normal file
File diff suppressed because one or more lines are too long
1408
tests/services/importer/data/reservix.html
Normal file
1408
tests/services/importer/data/reservix.html
Normal file
File diff suppressed because it is too large
Load Diff
88
tests/services/importer/test_event_importer.py
Normal file
88
tests/services/importer/test_event_importer.py
Normal 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
|
||||
133
tests/services/importer/test_ld_json_importer.py
Normal file
133
tests/services/importer/test_ld_json_importer.py
Normal 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
10
tests/sub_tests.py
Normal 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()
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user