From daabc2659b8df18be4dec6cd1a791307898f0c88 Mon Sep 17 00:00:00 2001 From: Daniel Grams Date: Thu, 30 Dec 2021 14:55:26 +0100 Subject: [PATCH] Custom widgets #345 --- messages.pot | 49 +- migrations/versions/c5fbefbe9881_.py | 55 ++ project/api/__init__.py | 2 + project/api/custom_widget/__init__.py | 0 project/api/custom_widget/resources.py | 84 +++ project/api/custom_widget/schemas.py | 82 +++ project/api/organization/resources.py | 48 ++ project/forms/event_date.py | 1 + project/models.py | 15 + project/services/admin_unit.py | 11 + project/services/event_search.py | 3 + project/static/vue/common/typeahead.vue.js | 35 +- .../widget-configurator/configurator.vue.js | 571 ++++++++++++++++++ .../vue/widget-configurator/list.vue.js | 170 ++++++ project/static/widget-loader.js | 150 +++-- project/static/widget/calendar.html | 273 +++++++++ project/static/widget/search.html | 525 ++++++++++++++++ project/templates/layout.html | 1 + project/templates/layout_vue.html | 20 +- project/templates/manage/custom_widgets.html | 46 ++ .../translations/de/LC_MESSAGES/messages.mo | Bin 33656 -> 33702 bytes .../translations/de/LC_MESSAGES/messages.po | 49 +- .../translations/en/LC_MESSAGES/messages.mo | Bin 3565 -> 3565 bytes .../translations/en/LC_MESSAGES/messages.po | 49 +- project/views/manage.py | 15 + tests/api/test_custom_widget.py | 55 ++ tests/api/test_event_date.py | 3 + tests/api/test_organization.py | 38 ++ tests/seeder.py | 22 + tests/views/test_manage.py | 7 + 30 files changed, 2270 insertions(+), 109 deletions(-) create mode 100644 migrations/versions/c5fbefbe9881_.py create mode 100644 project/api/custom_widget/__init__.py create mode 100644 project/api/custom_widget/resources.py create mode 100644 project/api/custom_widget/schemas.py create mode 100644 project/static/vue/widget-configurator/configurator.vue.js create mode 100644 project/static/vue/widget-configurator/list.vue.js create mode 100644 project/static/widget/calendar.html create mode 100644 project/static/widget/search.html create mode 100644 project/templates/manage/custom_widgets.html create mode 100644 tests/api/test_custom_widget.py diff --git a/messages.pot b/messages.pot index f93be78..ba28412 100644 --- a/messages.pot +++ b/messages.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2021-12-08 13:32+0100\n" +"POT-Creation-Date: 2021-12-30 14:51+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -193,29 +193,29 @@ msgstr "" msgid "message" msgstr "" -#: project/api/organization/resources.py:364 +#: project/api/organization/resources.py:371 #: project/views/admin_unit_member_invitation.py:85 msgid "You have received an invitation" msgstr "" -#: project/forms/admin.py:10 project/templates/layout.html:311 +#: project/forms/admin.py:10 project/templates/layout.html:312 #: project/views/root.py:37 msgid "Terms of service" msgstr "" -#: project/forms/admin.py:11 project/templates/layout.html:315 +#: project/forms/admin.py:11 project/templates/layout.html:316 #: project/views/root.py:45 msgid "Legal notice" msgstr "" #: project/forms/admin.py:12 project/templates/_macros.html:1392 -#: project/templates/layout.html:319 +#: project/templates/layout.html:320 #: 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:323 +#: project/forms/admin.py:13 project/templates/layout.html:324 #: project/views/root.py:61 msgid "Privacy" msgstr "" @@ -917,7 +917,7 @@ msgstr "" msgid "Distance" msgstr "" -#: project/forms/event_date.py:33 project/forms/planing.py:36 +#: project/forms/event_date.py:38 project/forms/planing.py:36 #: project/templates/widget/event_date/list.html:82 msgid "Find" msgstr "" @@ -1342,7 +1342,7 @@ msgid "Manage" msgstr "" #: project/templates/home.html:30 project/templates/security/login_user.html:38 -#: project/views/widget.py:158 +#: project/views/widget.py:159 msgid "Register for free" msgstr "" @@ -1366,7 +1366,7 @@ msgstr "" msgid "Planing" msgstr "" -#: project/templates/layout.html:187 project/templates/layout.html:273 +#: project/templates/layout.html:187 project/templates/layout.html:274 #: 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 @@ -1468,17 +1468,22 @@ msgstr "" msgid "Settings" msgstr "" -#: project/templates/layout.html:272 project/templates/manage/reviews.html:10 +#: project/templates/layout.html:272 +#: project/templates/manage/custom_widgets.html:13 +msgid "Custom widgets" +msgstr "" + +#: project/templates/layout.html:273 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:283 +#: project/templates/layout.html:284 msgid "Switch organization" msgstr "" -#: project/templates/developer/read.html:4 project/templates/layout.html:333 +#: project/templates/developer/read.html:4 project/templates/layout.html:334 #: project/templates/profile.html:29 msgid "Developer" msgstr "" @@ -1945,7 +1950,7 @@ msgstr "" msgid "Organization successfully updated" msgstr "" -#: project/views/admin.py:68 project/views/manage.py:331 +#: project/views/admin.py:68 project/views/manage.py:346 msgid "Settings successfully updated" msgstr "" @@ -2003,27 +2008,27 @@ msgstr "" msgid "Invitation successfully deleted" msgstr "" -#: project/views/event.py:179 +#: project/views/event.py:178 msgid "Event successfully published" msgstr "" -#: project/views/event.py:181 +#: project/views/event.py:180 msgid "Draft successfully saved" msgstr "" -#: project/views/event.py:224 +#: project/views/event.py:223 msgid "Event successfully updated" msgstr "" -#: project/views/event.py:250 +#: project/views/event.py:249 msgid "Event successfully deleted" msgstr "" -#: project/views/event.py:409 +#: project/views/event.py:408 msgid "Referenced event changed" msgstr "" -#: project/views/event.py:432 +#: project/views/event.py:431 msgid "New event report" msgstr "" @@ -2179,17 +2184,17 @@ msgid "" " the invitation was sent to." msgstr "" -#: project/views/widget.py:150 +#: project/views/widget.py:151 msgid "Thank you so much! The event is being verified." msgstr "" -#: project/views/widget.py:154 +#: project/views/widget.py:155 msgid "" "For more options and your own calendar of events, you can register for " "free." msgstr "" -#: project/views/widget.py:212 +#: project/views/widget.py:221 msgid "New event review" msgstr "" diff --git a/migrations/versions/c5fbefbe9881_.py b/migrations/versions/c5fbefbe9881_.py new file mode 100644 index 0000000..703eaea --- /dev/null +++ b/migrations/versions/c5fbefbe9881_.py @@ -0,0 +1,55 @@ +"""empty message + +Revision ID: c5fbefbe9881 +Revises: eba21922b9b7 +Create Date: 2021-12-29 22:35:29.300556 + +""" +import sqlalchemy as sa +import sqlalchemy_utils +from alembic import op +from sqlalchemy.dialects import postgresql + +from project import dbtypes + +# revision identifiers, used by Alembic. +revision = "c5fbefbe9881" +down_revision = "eba21922b9b7" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "customwidget", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("widget_type", sa.Unicode(length=255), nullable=False), + sa.Column("name", sa.Unicode(length=255), nullable=False), + sa.Column("admin_unit_id", sa.Integer(), nullable=False), + sa.Column("settings", postgresql.JSONB(astext_type=sa.Text()), nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("updated_at", sa.DateTime(), nullable=True), + sa.Column("created_by_id", sa.Integer(), nullable=True), + sa.Column("updated_by_id", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint( + ["admin_unit_id"], + ["adminunit.id"], + ), + sa.ForeignKeyConstraint( + ["created_by_id"], + ["user.id"], + ), + sa.ForeignKeyConstraint( + ["updated_by_id"], + ["user.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("customwidget") + # ### end Alembic commands ### diff --git a/project/api/__init__.py b/project/api/__init__.py index 14ef9fd..9958acd 100644 --- a/project/api/__init__.py +++ b/project/api/__init__.py @@ -125,6 +125,7 @@ scope_list = [ "organization:read", "organization:write", "eventlist:write", + "customwidget:write", ] scopes = {k: get_localized_scope(k) for v, k in enumerate(scope_list)} @@ -188,6 +189,7 @@ def add_oauth2_scheme_with_transport(insecure: bool): marshmallow_plugin.converter.add_attribute_function(enum_to_properties) +import project.api.custom_widget.resources import project.api.dump.resources import project.api.event.resources import project.api.event_category.resources diff --git a/project/api/custom_widget/__init__.py b/project/api/custom_widget/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/project/api/custom_widget/resources.py b/project/api/custom_widget/resources.py new file mode 100644 index 0000000..22ddb8a --- /dev/null +++ b/project/api/custom_widget/resources.py @@ -0,0 +1,84 @@ +from flask import make_response +from flask_apispec import doc, marshal_with, use_kwargs + +from project import db +from project.access import access_or_401, login_api_user_or_401 +from project.api import add_api_resource +from project.api.custom_widget.schemas import ( + CustomWidgetPatchRequestSchema, + CustomWidgetPostRequestSchema, + CustomWidgetSchema, +) +from project.api.resources import BaseResource, require_api_access +from project.models import CustomWidget + + +class CustomWidgetResource(BaseResource): + @doc(summary="Get custom widget", tags=["Custom Widgets"]) + @marshal_with(CustomWidgetSchema) + def get(self, id): + return CustomWidget.query.get_or_404(id) + + @doc( + summary="Update custom widget", + tags=["Custom Widgets"], + security=[{"oauth2": ["customwidget:write"]}], + ) + @use_kwargs(CustomWidgetPostRequestSchema, location="json", apply=False) + @marshal_with(None, 204) + @require_api_access("customwidget:write") + def put(self, id): + login_api_user_or_401() + customwidget = CustomWidget.query.get_or_404(id) + access_or_401(customwidget.adminunit, "admin_unit:update") + + customwidget = self.update_instance( + CustomWidgetPostRequestSchema, instance=customwidget + ) + db.session.commit() + + return make_response("", 204) + + @doc( + summary="Patch custom widget", + tags=["Custom Widgets"], + security=[{"oauth2": ["customwidget:write"]}], + ) + @use_kwargs(CustomWidgetPatchRequestSchema, location="json", apply=False) + @marshal_with(None, 204) + @require_api_access("customwidget:write") + def patch(self, id): + login_api_user_or_401() + customwidget = CustomWidget.query.get_or_404(id) + access_or_401(customwidget.adminunit, "admin_unit:update") + + customwidget = self.update_instance( + CustomWidgetPatchRequestSchema, instance=customwidget + ) + db.session.commit() + + return make_response("", 204) + + @doc( + summary="Delete custom widget", + tags=["Custom Widgets"], + security=[{"oauth2": ["customwidget:write"]}], + ) + @marshal_with(None, 204) + @require_api_access("customwidget:write") + def delete(self, id): + login_api_user_or_401() + customwidget = CustomWidget.query.get_or_404(id) + access_or_401(customwidget.adminunit, "admin_unit:update") + + db.session.delete(customwidget) + db.session.commit() + + return make_response("", 204) + + +add_api_resource( + CustomWidgetResource, + "/custom-widgets/", + "api_v1_custom_widget", +) diff --git a/project/api/custom_widget/schemas.py b/project/api/custom_widget/schemas.py new file mode 100644 index 0000000..6ba3a97 --- /dev/null +++ b/project/api/custom_widget/schemas.py @@ -0,0 +1,82 @@ +from marshmallow import fields, validate + +from project.api import marshmallow +from project.api.organization.schemas import OrganizationRefSchema +from project.api.schemas import ( + IdSchemaMixin, + PaginationRequestSchema, + PaginationResponseSchema, + SQLAlchemyBaseSchema, + TrackableSchemaMixin, + WriteIdSchemaMixin, +) +from project.models import CustomWidget + + +class CustomWidgetModelSchema(SQLAlchemyBaseSchema): + class Meta: + model = CustomWidget + load_instance = True + + +class CustomWidgetIdSchema(CustomWidgetModelSchema, IdSchemaMixin): + pass + + +class CustomWidgetDumpIdSchema(CustomWidgetModelSchema, IdSchemaMixin): + pass + + +class CustomWidgetWriteIdSchema(CustomWidgetModelSchema, WriteIdSchemaMixin): + pass + + +class CustomWidgetBaseSchemaMixin(TrackableSchemaMixin): + widget_type = marshmallow.auto_field( + required=True, validate=validate.Length(min=3, max=255) + ) + name = marshmallow.auto_field( + required=True, validate=validate.Length(min=3, max=255) + ) + settings = fields.Dict(keys=fields.Str()) + + +class CustomWidgetSchema(CustomWidgetIdSchema, CustomWidgetBaseSchemaMixin): + organization = fields.Nested(OrganizationRefSchema, attribute="adminunit") + + +class CustomWidgetDumpSchema(CustomWidgetIdSchema, CustomWidgetBaseSchemaMixin): + organization_id = fields.Int(attribute="admin_unit_id") + + +class CustomWidgetRefSchema(CustomWidgetIdSchema): + widget_type = marshmallow.auto_field() + name = marshmallow.auto_field() + + +class CustomWidgetListRequestSchema(PaginationRequestSchema): + name = fields.Str( + metadata={"description": "Looks for name."}, + ) + + +class CustomWidgetListResponseSchema(PaginationResponseSchema): + items = fields.List( + fields.Nested(CustomWidgetRefSchema), metadata={"description": "Custom widgets"} + ) + + +class CustomWidgetPostRequestSchema( + CustomWidgetModelSchema, CustomWidgetBaseSchemaMixin +): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.make_post_schema() + + +class CustomWidgetPatchRequestSchema( + CustomWidgetModelSchema, CustomWidgetBaseSchemaMixin +): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.make_patch_schema() diff --git a/project/api/organization/resources.py b/project/api/organization/resources.py index 59c489f..0e72036 100644 --- a/project/api/organization/resources.py +++ b/project/api/organization/resources.py @@ -11,6 +11,12 @@ from project.access import ( login_api_user_or_401, ) from project.api import add_api_resource +from project.api.custom_widget.schemas import ( + CustomWidgetIdSchema, + CustomWidgetListRequestSchema, + CustomWidgetListResponseSchema, + CustomWidgetPostRequestSchema, +) from project.api.event.resources import api_can_read_private_events from project.api.event.schemas import ( EventIdSchema, @@ -70,6 +76,7 @@ from project.oauth2 import require_oauth from project.services.admin_unit import ( get_admin_unit_invitation_query, get_admin_unit_query, + get_custom_widget_query, get_event_list_query, get_event_list_status_query, get_organizer_query, @@ -422,6 +429,42 @@ class OrganizationEventListStatusListResource(BaseResource): return pagination +class OrganizationCustomWidgetListResource(BaseResource): + @doc( + summary="List custom widgets of organization", + tags=["Organizations", "Custom Widgets"], + ) + @use_kwargs(CustomWidgetListRequestSchema, location=("query")) + @marshal_with(CustomWidgetListResponseSchema) + def get(self, id, **kwargs): + admin_unit = AdminUnit.query.get_or_404(id) + name = kwargs["name"] if "name" in kwargs else None + + pagination = get_custom_widget_query(admin_unit.id, name).paginate() + return pagination + + @doc( + summary="Add new custom widget", + tags=["Organizations", "CustomWidgets"], + security=[{"oauth2": ["customwidget:write"]}], + ) + @use_kwargs(CustomWidgetPostRequestSchema, location="json", apply=False) + @marshal_with(CustomWidgetIdSchema, 201) + @require_api_access("customwidget:write") + def post(self, id): + login_api_user_or_401() + admin_unit = get_admin_unit_for_manage_or_404(id) + access_or_401(admin_unit, "admin_unit:update") + + custom_widget = self.create_instance( + CustomWidgetPostRequestSchema, admin_unit_id=admin_unit.id + ) + db.session.add(custom_widget) + db.session.commit() + + return custom_widget, 201 + + add_api_resource(OrganizationResource, "/organizations/", "api_v1_organization") add_api_resource( OrganizationEventDateSearchResource, @@ -479,3 +522,8 @@ add_api_resource( "/organizations//organization-invitations", "api_v1_organization_organization_invitation_list", ) +add_api_resource( + OrganizationCustomWidgetListResource, + "/organizations//custom-widgets", + "api_v1_organization_custom_widget_list", +) diff --git a/project/forms/event_date.py b/project/forms/event_date.py index c47a3ac..d389885 100644 --- a/project/forms/event_date.py +++ b/project/forms/event_date.py @@ -29,6 +29,7 @@ class FindEventDateForm(FlaskForm): choices=distance_choices, ) event_list_id = HiddenField(validators=[Optional()]) + organization_id = HiddenField(validators=[Optional()]) s_ft = HiddenField(validators=[Optional()]) s_bg = HiddenField(validators=[Optional()]) s_pr = HiddenField(validators=[Optional()]) diff --git a/project/models.py b/project/models.py index 7dfaff3..8c42a3f 100644 --- a/project/models.py +++ b/project/models.py @@ -27,6 +27,7 @@ from sqlalchemy import ( func, select, ) +from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.event import listens_for from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.ext.hybrid import hybrid_property @@ -443,6 +444,11 @@ class AdminUnit(db.Model, TrackableMixin): cascade="all, delete-orphan", backref=backref("adminunit", lazy=True), ) + custom_widgets = relationship( + "CustomWidget", + cascade="all, delete-orphan", + backref=backref("adminunit", lazy=True), + ) location_id = deferred(db.Column(db.Integer, db.ForeignKey("location.id"))) location = db.relationship( "Location", uselist=False, single_parent=True, cascade="all, delete-orphan" @@ -1143,6 +1149,15 @@ class Analytics(db.Model): created_at = Column(DateTime, default=datetime.datetime.utcnow) +class CustomWidget(db.Model, TrackableMixin): + __tablename__ = "customwidget" + id = Column(Integer(), primary_key=True) + widget_type = Column(Unicode(255), nullable=False) + name = Column(Unicode(255), nullable=False) + admin_unit_id = db.Column(db.Integer, db.ForeignKey("adminunit.id"), nullable=False) + settings = Column(JSONB) + + # Deprecated begin class FeaturedEventReviewStatus(IntEnum): inbox = 1 diff --git a/project/services/admin_unit.py b/project/services/admin_unit.py index a3026a8..2eb0f17 100644 --- a/project/services/admin_unit.py +++ b/project/services/admin_unit.py @@ -8,6 +8,7 @@ from project.models import ( AdminUnitMemberInvitation, AdminUnitMemberRole, AdminUnitRelation, + CustomWidget, EventEventLists, EventList, EventOrganizer, @@ -195,6 +196,16 @@ def get_organizer_query(admin_unit_id, name=None): return query.order_by(func.lower(EventOrganizer.name)) +def get_custom_widget_query(admin_unit_id, name=None): + query = CustomWidget.query.filter(CustomWidget.admin_unit_id == admin_unit_id) + + if name: + like_name = "%" + name + "%" + query = query.filter(CustomWidget.name.ilike(like_name)) + + return query.order_by(func.lower(CustomWidget.name)) + + def get_place_query(admin_unit_id, name=None): query = EventPlace.query.filter(EventPlace.admin_unit_id == admin_unit_id) diff --git a/project/services/event_search.py b/project/services/event_search.py index d8ac067..08f9534 100644 --- a/project/services/event_search.py +++ b/project/services/event_search.py @@ -127,3 +127,6 @@ class EventSearchParams(object): if "sort" in request.args: self.sort = request.args["sort"] + + if "organization_id" in request.args: + self.admin_unit_id = request.args["organization_id"] diff --git a/project/static/vue/common/typeahead.vue.js b/project/static/vue/common/typeahead.vue.js index 626c6b7..8692812 100644 --- a/project/static/vue/common/typeahead.vue.js +++ b/project/static/vue/common/typeahead.vue.js @@ -5,16 +5,17 @@ const CustomTypeahead = { :name="label" :detectInput="false" ref="validationProvider" - rules="required" + :rules="rules" v-slot="validationContext"> ({ query: '', @@ -45,7 +61,7 @@ const CustomTypeahead = { }), computed: { label() { - return this.$t(this.labelKey) + return this.labelValue != null ? this.labelValue : this.$t(this.labelKey); }, }, methods: { @@ -57,7 +73,7 @@ const CustomTypeahead = { return ""; } - return valid ? "is-valid" : "is-invalid"; + return valid ? this.validClass : this.invalidClass; }, fetchData(query) { const vm = this @@ -74,7 +90,16 @@ const CustomTypeahead = { }, }, mounted() { - this.$refs.validationProvider.syncValue(this.selected) + this.$refs.validationProvider.syncValue(this.selected); + + this.$watch( + "$refs.typeahead.isFocused", + (new_value, old_value) => { + if (new_value && this.$refs.typeahead.showOnFocus && this.query == "") { + this.fetchData(this.query); + } + } + ); }, watch: { selected(newVal) { diff --git a/project/static/vue/widget-configurator/configurator.vue.js b/project/static/vue/widget-configurator/configurator.vue.js new file mode 100644 index 0000000..ae9cac8 --- /dev/null +++ b/project/static/vue/widget-configurator/configurator.vue.js @@ -0,0 +1,571 @@ +const WidgetConfigurator = { + template: ` + + + +

{{ $t("comp.title") }}

+
+ + + + + + + + + + {{ $t("shared.cancel") }} + + + {{ $t("shared.save") }} + + + + +
+ + + + + + +
+ + + + + + + + + + + {{ $t("comp.search.showFilter") }} + + + {{ $t("comp.search.pagination") }} + + + {{ $t("comp.search.showOvedaLink") }} + + + {{ $t("comp.search.printButton") }} + + + + + + + + + +
+ +
+ + + + + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + +
+
+
+
+
+
+ + + + + + + + + + + + +
+
{{ $t("comp.preview") }}
+ +
+
+
+
+
+
+
+ `, + i18n: { + messages: { + en: { + comp: { + title: "Widget configurator", + successMessage: "Widget successfully saved", + preview: "Preview", + tabSettings: "Serrings", + tabStyles: "Styles", + generic: { + fontFamily: "Font family", + padding: "Padding", + backgroundColor: "Background color", + textColor: "Text color", + linkColor: "Link color", + borderColor: "Border color", + color: "Color", + disabled: "Deactivated", + active: "Activ", + }, + calendar: { + iFrameHeight: "Height", + eventBackgroundColor: "Event color", + calendarType: "Calendar type", + }, + search: { + iFrameMinHeight: "Min. Height", + iFrameMaxHeight: "Max. Height", + eventListId: "Event list", + view: "Display", + showFilter: "Filter", + showOvedaLink: "Oveda link", + layout: "Layout", + eventsPerPage: "Events per page", + event: "Event", + eventName: "Event name", + eventDate: "Event datum", + eventInfo: "Event info", + eventBadgeWarning: "Event warning badge", + eventBadgeInfo: "Event info badge", + filterLabel: "Filter label", + filterInput: "Filter input", + filterButton: "Filter button", + pagination: "Pagination", + printButton: "Print button", + }, + }, + }, + de: { + comp: { + title: "Widget Konfigurator", + successMessage: "Widget erfolgreich gespeichert", + preview: "Vorschau", + tabSettings: "Einstellungen", + tabStyles: "Styles", + generic: { + fontFamily: "Schriftart", + padding: "Abstand", + backgroundColor: "Hintergrundfarbe", + textColor: "Textfarbe", + linkColor: "Link-Farbe", + borderColor: "Rahmen-Farbe", + color: "Farbe", + disabled: "Deaktiviert", + active: "Aktiv", + }, + calendar: { + iFrameHeight: "Höhe", + eventBackgroundColor: "Event Farbe", + calendarType: "Kalender Typ", + }, + search: { + iFrameMinHeight: "Min. Höhe", + iFrameMaxHeight: "Max. Höhe", + eventListId: "Veranstaltungsliste", + view: "Anzeige", + showFilter: "Filter", + showOvedaLink: "Oveda-Link", + layout: "Layout", + eventsPerPage: "Events pro Seite", + event: "Event", + eventName: "Event-Name", + eventDate: "Event-Datum", + eventInfo: "Event-Info", + eventBadgeWarning: "Event Warnungszeichen", + eventBadgeInfo: "Event Infozeichen", + filterLabel: "Filter-Label", + filterInput: "Filter-Eingabe", + filterButton: "Filter-Button", + pagination: "Paginierung", + printButton: "Drucken-Button", + }, + }, + }, + }, + }, + data: () => ({ + iFrameActive: true, + iFrameCounter: 0, + previewSize: '100%', + previewSizes: [ + { text: 'Desktop', value: '100%' }, + { text: 'Tablet', value: '768px' }, + { text: 'Mobile', value: '400px' }, + ], + previewBackgroundColor: '#f8f9fa', + isLoading: false, + isSubmitting: false, + customWidget: null, + form: { + search: { + layout: "card", + iFrameMinHeight: 400, + iFrameMaxHeight: "Infinity", + iFrameAutoResize: true, + organizationId: null, + eventListId: null, + eventsPerPage: 10, + showFilter: true, + showPagination: true, + showPrintButton: false, + showOvedaLink: true, + fontFamily: "", + background: "#ffffff", + textColor: "#212529", + padding: "1rem", + linkColor: "#007bff", + buttonBackgroundColor: "#007bff", + buttonBorderColor: "#007bff", + buttonTextColor: "#ffffff", + printButtonBackgroundColor: "#6c757d", + printButtonBorderColor: "#6c757d", + printButtonTextColor: "#ffffff", + filterLabelBackgroundColor: "#e9ecef", + filterLabelBorderColor: "#ced4da", + filterLabelTextColor: "#495057", + filterInputBackgroundColor: "#ffffff", + filterInputBorderColor: "#ced4da", + filterInputTextColor: "#495057", + eventBackgroundColor: "#ffffff", + eventBorderColor: "#00000020", + eventNameTextColor: "#000000", + eventDateTextColor: "#212529", + eventInfoTextColor: "#6c757d", + pagingBorderColor: "#dee2e6", + pagingColor: "#007bff", + pagingTextColor: "#007bff", + pagingActiveTextColor: "#ffffff", + pagingDisabledTextColor: "#6c757d", + eventBadgeWarningBackgroundColor: "#ffc107", + eventBadgeWarningTextColor: "#212529", + eventBadgeInfoBackgroundColor: "#17a2b8", + eventBadgeInfoTextColor: "#ffffff", + }, + calendar: { + iFrameHeight: 600, + iFrameAutoResize: false, + organizationId: null, + eventListId: null, + calendarType: "week", + eventBackgroundColor: "#007bff", + } + }, + calendarTypes: [ + { value: "week", text: 'Woche' }, + { value: 'month', text: 'Monat' }, + ], + searchLayouts: [ + { value: "card", text: 'Karten' }, + { value: 'text', text: 'Text' }, + ], + widgetType: "search", + widgetTypes: [], + name: "Widget", + searchEventList: null, + }), + computed: { + iFrameSource() { + return `${window.location.origin}/static/widget/${this.widgetType}.html`; + }, + organizationId() { + return this.$route.params.organization_id; + }, + customWidgetId() { + return this.$route.params.custom_widget_id; + }, + settings() { + return this.form[this.widgetType]; + }, + iFrameResizerOptions() { + return { + autoResize: this.settings.iFrameAutoResize, + minHeight: this.settings.iFrameMinHeight != null ? this.settings.iFrameMinHeight : this.settings.iFrameHeight, + maxHeight: this.settings.iFrameMaxHeight != null ? this.settings.iFrameMaxHeight : this.settings.iFrameHeight, + scrolling: "omit", + }; + }, + eventListFetchUrl() { + return `/api/v1/organizations/${this.organizationId}/event-lists?name={query}`; + }, + }, + mounted() { + this.isLoading = false; + this.customWidget = null; + this.widgetTypes = [ + { value: "search", text: this.$t("shared.models.customWidget.widgetTypeSearch") }, + { value: "calendar", text: this.$t("shared.models.customWidget.widgetTypeCalendar") }, + ] + this.form.search.organizationId = this.organizationId; + this.form.calendar.organizationId = this.organizationId; + + if (this.customWidgetId == null) { + this.initResizer(); + } else { + this.loadFormData(); + } + }, + watch: { + settings: { + handler: function (val, oldVal) { + this.updatePreview(); + }, + deep: true + }, + iFrameResizerOptions: { + handler: function (val, oldVal) { + this.reloadIframe(); + }, + deep: true + }, + searchEventList: function(val) { + this.form.search.eventListId = val != null ? val.id : null; + }, + previewSize: function(val) { + this.resizePreview(); + } + }, + methods: { + reloadIframe() { + this.iFrameActive = false; + this.iFrameCounter++; + this.iFrameActive = true; + this.initResizer(); + }, + initResizer() { + const vm = this; + Vue.nextTick(function () { + iFrameResize({ + autoResize: vm.iFrameResizerOptions.autoResize, + minHeight: vm.iFrameResizerOptions.minHeight, + maxHeight: vm.iFrameResizerOptions.maxHeight, + scrolling: vm.iFrameResizerOptions.scrolling, + onMessage: function(m) {}, + onInit: function() { vm.updatePreview() }, + }, + '.preview_iframe'); + }); + }, + updatePreview() { + const resizer = this.$refs.previewIframe.iFrameResizer; + + if (resizer === undefined) { + return; + } + + resizer.sendMessage({'type': 'OVEDA_WIDGET_SETTINGS_UPDATE_EVENT', 'data': this.settings}); + }, + resizePreview() { + const resizer = this.$refs.previewIframe.iFrameResizer; + + if (resizer === undefined) { + return; + } + + resizer.resize(); + }, + loadFormData() { + axios + .get(`/api/v1/custom-widgets/${this.customWidgetId}`, { + withCredentials: true, + handleLoading: this.handleLoading, + }) + .then((response) => { + this.customWidget = response.data; + this.widgetType = this.customWidget.widget_type; + this.name = this.customWidget.name; + + for (var key in this.customWidget.settings) { + this.settings[key] = this.customWidget.settings[key]; + } + }); + }, + handleLoading(isLoading) { + this.isLoading = isLoading; + }, + submitForm() { + let data = { + 'widget_type': this.widgetType, + 'name': this.name, + 'settings': this.settings, + }; + + if (this.customWidgetId == null) { + axios + .post(`/api/v1/organizations/${this.organizationId}/custom-widgets`, + data, + { + withCredentials: true, + handleLoading: this.handleSubmitting, + }) + .then(() => { + this.$root.makeSuccessToast(this.$t("comp.successMessage")) + this.goBack() + }) + } else { + axios + .put(`/api/v1/custom-widgets/${this.customWidgetId}`, + data, + { + withCredentials: true, + handleLoading: this.handleSubmitting, + }) + .then(() => { + this.$root.makeSuccessToast(this.$t("comp.successMessage")) + this.goBack() + }) + } + }, + handleSubmitting(isLoading) { + this.isSubmitting = isLoading; + }, + goBack() { + window.location.href = `/manage/admin_unit/${this.organizationId}/custom-widgets`; + }, + }, +}; diff --git a/project/static/vue/widget-configurator/list.vue.js b/project/static/vue/widget-configurator/list.vue.js new file mode 100644 index 0000000..e47a195 --- /dev/null +++ b/project/static/vue/widget-configurator/list.vue.js @@ -0,0 +1,170 @@ +const WidgetConfiguratorList = { + template: ` +
+

{{ $t("shared.models.customWidget.listName") }}

+ +
+ {{ $t("comp.addTitle") }} +
+ + + + +

Kopiere den unten stehenden Code und füge ihn auf deiner Website ein.

+

Füge den folgenden Code im <head> der Seite ein.

+ + +

Füge den folgenden Code an der Stelle im <body> der Seite ein, wo das Widget dargestellt werden soll.

+ +
+ + + + + + +
+ `, + i18n: { + messages: { + en: { + comp: { + addTitle: "Add widget", + installation: "Installation", + deletedMessage: "Widget successfully deleted", + deleteConfirmation: "Do you really want to delete the widget?", + }, + }, + de: { + comp: { + addTitle: "Widget hinzufügen", + installation: "Installation", + deletedMessage: "Widget erfolgreich gelöscht", + deleteConfirmation: "Möchtest du das Widget wirklich löschen?", + }, + }, + }, + }, + data: () => ({ + errorMessage: null, + fields: [ + { + key: "name", + label: i18n.t("shared.models.customWidget.name"), + }, + { + key: "widget_type", + label: i18n.t("shared.models.customWidget.widgetType"), + }, + ], + totalRows: 0, + currentPage: 1, + perPage: 10, + searchResult: { + items: [], + }, + widgetTypes: { + "search": "Search", + "calendar": "Calendar", + }, + installationWidgetId: 0, + }), + computed: { + organizationId() { + return this.$route.params.organization_id; + }, + installationHeader() { + return "\n\n"; + }, + installationBody() { + return '
'; + } + }, + mounted() { + this.widgetTypes["search"] = this.$t("shared.models.customWidget.widgetTypeSearch"); + this.widgetTypes["calendar"] = this.$t("shared.models.customWidget.widgetTypeCalendar"); + }, + methods: { + loadTableData(ctx, callback) { + const vm = this; + axios + .get(`/api/v1/organizations/${this.organizationId}/custom-widgets`, { + params: { + page: ctx.currentPage, + per_page: ctx.perPage, + }, + withCredentials: true, + handler: this, + }) + .then((response) => { + vm.totalRows = response.data.total; + callback(response.data.items); + }) + .catch(() => { + callback([]); + }); + return null; + }, + refreshTableData() { + this.$refs.table.refresh(); + }, + handleRequestStart() { + this.errorMessage = null; + }, + handleRequestError(error, message) { + this.errorMessage = message; + }, + createItem() { + window.location.href = `/manage/admin_unit/${this.organizationId}/custom-widgets/create`; + }, + editItem(id) { + window.location.href = `/manage/admin_unit/${this.organizationId}/custom-widgets/${id}/update`; + }, + deleteItem(id) { + if (confirm(this.$t("comp.deleteConfirmation"))) { + axios + .delete(`/api/v1/custom-widgets/${id}`, { + withCredentials: true, + }) + .then(() => { + this.$root.makeSuccessToast(this.$t("comp.deletedMessage")); + this.refreshTableData(); + }); + } + }, + installItem(id) { + this.installationWidgetId = id; + this.$bvModal.show("custom-widget-installation-modal"); + }, + }, +}; diff --git a/project/static/widget-loader.js b/project/static/widget-loader.js index bb882e2..c8cff12 100644 --- a/project/static/widget-loader.js +++ b/project/static/widget-loader.js @@ -3,7 +3,7 @@ var script = document.currentScript || document.querySelector('script[src*="widget-loader.js"]') var baseUrl = script.src.replace("/static/widget-loader.js", ""); - var iFrameElements = []; + var containers = []; var needsResizer = false; w.onload = function() { @@ -17,30 +17,69 @@ function initWidgets() { var elements = d.getElementsByClassName("oveda-widget"); for (var i = 0; i < elements.length; i++) { - initIframeWidget(elements.item(i)); + initIframeWidget(elements.item(i), i); } } - function initIframeWidget(element) { - var shortName = getWidgetData(element, 'short-name'); - var width = getWidgetData(element, 'width', '100%'); - var height = getWidgetData(element, 'height', '400px'); - var resize = getWidgetBoolData(element, 'resize', false); - var googleTagManager = getWidgetBoolData(element, 'google-tag-manager', false); + function initIframeWidget(element, index) { + var customId = getWidgetData(element, 'id'); + var customWidgetData = null; + var src = null; + var resize = true; + var minHeight = "400"; + var maxHeight = "Infinity"; + var width = "100%"; + var googleTagManager = false; - var style = "border: none; width:" + width + ";height:" + height + ";"; - if (resize) { + if (customId != null) { + var url = baseUrl + "/api/v1/custom-widgets/" + customId; + + customWidgetData = loadJSON(url); + var settings = customWidgetData.settings; + src = baseUrl + "/static/widget/" + customWidgetData.widget_type + ".html"; + + if (settings.hasOwnProperty('iFrameAutoResize') && settings.iFrameAutoResize != null) { + resize = settings.iFrameAutoResize; + } + + if (settings.hasOwnProperty('iFrameHeight') && settings.iFrameHeight != null) { + minHeight = settings.iFrameHeight; + maxHeight = settings.iFrameHeight; + } + + if (settings.hasOwnProperty('iFrameMinHeight') && settings.iFrameMinHeight != null) { + minHeight = settings.iFrameMinHeight; + } + + if (settings.hasOwnProperty('iFrameMaxHeight') && settings.iFrameMaxHeight != null) { + maxHeight = settings.iFrameMaxHeight; + } + + if (settings.hasOwnProperty('googleTagManager') && settings.googleTagManager != null) { + googleTagManager = settings.googleTagManager; + } + } else { + width = getWidgetData(element, 'width', '100%'); + minHeight = getWidgetData(element, 'height', '400px'); + resize = getWidgetBoolData(element, 'resize', false); + googleTagManager = getWidgetBoolData(element, 'google-tag-manager', false); + + var shortName = getWidgetData(element, 'short-name'); + var src = baseUrl + "/" + shortName + "/widget/eventdates?"; + src = addParamToQuery(element, src, 'event-list', 'event_list_id'); + src = addParamToQuery(element, src, 'font', 's_ft'); + src = addParamToQuery(element, src, 'background', 's_bg'); + src = addParamToQuery(element, src, 'primary', 's_pr'); + src = addParamToQuery(element, src, 'link', 's_li'); + } + + var style = "border: none; width:" + width + ";height:" + minHeight + ";"; + if (resize || customWidgetData != null) { style += "min-width:100%;max-width:100%;"; } - var src = baseUrl + "/" + shortName + "/widget/eventdates?"; - src = addParamToQuery(element, src, 'event-list', 'event_list_id'); - src = addParamToQuery(element, src, 'font', 's_ft'); - src = addParamToQuery(element, src, 'background', 's_bg'); - src = addParamToQuery(element, src, 'primary', 's_pr'); - src = addParamToQuery(element, src, 'link', 's_li'); - var iFrame = d.createElement("iframe"); + iFrame.id = "oveda-widget-iframe-" + index; iFrame.class = "oveda-widget-iframe"; iFrame.src = src; iFrame.style = style; @@ -48,11 +87,20 @@ iFrame.allowtransparency = "true"; element.appendChild(iFrame); - if (resize || googleTagManager) { + if (resize || googleTagManager || customWidgetData != null) { needsResizer = true; } - iFrameElements.push(element); + var container = { + element: element, + iFrame: iFrame, + minHeight: minHeight, + maxHeight: maxHeight, + resize: resize, + googleTagManager: googleTagManager, + customWidgetData: customWidgetData + }; + containers.push(container); } function addParamToQuery(element, url, attr, param) @@ -85,29 +133,40 @@ } function startIframeResizer() { - for (var i = 0; i < iFrameElements.length; i++) { - var element = iFrameElements[i]; - var iFrame = element.getElementsByTagName("iframe")[0]; - var height = getWidgetData(element, 'height', '400px'); - var resize = getWidgetBoolData(element, 'resize', false); - var googleTagManager = getWidgetBoolData(element, 'google-tag-manager', false); + for (var i = 0; i < containers.length; i++) { + var container = containers[i]; - if (resize || googleTagManager) { - var config = { autoResize: resize }; - - if (resize) { - config.minHeight = height; - } else { - config.scrolling = "omit"; - config.sizeHeight = false; - config.sizeWidth = false; - } + if (container.resize || container.googleTagManager || container.customWidgetData != null) { + var config = { }; + config.autoResize = container.resize; + config.scrolling = "omit"; + config.id = "iFrameResizer" + i; config.onMessage = function(messageData) { - onIframeMessage(messageData, googleTagManager); + onIframeMessage(messageData, container.googleTagManager); } - iFrameResize(config, iFrame); + if (container.customWidgetData == null) { + if (container.resize) { + config.minHeight = container.minHeight; + config.maxHeight = container.maxHeight; + } else { + config.sizeHeight = false; + config.sizeWidth = false; + } + } else { + config.minHeight = container.minHeight; + config.maxHeight = container.maxHeight; + + (function(data) { + config.onInit = function (iFrame) { + iFrame.iFrameResizer.sendMessage({'type': 'OVEDA_WIDGET_SETTINGS_UPDATE_EVENT', 'data': data}); + } + })(container.customWidgetData.settings); + } + + var resizers = iFrameResize(config, container.iFrame); + container.resizer = resizers[0]; } } }; @@ -144,4 +203,21 @@ p.parentNode.insertBefore(element, p); } + function loadJSON(url) { + var json = loadUrlSync(url, "application/json"); + return JSON.parse(json); + } + + function loadUrlSync(url, mimeType) + { + var xmlhttp=new XMLHttpRequest(); + xmlhttp.open("GET", url, false); + if (mimeType != null && xmlhttp.overrideMimeType) { + xmlhttp.overrideMimeType(mimeType); + } + + xmlhttp.send(); + return xmlhttp.responseText; + } + })(window, document); \ No newline at end of file diff --git a/project/static/widget/calendar.html b/project/static/widget/calendar.html new file mode 100644 index 0000000..ba0d9e3 --- /dev/null +++ b/project/static/widget/calendar.html @@ -0,0 +1,273 @@ + + + + + + + + + + +
+ +
+ + + + + + + + + \ No newline at end of file diff --git a/project/static/widget/search.html b/project/static/widget/search.html new file mode 100644 index 0000000..97d4bdd --- /dev/null +++ b/project/static/widget/search.html @@ -0,0 +1,525 @@ + + + + + + Oveda Widget + + + + + + + + + + + + + +
+ +
+ + + + + diff --git a/project/templates/layout.html b/project/templates/layout.html index 46a35e1..7d3f9fa 100644 --- a/project/templates/layout.html +++ b/project/templates/layout.html @@ -269,6 +269,7 @@ {{ _('Organization invitations') }} {% endif %} {{ _('Settings') }} + {{ _('Custom widgets') }} {{ _('Widgets') }} {{ _('Profile') }} diff --git a/project/templates/layout_vue.html b/project/templates/layout_vue.html index 7da4ee4..7938115 100644 --- a/project/templates/layout_vue.html +++ b/project/templates/layout_vue.html @@ -35,7 +35,7 @@ {% block content %} {% block vue_container %} -
+
{% endblock %} +{%- endblock -%} + +{% block component_scripts %} + + +{% endblock %} + +{% block component_definitions %} +Vue.component("WidgetConfiguratorList", WidgetConfiguratorList); +Vue.component("WidgetConfigurator", WidgetConfigurator); +{% endblock %} + +{% block vue_routes %} +const routes = [ + { + path: "/manage/admin_unit/:organization_id/custom-widgets", + component: WidgetConfiguratorList, + }, + { + path: "/manage/admin_unit/:organization_id/custom-widgets/create", + component: WidgetConfigurator, + }, + { + path: "/manage/admin_unit/:organization_id/custom-widgets/:custom_widget_id/update", + component: WidgetConfigurator, + }, +]; +{% endblock %} diff --git a/project/translations/de/LC_MESSAGES/messages.mo b/project/translations/de/LC_MESSAGES/messages.mo index 7415cdd34d465014459629c47ce9110491e0c4fc..d47d40ef48b11c38f93e4bcd1bf49749ecd42c8d 100644 GIT binary patch delta 7665 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 s?&>KfXhI^>}SD0KCQQSXgJ zb)4wxvs`^4s^2rHelMbSz6sTDCn_@Uq9St?wc(Q zMZJFtwUG0u2`{7ieTz!oTc~9A8)-jm4N&jLBe%>mDHOEg_U=J9)XIubSv>?b@IkDO zAD{+4idy*R_yT^3?Q!-ft{(2e2^h;Dy|E1SK|6=uNcKdv55#!pH>E1zTGW7V zp(1h?70N59?7r@v`;E8vHv#L@-U>BvFVwYq1OspqYT?r{6}O_sJ%f7xEA$k?>#pG@ z)}($H^=0!fw%0Bav#7U4JugA^n}I<%2Q~0w)X|lrvVA@3{Wnk>+kpvq9QEF>KM)(b;-~%Q0{D?nEW|gk8G1jthQ!HsWs^~7HXqe-DQch0uMgynVaZ@lz_rEy>g`_QpVi!~-`k_Ksg8FIq9O{<5fq4`Wj- zm~MZXPD3qt1u6ntobO^3^cpbI#UtB$Wh7D;9>L?PBkeM{pk+j6x*bO6b z7}mlmn2w7v3wNN#{TgfIJ=EDp&LsZYQ7#3Ax)}Asv#8$(UPcXY0Cl!MIDf`y>c69| zQ`9VeXTWS!zs0D?Y((~Aj^jw|JllS0D^cS{mJ)y6)6S*#?2A$NbvA0kji_XN4HIxH zcE;nbJ$jD4hH+Sr_H<`~b2w_8QdERJSAQB6(Uo(Eze2apHT(^=vt6ic{Rr#eHPit2 zoxyYMf}$~s_H0zhdte&Q!6K|cZRj>G#Rl{2KeE1zEvcXNC=8N8Q}Z^CRmhVfcL zz+yW>RS|tAU4`{tp1r Bd!zsW diff --git a/project/translations/de/LC_MESSAGES/messages.po b/project/translations/de/LC_MESSAGES/messages.po index fb53f73..15c3eab 100644 --- a/project/translations/de/LC_MESSAGES/messages.po +++ b/project/translations/de/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2021-12-08 13:32+0100\n" +"POT-Creation-Date: 2021-12-30 14:51+0100\n" "PO-Revision-Date: 2020-06-07 18:51+0200\n" "Last-Translator: FULL NAME \n" "Language: de\n" @@ -194,29 +194,29 @@ msgstr "." msgid "message" msgstr "message" -#: project/api/organization/resources.py:364 +#: project/api/organization/resources.py:371 #: 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:311 +#: project/forms/admin.py:10 project/templates/layout.html:312 #: project/views/root.py:37 msgid "Terms of service" msgstr "Nutzungsbedingungen" -#: project/forms/admin.py:11 project/templates/layout.html:315 +#: project/forms/admin.py:11 project/templates/layout.html:316 #: project/views/root.py:45 msgid "Legal notice" msgstr "Impressum" #: project/forms/admin.py:12 project/templates/_macros.html:1392 -#: project/templates/layout.html:319 +#: project/templates/layout.html:320 #: 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:323 +#: project/forms/admin.py:13 project/templates/layout.html:324 #: project/views/root.py:61 msgid "Privacy" msgstr "Datenschutz" @@ -957,7 +957,7 @@ msgstr "Standort" msgid "Distance" msgstr "Distanz" -#: project/forms/event_date.py:33 project/forms/planing.py:36 +#: project/forms/event_date.py:38 project/forms/planing.py:36 #: project/templates/widget/event_date/list.html:82 msgid "Find" msgstr "Finden" @@ -1388,7 +1388,7 @@ msgid "Manage" msgstr "Verwaltung" #: project/templates/home.html:30 project/templates/security/login_user.html:38 -#: project/views/widget.py:158 +#: project/views/widget.py:159 msgid "Register for free" msgstr "Kostenlos registrieren" @@ -1412,7 +1412,7 @@ msgstr "Organisationen" msgid "Planing" msgstr "Planung" -#: project/templates/layout.html:187 project/templates/layout.html:273 +#: project/templates/layout.html:187 project/templates/layout.html:274 #: 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 @@ -1514,17 +1514,22 @@ msgstr "Organisationseinladungen" msgid "Settings" msgstr "Einstellungen" -#: project/templates/layout.html:272 project/templates/manage/reviews.html:10 +#: project/templates/layout.html:272 +#: 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/manage/widgets.html:5 #: project/templates/manage/widgets.html:9 msgid "Widgets" msgstr "Widgets" -#: project/templates/layout.html:283 +#: project/templates/layout.html:284 msgid "Switch organization" msgstr "Organisation wechseln" -#: project/templates/developer/read.html:4 project/templates/layout.html:333 +#: project/templates/developer/read.html:4 project/templates/layout.html:334 #: project/templates/profile.html:29 msgid "Developer" msgstr "Entwickler" @@ -1997,7 +2002,7 @@ msgstr "Vorschau" msgid "Organization successfully updated" msgstr "Organisation erfolgreich aktualisiert" -#: project/views/admin.py:68 project/views/manage.py:331 +#: project/views/admin.py:68 project/views/manage.py:346 msgid "Settings successfully updated" msgstr "Einstellungen erfolgreich aktualisiert" @@ -2058,27 +2063,27 @@ msgstr "Die eingegebene Email passt nicht zur Email der Einladung" msgid "Invitation successfully deleted" msgstr "Einladung erfolgreich gelöscht" -#: project/views/event.py:179 +#: project/views/event.py:178 msgid "Event successfully published" msgstr "Veranstaltung erfolgreich veröffentlicht" -#: project/views/event.py:181 +#: project/views/event.py:180 msgid "Draft successfully saved" msgstr "Entwurf erfolgreich gespeichert" -#: project/views/event.py:224 +#: project/views/event.py:223 msgid "Event successfully updated" msgstr "Veranstaltung erfolgreich aktualisiert" -#: project/views/event.py:250 +#: project/views/event.py:249 msgid "Event successfully deleted" msgstr "Veranstaltung erfolgreich gelöscht" -#: project/views/event.py:409 +#: project/views/event.py:408 msgid "Referenced event changed" msgstr "Empfohlene Veranstaltung wurde geändert" -#: project/views/event.py:432 +#: project/views/event.py:431 msgid "New event report" msgstr "Neue Meldung zu einer Veranstaltung" @@ -2242,11 +2247,11 @@ msgstr "" "Die Einladung wurde für einen anderen Nutzer ausgestellt. Melde dich mit " "der Email-Adresse an, an die die Einladung geschickt wurde." -#: project/views/widget.py:150 +#: project/views/widget.py:151 msgid "Thank you so much! The event is being verified." msgstr "Vielen Dank! Die Veranstaltung wird geprüft." -#: project/views/widget.py:154 +#: project/views/widget.py:155 msgid "" "For more options and your own calendar of events, you can register for " "free." @@ -2254,7 +2259,7 @@ msgstr "" "Für mehr Optionen und einen eigenen Veranstaltungskalender, kannst du " "dich kostenlos registrieren." -#: project/views/widget.py:212 +#: project/views/widget.py:221 msgid "New event review" msgstr "Neue Veranstaltung zu prüfen" diff --git a/project/translations/en/LC_MESSAGES/messages.mo b/project/translations/en/LC_MESSAGES/messages.mo index 2f422ea8edcb3779c26aa32e67a159c290aafa1b..eb4e5cd94747c46d79e6335a01805127c46c99b2 100644 GIT binary patch delta 21 ccmaDW{Z@LzEKUw%0|i49D^tVGi#WYm091emp#T5? delta 21 ccmaDW{Z@LzEKUvs3k5@CD`TV0i#WYm092?3qyPW_ diff --git a/project/translations/en/LC_MESSAGES/messages.po b/project/translations/en/LC_MESSAGES/messages.po index 13a4a4e..e6599bd 100644 --- a/project/translations/en/LC_MESSAGES/messages.po +++ b/project/translations/en/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2021-12-08 13:32+0100\n" +"POT-Creation-Date: 2021-12-30 14:51+0100\n" "PO-Revision-Date: 2021-04-30 15:04+0200\n" "Last-Translator: FULL NAME \n" "Language: en\n" @@ -194,29 +194,29 @@ msgstr "" msgid "message" msgstr "" -#: project/api/organization/resources.py:364 +#: project/api/organization/resources.py:371 #: project/views/admin_unit_member_invitation.py:85 msgid "You have received an invitation" msgstr "" -#: project/forms/admin.py:10 project/templates/layout.html:311 +#: project/forms/admin.py:10 project/templates/layout.html:312 #: project/views/root.py:37 msgid "Terms of service" msgstr "" -#: project/forms/admin.py:11 project/templates/layout.html:315 +#: project/forms/admin.py:11 project/templates/layout.html:316 #: project/views/root.py:45 msgid "Legal notice" msgstr "" #: project/forms/admin.py:12 project/templates/_macros.html:1392 -#: project/templates/layout.html:319 +#: project/templates/layout.html:320 #: 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:323 +#: project/forms/admin.py:13 project/templates/layout.html:324 #: project/views/root.py:61 msgid "Privacy" msgstr "" @@ -918,7 +918,7 @@ msgstr "" msgid "Distance" msgstr "" -#: project/forms/event_date.py:33 project/forms/planing.py:36 +#: project/forms/event_date.py:38 project/forms/planing.py:36 #: project/templates/widget/event_date/list.html:82 msgid "Find" msgstr "" @@ -1343,7 +1343,7 @@ msgid "Manage" msgstr "" #: project/templates/home.html:30 project/templates/security/login_user.html:38 -#: project/views/widget.py:158 +#: project/views/widget.py:159 msgid "Register for free" msgstr "" @@ -1367,7 +1367,7 @@ msgstr "" msgid "Planing" msgstr "" -#: project/templates/layout.html:187 project/templates/layout.html:273 +#: project/templates/layout.html:187 project/templates/layout.html:274 #: 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 @@ -1469,17 +1469,22 @@ msgstr "" msgid "Settings" msgstr "" -#: project/templates/layout.html:272 project/templates/manage/reviews.html:10 +#: project/templates/layout.html:272 +#: project/templates/manage/custom_widgets.html:13 +msgid "Custom widgets" +msgstr "" + +#: project/templates/layout.html:273 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:283 +#: project/templates/layout.html:284 msgid "Switch organization" msgstr "" -#: project/templates/developer/read.html:4 project/templates/layout.html:333 +#: project/templates/developer/read.html:4 project/templates/layout.html:334 #: project/templates/profile.html:29 msgid "Developer" msgstr "" @@ -1946,7 +1951,7 @@ msgstr "" msgid "Organization successfully updated" msgstr "" -#: project/views/admin.py:68 project/views/manage.py:331 +#: project/views/admin.py:68 project/views/manage.py:346 msgid "Settings successfully updated" msgstr "" @@ -2004,27 +2009,27 @@ msgstr "" msgid "Invitation successfully deleted" msgstr "" -#: project/views/event.py:179 +#: project/views/event.py:178 msgid "Event successfully published" msgstr "" -#: project/views/event.py:181 +#: project/views/event.py:180 msgid "Draft successfully saved" msgstr "" -#: project/views/event.py:224 +#: project/views/event.py:223 msgid "Event successfully updated" msgstr "" -#: project/views/event.py:250 +#: project/views/event.py:249 msgid "Event successfully deleted" msgstr "" -#: project/views/event.py:409 +#: project/views/event.py:408 msgid "Referenced event changed" msgstr "" -#: project/views/event.py:432 +#: project/views/event.py:431 msgid "New event report" msgstr "" @@ -2180,17 +2185,17 @@ msgid "" " the invitation was sent to." msgstr "" -#: project/views/widget.py:150 +#: project/views/widget.py:151 msgid "Thank you so much! The event is being verified." msgstr "" -#: project/views/widget.py:154 +#: project/views/widget.py:155 msgid "" "For more options and your own calendar of events, you can register for " "free." msgstr "" -#: project/views/widget.py:212 +#: project/views/widget.py:221 msgid "New event review" msgstr "" diff --git a/project/views/manage.py b/project/views/manage.py index de71e45..d648d59 100644 --- a/project/views/manage.py +++ b/project/views/manage.py @@ -291,6 +291,21 @@ def manage_admin_unit_event_lists(id, path=None): ) +@app.route("/manage/admin_unit//custom-widgets") +@app.route("/manage/admin_unit//custom-widgets/") +@auth_required() +def manage_admin_unit_custom_widgets(id, path=None): + admin_unit = get_admin_unit_for_manage_or_404(id) + set_current_admin_unit(admin_unit) + full_height = path is not None and (path == "create" or path.endswith("/update")) + + return render_template( + "manage/custom_widgets.html", + admin_unit=admin_unit, + full_height=full_height, + ) + + @app.route("/manage/admin_unit//widgets", methods=("GET", "POST")) @auth_required() def manage_admin_unit_widgets(id): diff --git a/tests/api/test_custom_widget.py b/tests/api/test_custom_widget.py new file mode 100644 index 0000000..207a5a8 --- /dev/null +++ b/tests/api/test_custom_widget.py @@ -0,0 +1,55 @@ +def test_read(client, seeder, utils): + _, admin_unit_id = seeder.setup_base() + custom_widget_id = seeder.insert_event_custom_widget(admin_unit_id) + + url = utils.get_url("api_v1_custom_widget", id=custom_widget_id) + response = utils.get_json(url) + utils.assert_response_ok(response) + assert response.json["settings"]["color"] == "black" + + +def test_put(client, seeder, utils, app): + _, admin_unit_id = seeder.setup_api_access() + custom_widget_id = seeder.insert_event_custom_widget(admin_unit_id) + + url = utils.get_url("api_v1_custom_widget", id=custom_widget_id) + response = utils.put_json(url, {"widget_type": "search", "name": "Neuer Name"}) + utils.assert_response_no_content(response) + + with app.app_context(): + from project.models import CustomWidget + + custom_widget = CustomWidget.query.get(custom_widget_id) + assert custom_widget.name == "Neuer Name" + assert custom_widget.widget_type == "search" + + +def test_patch(client, seeder, utils, app): + _, admin_unit_id = seeder.setup_api_access() + custom_widget_id = seeder.insert_event_custom_widget(admin_unit_id) + + url = utils.get_url("api_v1_custom_widget", id=custom_widget_id) + response = utils.patch_json(url, {"name": "Neuer Name"}) + utils.assert_response_no_content(response) + + with app.app_context(): + from project.models import CustomWidget + + custom_widget = CustomWidget.query.get(custom_widget_id) + assert custom_widget.name == "Neuer Name" + assert custom_widget.widget_type == "search" + + +def test_delete(client, seeder, utils, app): + _, admin_unit_id = seeder.setup_api_access() + custom_widget_id = seeder.insert_event_custom_widget(admin_unit_id) + + url = utils.get_url("api_v1_custom_widget", id=custom_widget_id) + response = utils.delete(url) + utils.assert_response_no_content(response) + + with app.app_context(): + from project.models import CustomWidget + + custom_widget = CustomWidget.query.get(custom_widget_id) + assert custom_widget is None diff --git a/tests/api/test_event_date.py b/tests/api/test_event_date.py index 993b958..771179a 100644 --- a/tests/api/test_event_date.py +++ b/tests/api/test_event_date.py @@ -81,6 +81,9 @@ def test_search(client, seeder, utils): url = utils.get_url("api_v1_event_date_search", organizer_id=organizer_id) response = utils.get_ok(url) + url = utils.get_url("api_v1_event_date_search", organization_id=admin_unit_id) + response = utils.get_ok(url) + listed_event_id = seeder.create_event(admin_unit_id) event_list_id = seeder.create_event_list(admin_unit_id, listed_event_id) url = utils.get_url("api_v1_event_date_search", event_list_id=event_list_id) diff --git a/tests/api/test_organization.py b/tests/api/test_organization.py index 31110e1..30d3e31 100644 --- a/tests/api/test_organization.py +++ b/tests/api/test_organization.py @@ -544,3 +544,41 @@ def test_organization_invitation_list_post(client, app, seeder, utils, mocker): id=invitation_id, ) utils.assert_send_mail_called(mail_mock, "invited@test.de", invitation_url) + + +def test_custom_widgets(client, seeder, utils): + _, admin_unit_id = seeder.setup_base() + seeder.insert_event_custom_widget(admin_unit_id) + + url = utils.get_url( + "api_v1_organization_custom_widget_list", id=admin_unit_id, name="mein" + ) + utils.get_ok(url) + + +def test_custom_widgets_post(client, seeder, utils, app): + user_id, admin_unit_id = seeder.setup_api_access() + + url = utils.get_url("api_v1_organization_custom_widget_list", id=admin_unit_id) + response = utils.post_json( + url, + { + "widget_type": "search", + "name": "Neues Widget", + "settings": {"color": "black"}, + }, + ) + utils.assert_response_created(response) + assert "id" in response.json + + with app.app_context(): + from project.models import CustomWidget + + custom_widget = ( + CustomWidget.query.filter(CustomWidget.admin_unit_id == admin_unit_id) + .filter(CustomWidget.name == "Neues Widget") + .first() + ) + assert custom_widget is not None + assert custom_widget.widget_type == "search" + assert custom_widget.settings["color"] == "black" diff --git a/tests/seeder.py b/tests/seeder.py index 2cde408..822376d 100644 --- a/tests/seeder.py +++ b/tests/seeder.py @@ -200,6 +200,28 @@ class Seeder(object): return organizer_id + def insert_event_custom_widget( + self, + admin_unit_id, + widget_type="search", + name="Mein Widget", + settings={"color": "black"}, + ): + from project.models import CustomWidget + + with self._app.app_context(): + custom_widget = CustomWidget() + custom_widget.admin_unit_id = admin_unit_id + custom_widget.widget_type = widget_type + custom_widget.name = name + custom_widget.settings = settings + + self._db.session.add(custom_widget) + self._db.session.commit() + custom_widget_id = custom_widget.id + + return custom_widget_id + def insert_default_oauth2_client(self, user_id): from project.api import scope_list from project.models import OAuth2Client diff --git a/tests/views/test_manage.py b/tests/views/test_manage.py index c3e1b07..43a04b0 100644 --- a/tests/views/test_manage.py +++ b/tests/views/test_manage.py @@ -202,3 +202,10 @@ def test_admin_unit_event_lists(client, seeder, utils): url = utils.get_url("manage_admin_unit_event_lists", id=admin_unit_id) utils.get_ok(url) + + +def test_admin_unit_custom_widgets(client, seeder, utils): + _, admin_unit_id = seeder.setup_base() + + url = utils.get_url("manage_admin_unit_custom_widgets", id=admin_unit_id) + utils.get_ok(url)