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 7415cdd..d47d40e 100644 Binary files a/project/translations/de/LC_MESSAGES/messages.mo and b/project/translations/de/LC_MESSAGES/messages.mo differ 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 2f422ea..eb4e5cd 100644 Binary files a/project/translations/en/LC_MESSAGES/messages.mo and b/project/translations/en/LC_MESSAGES/messages.mo differ 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)