mirror of
https://github.com/lucaspalomodevelop/eventcally.git
synced 2026-03-13 00:07:22 +00:00
Merge pull request #332 from DanielGrams/issue/331
Datenmodell und API für EventDateDefinitions vorbereiten #331
This commit is contained in:
commit
eed987524a
@ -1,5 +1,5 @@
|
||||
describe("Admin Unit", () => {
|
||||
it.skip("creates", () => {
|
||||
it("creates", () => {
|
||||
cy.login();
|
||||
cy.visit("/admin_unit/create");
|
||||
cy.get("#name").type("Second Crew");
|
||||
@ -33,7 +33,7 @@ describe("Admin Unit", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.skip("updates", () => {
|
||||
it("updates", () => {
|
||||
cy.login();
|
||||
cy.createAdminUnit().then(function (adminUnitId) {
|
||||
cy.visit("/admin_unit/" + adminUnitId + "/update");
|
||||
@ -47,7 +47,7 @@ describe("Admin Unit", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.skip("widgets", () => {
|
||||
it("widgets", () => {
|
||||
cy.login();
|
||||
cy.createAdminUnit().then(function (adminUnitId) {
|
||||
cy.visit("/manage/admin_unit/" + adminUnitId + "/widgets");
|
||||
|
||||
@ -6,7 +6,7 @@ describe("Event", () => {
|
||||
cy.visit("/admin_unit/" + adminUnitId + "/events/create");
|
||||
|
||||
cy.get("#name").type("Stadtfest");
|
||||
cy.checkEventStartEnd(false, test.recurrence);
|
||||
cy.checkEventStartEnd(false, test.recurrence, "date_definitions-0-");
|
||||
|
||||
cy.select2("event_place_id", "Neu");
|
||||
cy.get("#new_event_place-location-city").type("Goslar");
|
||||
@ -27,7 +27,7 @@ describe("Event", () => {
|
||||
|
||||
cy.contains("a", "Veranstaltung bearbeiten").click();
|
||||
cy.url().should("include", "/update");
|
||||
cy.checkEventStartEnd(true, test.recurrence);
|
||||
cy.checkEventStartEnd(true, test.recurrence, "date_definitions-0-");
|
||||
cy.get("#submit").click();
|
||||
cy.url().should(
|
||||
"include",
|
||||
|
||||
@ -19,7 +19,7 @@ describe("Manage", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.skip("Events", () => {
|
||||
it("Events", () => {
|
||||
cy.login();
|
||||
cy.createAdminUnit().then(function (adminUnitId) {
|
||||
cy.createEvent(adminUnitId).then(function (eventId) {
|
||||
|
||||
@ -219,70 +219,70 @@ Cypress.Commands.add("inputsShouldHaveSameValue", (input1, input2) => {
|
||||
|
||||
Cypress.Commands.add(
|
||||
"checkEventStartEnd",
|
||||
(update = false, recurrence = false) => {
|
||||
(update = false, recurrence = false, prefix = "") => {
|
||||
if (update && recurrence) {
|
||||
cy.get("#single-event-container").should("not.be.visible");
|
||||
cy.get("#recc-event-container").should("be.visible");
|
||||
cy.get('#' + prefix + 'single-event-container').should("not.be.visible");
|
||||
cy.get('#' + prefix + 'recc-event-container').should("be.visible");
|
||||
cy.get('[name="riedit"]').click();
|
||||
} else {
|
||||
cy.checkEventAllday();
|
||||
cy.checkEventAllday(prefix);
|
||||
|
||||
cy.get("#start-user").click();
|
||||
cy.get('#' + prefix + 'start-user').click();
|
||||
cy.get("#ui-datepicker-div").should("be.visible");
|
||||
cy.get("#ui-datepicker-div a.ui-state-default:first").click(); // select first date
|
||||
cy.get("#ui-datepicker-div").should("not.be.visible");
|
||||
|
||||
cy.get("#start-time").click();
|
||||
cy.get('#' + prefix + 'start-time').click();
|
||||
cy.get(".ui-timepicker-wrapper").should("be.visible");
|
||||
cy.get(".ui-timepicker-wrapper .ui-timepicker-am[data-time=0]").click(); // select 00:00
|
||||
cy.get("#ui-datepicker-div").should("not.be.visible");
|
||||
|
||||
cy.get("#end-container").should("not.be.visible");
|
||||
cy.get("#end-show-container .show-link").click();
|
||||
cy.get("#end-show-container").should("not.be.visible");
|
||||
cy.get("#end-container").should("be.visible");
|
||||
cy.inputsShouldHaveSameValue("#start-user", "#end-user");
|
||||
cy.get("#end-time").should("have.value", "03:00");
|
||||
cy.get("#end-hide-container .hide-link").click();
|
||||
cy.get("#end-show-container").should("be.visible");
|
||||
cy.get("#end-container").should("not.be.visible");
|
||||
cy.get("#end-user").should("have.value", "");
|
||||
cy.get("#end-time").should("have.value", "");
|
||||
cy.get('#' + prefix + 'end-container').should("not.be.visible");
|
||||
cy.get('#' + prefix + 'end-show-container .show-link').click();
|
||||
cy.get('#' + prefix + 'end-show-container').should("not.be.visible");
|
||||
cy.get('#' + prefix + 'end-container').should("be.visible");
|
||||
cy.inputsShouldHaveSameValue('#' + prefix + 'start-user', '#' + prefix + 'end-user');
|
||||
cy.get('#' + prefix + 'end-time').should("have.value", "03:00");
|
||||
cy.get('#' + prefix + 'end-hide-container .hide-link').click();
|
||||
cy.get('#' + prefix + 'end-show-container').should("be.visible");
|
||||
cy.get('#' + prefix + 'end-container').should("not.be.visible");
|
||||
cy.get('#' + prefix + 'end-user').should("have.value", "");
|
||||
cy.get('#' + prefix + 'end-time').should("have.value", "");
|
||||
|
||||
cy.get("#recc-event-container").should("not.be.visible");
|
||||
cy.get("#recc-button").click();
|
||||
cy.get('#' + prefix + 'recc-event-container').should("not.be.visible");
|
||||
cy.get('#' + prefix + 'recc-button').click();
|
||||
}
|
||||
|
||||
cy.get(".modal-recurrence").should("be.visible");
|
||||
cy.inputsShouldHaveSameValue("#start-user", "#recc-start-user");
|
||||
cy.inputsShouldHaveSameValue("#start-time", "#recc-start-time");
|
||||
cy.inputsShouldHaveSameValue('#' + prefix + 'start-user', "#recc-start-user");
|
||||
cy.inputsShouldHaveSameValue('#' + prefix + 'start-time', "#recc-start-time");
|
||||
cy.get("#rirtemplate option").should("have.length", 4);
|
||||
cy.get(".modal-recurrence input[value=BYENDDATE]").should("be.checked");
|
||||
cy.get(".modal-recurrence .modal-footer .btn-primary").click();
|
||||
|
||||
cy.get("#single-event-container").should("not.be.visible");
|
||||
cy.get("#recc-event-container").should("be.visible");
|
||||
cy.get('#' + prefix + 'single-event-container').should("not.be.visible");
|
||||
cy.get('#' + prefix + 'recc-event-container').should("be.visible");
|
||||
|
||||
if (recurrence == false) {
|
||||
cy.get('[name="ridelete"]').click();
|
||||
cy.get("#single-event-container").should("be.visible");
|
||||
cy.get("#recc-event-container").should("not.be.visible");
|
||||
cy.get("#end-container").should("not.be.visible");
|
||||
cy.get('#' + prefix + 'single-event-container').should("be.visible");
|
||||
cy.get('#' + prefix + 'recc-event-container').should("not.be.visible");
|
||||
cy.get('#' + prefix + 'end-container').should("not.be.visible");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
Cypress.Commands.add(
|
||||
"checkEventAllday",
|
||||
() => {
|
||||
(prefix = "") => {
|
||||
// Turn on
|
||||
cy.get('#allday').click();
|
||||
cy.get("#end-container").should("be.visible");
|
||||
cy.get('#start-time').should("not.be.visible");
|
||||
cy.get('#end-time').should("not.be.visible");
|
||||
cy.get('#' + prefix + 'allday').click();
|
||||
cy.get('#' + prefix + 'end-container').should("be.visible");
|
||||
cy.get('#' + prefix + 'start-time').should("not.be.visible");
|
||||
cy.get('#' + prefix + 'end-time').should("not.be.visible");
|
||||
|
||||
// Recurrence
|
||||
cy.get("#recc-button").click();
|
||||
cy.get('#' + prefix + 'recc-button').click();
|
||||
cy.get(".modal-recurrence").should("be.visible");
|
||||
cy.get('#recc-allday').should("be.checked");
|
||||
cy.get('#recc-start-time').should("not.be.visible");
|
||||
@ -294,18 +294,18 @@ Cypress.Commands.add(
|
||||
cy.get(".modal-recurrence .modal-footer .btn-secondary").click();
|
||||
|
||||
// Turn off
|
||||
cy.get("#allday").click();
|
||||
cy.get('#start-time').should("be.visible");
|
||||
cy.get('#end-time').should("be.visible");
|
||||
cy.get('#' + prefix + 'allday').click();
|
||||
cy.get('#' + prefix + 'start-time').should("be.visible");
|
||||
cy.get('#' + prefix + 'end-time').should("be.visible");
|
||||
|
||||
// Turn again
|
||||
cy.get('#allday').click();
|
||||
cy.get("#end-container").should("be.visible");
|
||||
cy.get('#' + prefix + 'allday').click();
|
||||
cy.get('#' + prefix + 'end-container').should("be.visible");
|
||||
|
||||
// Removing end turns off allday
|
||||
cy.get("#end-hide-container .hide-link").click();
|
||||
cy.get('#allday').should("not.be.checked");
|
||||
cy.get('#start-time').should("be.visible");
|
||||
cy.get('#' + prefix + 'end-hide-container .hide-link').click();
|
||||
cy.get('#' + prefix + 'allday').should("not.be.checked");
|
||||
cy.get('#' + prefix + 'start-time').should("be.visible");
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
155
migrations/versions/f350153a5691_.py
Normal file
155
migrations/versions/f350153a5691_.py
Normal file
@ -0,0 +1,155 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: f350153a5691
|
||||
Revises: 920329927dc6
|
||||
Create Date: 2021-11-05 23:50:21.539575
|
||||
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
import sqlalchemy_utils
|
||||
from alembic import op
|
||||
from sqlalchemy import orm
|
||||
from sqlalchemy.dialects import postgresql
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
from project import dbtypes
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "f350153a5691"
|
||||
down_revision = "920329927dc6"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
class Event(Base):
|
||||
__tablename__ = "event"
|
||||
id = sa.Column(sa.Integer(), primary_key=True)
|
||||
start = sa.Column(sa.DateTime(timezone=True), nullable=False)
|
||||
end = sa.Column(sa.DateTime(timezone=True), nullable=True)
|
||||
allday = sa.Column(
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
default=False,
|
||||
server_default="0",
|
||||
)
|
||||
recurrence_rule = sa.Column(sa.UnicodeText())
|
||||
date_definitions = orm.relationship(
|
||||
"EventDateDefinition",
|
||||
backref=orm.backref("event", lazy=False),
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
|
||||
|
||||
class EventDateDefinition(Base):
|
||||
__tablename__ = "eventdatedefinition"
|
||||
id = sa.Column(sa.Integer(), primary_key=True)
|
||||
event_id = sa.Column(sa.Integer, sa.ForeignKey("event.id"), nullable=False)
|
||||
start = sa.Column(sa.DateTime(timezone=True), nullable=False)
|
||||
end = sa.Column(sa.DateTime(timezone=True), nullable=True)
|
||||
allday = sa.Column(
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
default=False,
|
||||
server_default="0",
|
||||
)
|
||||
recurrence_rule = sa.Column(sa.UnicodeText())
|
||||
|
||||
|
||||
def upgrade_event_definitions():
|
||||
bind = op.get_bind()
|
||||
session = orm.Session(bind=bind)
|
||||
|
||||
for event in session.query(Event):
|
||||
date_definition = EventDateDefinition()
|
||||
date_definition.event = event
|
||||
date_definition.start = event.start
|
||||
date_definition.end = event.end
|
||||
date_definition.allday = event.allday
|
||||
date_definition.recurrence_rule = event.recurrence_rule
|
||||
event.date_definitions = [date_definition]
|
||||
|
||||
session.commit()
|
||||
|
||||
|
||||
def downgrade_event_definitions():
|
||||
bind = op.get_bind()
|
||||
session = orm.Session(bind=bind)
|
||||
|
||||
for event in session.query(Event):
|
||||
date_definition = event.date_definitions[0]
|
||||
event.start = date_definition.start
|
||||
event.end = date_definition.end
|
||||
event.allday = date_definition.allday
|
||||
event.recurrence_rule = date_definition.recurrence_rule
|
||||
|
||||
session.commit()
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"eventdatedefinition",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("event_id", sa.Integer(), nullable=False),
|
||||
sa.Column("start", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("end", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("allday", sa.Boolean(), server_default="0", nullable=False),
|
||||
sa.Column("recurrence_rule", sa.UnicodeText(), nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["event_id"],
|
||||
["event.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
|
||||
upgrade_event_definitions()
|
||||
|
||||
op.drop_column("event", "end")
|
||||
op.drop_column("event", "allday")
|
||||
op.drop_column("event", "start")
|
||||
op.drop_column("event", "recurrence_rule")
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column(
|
||||
"event",
|
||||
sa.Column("recurrence_rule", sa.TEXT(), autoincrement=False, nullable=True),
|
||||
)
|
||||
op.add_column(
|
||||
"event",
|
||||
sa.Column(
|
||||
"start",
|
||||
postgresql.TIMESTAMP(timezone=True),
|
||||
server_default=sa.text("CURRENT_TIMESTAMP"),
|
||||
autoincrement=False,
|
||||
nullable=False,
|
||||
),
|
||||
)
|
||||
op.add_column(
|
||||
"event",
|
||||
sa.Column(
|
||||
"allday",
|
||||
sa.BOOLEAN(),
|
||||
server_default=sa.text("false"),
|
||||
autoincrement=False,
|
||||
nullable=False,
|
||||
),
|
||||
)
|
||||
op.add_column(
|
||||
"event",
|
||||
sa.Column(
|
||||
"end",
|
||||
postgresql.TIMESTAMP(timezone=True),
|
||||
autoincrement=False,
|
||||
nullable=True,
|
||||
),
|
||||
)
|
||||
|
||||
downgrade_event_definitions()
|
||||
|
||||
op.drop_table("eventdatedefinition")
|
||||
# ### end Alembic commands ###
|
||||
@ -1,5 +1,5 @@
|
||||
from dateutil.rrule import rrulestr
|
||||
from marshmallow import ValidationError, fields, validate
|
||||
from marshmallow import fields, validate
|
||||
from marshmallow.decorators import pre_load
|
||||
from marshmallow_enum import EnumField
|
||||
|
||||
from project.api import marshmallow
|
||||
@ -8,6 +8,11 @@ from project.api.event_category.schemas import (
|
||||
EventCategoryRefSchema,
|
||||
EventCategoryWriteIdSchema,
|
||||
)
|
||||
from project.api.event_date_definition.schemas import (
|
||||
EventDateDefinitionPatchRequestSchema,
|
||||
EventDateDefinitionPostRequestSchema,
|
||||
EventDateDefinitionSchema,
|
||||
)
|
||||
from project.api.fields import CustomDateTimeField, Owned
|
||||
from project.api.image.schemas import (
|
||||
ImageDumpSchema,
|
||||
@ -52,13 +57,6 @@ class EventIdSchema(EventModelSchema, IdSchemaMixin):
|
||||
pass
|
||||
|
||||
|
||||
def validate_recurrence_rule(recurrence_rule):
|
||||
try:
|
||||
rrulestr(recurrence_rule, forceset=True)
|
||||
except Exception as e:
|
||||
raise ValidationError(str(e))
|
||||
|
||||
|
||||
class EventBaseSchemaMixin(TrackableSchemaMixin):
|
||||
name = marshmallow.auto_field(
|
||||
required=True,
|
||||
@ -137,27 +135,6 @@ class EventBaseSchemaMixin(TrackableSchemaMixin):
|
||||
"description": "Price information in textual form. E.g., different prices for adults and children."
|
||||
},
|
||||
)
|
||||
recurrence_rule = marshmallow.auto_field(
|
||||
validate=validate_recurrence_rule,
|
||||
metadata={
|
||||
"description": "If the event takes place regularly. Format: RFC 5545."
|
||||
},
|
||||
)
|
||||
start = CustomDateTimeField(
|
||||
required=True,
|
||||
metadata={
|
||||
"description": "When the event will take place. If the event takes place regularly, enter when the first date will begin."
|
||||
},
|
||||
)
|
||||
end = CustomDateTimeField(
|
||||
metadata={
|
||||
"description": "When the event will end. An event can last a maximum of 14 days. If the event takes place regularly, enter when the first date will end."
|
||||
},
|
||||
)
|
||||
allday = marshmallow.auto_field(
|
||||
missing=False,
|
||||
metadata={"description": "If the event is an all-day event."},
|
||||
)
|
||||
public_status = EnumField(
|
||||
PublicStatus,
|
||||
missing=PublicStatus.published,
|
||||
@ -172,6 +149,7 @@ class EventSchema(EventIdSchema, EventBaseSchemaMixin):
|
||||
photo = fields.Nested(ImageSchema)
|
||||
categories = fields.List(fields.Nested(EventCategoryRefSchema))
|
||||
co_organizers = fields.List(fields.Nested(OrganizerRefSchema))
|
||||
date_definitions = fields.List(fields.Nested(EventDateDefinitionSchema))
|
||||
|
||||
|
||||
class EventDumpSchema(EventIdSchema, EventBaseSchemaMixin):
|
||||
@ -185,6 +163,7 @@ class EventDumpSchema(EventIdSchema, EventBaseSchemaMixin):
|
||||
co_organizer_ids = fields.Pluck(
|
||||
OrganizerDumpIdSchema, "id", many=True, attribute="co_organizers"
|
||||
)
|
||||
date_definitions = fields.List(fields.Nested(EventDateDefinitionSchema))
|
||||
|
||||
|
||||
class EventRefSchema(EventIdSchema):
|
||||
@ -193,10 +172,7 @@ class EventRefSchema(EventIdSchema):
|
||||
|
||||
class EventSearchItemSchema(EventRefSchema):
|
||||
description = marshmallow.auto_field()
|
||||
start = CustomDateTimeField()
|
||||
end = CustomDateTimeField()
|
||||
allday = marshmallow.auto_field()
|
||||
recurrence_rule = marshmallow.auto_field()
|
||||
date_definitions = fields.List(fields.Nested(EventDateDefinitionSchema))
|
||||
photo = fields.Nested(ImageSchema)
|
||||
place = fields.Nested(PlaceSearchItemSchema, attribute="event_place")
|
||||
status = EnumField(EventStatus)
|
||||
@ -293,6 +269,14 @@ class EventWriteSchemaMixin(object):
|
||||
},
|
||||
)
|
||||
|
||||
@pre_load()
|
||||
def handle_deprecated_fields(self, data, **kwargs):
|
||||
if "start" in data:
|
||||
if "date_definitions" not in data:
|
||||
data["date_definitions"] = [{"start": data["start"]}]
|
||||
data.pop("start")
|
||||
return data
|
||||
|
||||
|
||||
class EventPostRequestSchema(
|
||||
EventModelSchema, EventBaseSchemaMixin, EventWriteSchemaMixin
|
||||
@ -301,6 +285,13 @@ class EventPostRequestSchema(
|
||||
super().__init__(*args, **kwargs)
|
||||
self.make_post_schema()
|
||||
|
||||
date_definitions = fields.List(
|
||||
fields.Nested(EventDateDefinitionPostRequestSchema),
|
||||
default=None,
|
||||
required=True,
|
||||
validate=[validate.Length(min=1)],
|
||||
metadata={"description": "At least one date definition."},
|
||||
)
|
||||
photo = Owned(ImagePostRequestSchema)
|
||||
|
||||
|
||||
@ -311,6 +302,13 @@ class EventPatchRequestSchema(
|
||||
super().__init__(*args, **kwargs)
|
||||
self.make_patch_schema()
|
||||
|
||||
date_definitions = fields.List(
|
||||
fields.Nested(EventDateDefinitionPatchRequestSchema),
|
||||
default=None,
|
||||
required=True,
|
||||
validate=[validate.Length(min=1)],
|
||||
metadata={"description": "At least one date definition."},
|
||||
)
|
||||
photo = Owned(ImagePatchRequestSchema)
|
||||
|
||||
|
||||
|
||||
0
project/api/event_date_definition/__init__.py
Normal file
0
project/api/event_date_definition/__init__.py
Normal file
83
project/api/event_date_definition/schemas.py
Normal file
83
project/api/event_date_definition/schemas.py
Normal file
@ -0,0 +1,83 @@
|
||||
from dateutil.rrule import rrulestr
|
||||
from marshmallow import ValidationError, fields
|
||||
|
||||
from project.api.fields import CustomDateTimeField
|
||||
from project.api.schemas import IdSchemaMixin, SQLAlchemyBaseSchema
|
||||
from project.models import EventDateDefinition
|
||||
|
||||
|
||||
class EventDateDefinitionModelSchema(SQLAlchemyBaseSchema):
|
||||
class Meta:
|
||||
model = EventDateDefinition
|
||||
load_instance = True
|
||||
|
||||
|
||||
class EventDateDefinitionIdSchema(EventDateDefinitionModelSchema, IdSchemaMixin):
|
||||
pass
|
||||
|
||||
|
||||
def validate_recurrence_rule(recurrence_rule):
|
||||
try:
|
||||
rrulestr(recurrence_rule, forceset=True)
|
||||
except Exception as e:
|
||||
raise ValidationError(str(e))
|
||||
|
||||
|
||||
class EventDateDefinitionBaseSchemaMixin(object):
|
||||
start = CustomDateTimeField(
|
||||
required=True,
|
||||
metadata={
|
||||
"description": "When the event will take place. If the event takes place regularly, enter when the first date will begin."
|
||||
},
|
||||
)
|
||||
end = CustomDateTimeField(
|
||||
metadata={
|
||||
"description": "When the event will end. An event can last a maximum of 14 days. If the event takes place regularly, enter when the first date will end."
|
||||
},
|
||||
)
|
||||
allday = fields.Bool(
|
||||
missing=False,
|
||||
metadata={"description": "If the event is an all-day event."},
|
||||
)
|
||||
recurrence_rule = fields.Str(
|
||||
validate=validate_recurrence_rule,
|
||||
metadata={
|
||||
"description": "If the event takes place regularly. Format: RFC 5545."
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class EventDateDefinitionSchema(
|
||||
EventDateDefinitionIdSchema, EventDateDefinitionBaseSchemaMixin
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
class EventDateDefinitionDumpSchema(
|
||||
EventDateDefinitionIdSchema, EventDateDefinitionBaseSchemaMixin
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
class EventDateDefinitionWriteSchemaMixin(object):
|
||||
pass
|
||||
|
||||
|
||||
class EventDateDefinitionPostRequestSchema(
|
||||
EventDateDefinitionModelSchema,
|
||||
EventDateDefinitionBaseSchemaMixin,
|
||||
EventDateDefinitionWriteSchemaMixin,
|
||||
):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.make_post_schema()
|
||||
|
||||
|
||||
class EventDateDefinitionPatchRequestSchema(
|
||||
EventDateDefinitionModelSchema,
|
||||
EventDateDefinitionBaseSchemaMixin,
|
||||
EventDateDefinitionWriteSchemaMixin,
|
||||
):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.make_patch_schema()
|
||||
@ -14,6 +14,7 @@ from project.models import (
|
||||
AdminUnitInvitation,
|
||||
Event,
|
||||
EventAttendanceMode,
|
||||
EventDateDefinition,
|
||||
EventReference,
|
||||
EventReferenceRequest,
|
||||
EventReferenceRequestReviewStatus,
|
||||
@ -188,13 +189,17 @@ def _create_event(admin_unit_id):
|
||||
event.categories = [upsert_event_category("Other")]
|
||||
event.name = "Name"
|
||||
event.description = "Beschreibung"
|
||||
event.start = _get_now_by_minute()
|
||||
event.event_place_id = _get_default_event_place_id(admin_unit_id)
|
||||
event.organizer_id = _get_default_organizer_id(admin_unit_id)
|
||||
event.ticket_link = ""
|
||||
event.tags = ""
|
||||
event.price_info = ""
|
||||
event.attendance_mode = EventAttendanceMode.offline
|
||||
|
||||
date_definition = EventDateDefinition()
|
||||
date_definition.start = _get_now_by_minute()
|
||||
event.date_definitions = [date_definition]
|
||||
|
||||
insert_event(event)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@ -33,8 +33,7 @@ class Base64ImageForm(BaseImageForm):
|
||||
)
|
||||
|
||||
def validate(self):
|
||||
if not super().validate(): # pragma: no cover
|
||||
return False
|
||||
result = super().validate()
|
||||
|
||||
if self.image_base64.data:
|
||||
image = get_image_from_base64_str(self.image_base64.data)
|
||||
@ -43,9 +42,9 @@ class Base64ImageForm(BaseImageForm):
|
||||
except ValueError as e:
|
||||
msg = str(e)
|
||||
self.image_base64.errors.append(msg)
|
||||
return False
|
||||
result = False
|
||||
|
||||
return True
|
||||
return result
|
||||
|
||||
def populate_obj(self, obj):
|
||||
super(BaseImageForm, self).populate_obj(obj)
|
||||
|
||||
@ -13,6 +13,7 @@ from wtforms import (
|
||||
SubmitField,
|
||||
TextAreaField,
|
||||
)
|
||||
from wtforms.fields.core import FieldList
|
||||
from wtforms.fields.html5 import EmailField, URLField
|
||||
from wtforms.validators import DataRequired, Length, Optional
|
||||
|
||||
@ -21,6 +22,7 @@ from project.forms.event_place import EventPlaceLocationForm
|
||||
from project.forms.widgets import CustomDateField, CustomDateTimeField, HTML5StringField
|
||||
from project.models import (
|
||||
EventAttendanceMode,
|
||||
EventDateDefinition,
|
||||
EventOrganizer,
|
||||
EventPlace,
|
||||
EventStatus,
|
||||
@ -31,6 +33,53 @@ from project.models import (
|
||||
)
|
||||
|
||||
|
||||
class EventDateDefinitionFormMixin:
|
||||
start = CustomDateTimeField(
|
||||
lazy_gettext("Start"),
|
||||
validators=[DataRequired()],
|
||||
description=lazy_gettext("Indicate when the event will take place."),
|
||||
)
|
||||
end = CustomDateTimeField(
|
||||
lazy_gettext("End"),
|
||||
validators=[Optional()],
|
||||
description=lazy_gettext(
|
||||
"Indicate when the event will end. An event can last a maximum of 14 days."
|
||||
),
|
||||
)
|
||||
allday = BooleanField(
|
||||
lazy_gettext("All-day"),
|
||||
validators=[Optional()],
|
||||
)
|
||||
recurrence_rule = TextAreaField(
|
||||
lazy_gettext("Recurring event"),
|
||||
validators=[Optional()],
|
||||
)
|
||||
|
||||
def validate_date_definition(self):
|
||||
if self.start.data and self.end.data:
|
||||
if self.start.data > self.end.data:
|
||||
msg = gettext("The start must be before the end.")
|
||||
self.start.errors.append(msg)
|
||||
return False
|
||||
|
||||
max_end = self.start.data + relativedelta(days=14)
|
||||
if self.end.data > max_end:
|
||||
msg = gettext("An event can last a maximum of 14 days.")
|
||||
self.end.errors.append(msg)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class EventDateDefinitionForm(FlaskForm, EventDateDefinitionFormMixin):
|
||||
def validate(self):
|
||||
result = super().validate()
|
||||
|
||||
if not self.validate_date_definition():
|
||||
result = False
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class EventPlaceForm(FlaskForm):
|
||||
name = StringField(
|
||||
lazy_gettext("Name"),
|
||||
@ -66,26 +115,6 @@ class SharedEventForm(FlaskForm):
|
||||
validators=[DataRequired(), Length(min=3, max=255)],
|
||||
description=lazy_gettext("Enter a short, meaningful name for the event."),
|
||||
)
|
||||
start = CustomDateTimeField(
|
||||
lazy_gettext("Start"),
|
||||
validators=[DataRequired()],
|
||||
description=lazy_gettext("Indicate when the event will take place."),
|
||||
)
|
||||
end = CustomDateTimeField(
|
||||
lazy_gettext("End"),
|
||||
validators=[Optional()],
|
||||
description=lazy_gettext(
|
||||
"Indicate when the event will end. An event can last a maximum of 14 days."
|
||||
),
|
||||
)
|
||||
allday = BooleanField(
|
||||
lazy_gettext("All-day"),
|
||||
validators=[Optional()],
|
||||
)
|
||||
recurrence_rule = TextAreaField(
|
||||
lazy_gettext("Recurring event"),
|
||||
validators=[Optional()],
|
||||
)
|
||||
description = TextAreaField(
|
||||
lazy_gettext("Description"),
|
||||
validators=[Optional()],
|
||||
@ -200,24 +229,12 @@ class SharedEventForm(FlaskForm):
|
||||
),
|
||||
)
|
||||
|
||||
def validate(self):
|
||||
if not super().validate():
|
||||
return False
|
||||
if self.start.data and self.end.data:
|
||||
if self.start.data > self.end.data:
|
||||
msg = gettext("The start must be before the end.")
|
||||
self.start.errors.append(msg)
|
||||
return False
|
||||
|
||||
max_end = self.start.data + relativedelta(days=14)
|
||||
if self.end.data > max_end:
|
||||
msg = gettext("An event can last a maximum of 14 days.")
|
||||
self.end.errors.append(msg)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class BaseEventForm(SharedEventForm):
|
||||
date_definitions = FieldList(
|
||||
FormField(EventDateDefinitionForm, default=lambda: EventDateDefinition()),
|
||||
min_entries=1,
|
||||
)
|
||||
previous_start_date = CustomDateTimeField(
|
||||
lazy_gettext("Previous start date"),
|
||||
validators=[Optional()],
|
||||
@ -320,23 +337,25 @@ class CreateEventForm(BaseEventForm):
|
||||
)
|
||||
|
||||
def validate(self):
|
||||
if not super().validate():
|
||||
return False
|
||||
result = super().validate()
|
||||
|
||||
if (
|
||||
not self.event_place_id.data or self.event_place_id.data == 0
|
||||
) and not self.new_event_place.form.name.data:
|
||||
msg = gettext("Select existing place or enter new place")
|
||||
self.event_place_id.errors.append(msg)
|
||||
self.new_event_place.form.name.errors.append(msg)
|
||||
return False
|
||||
result = False
|
||||
|
||||
if (
|
||||
not self.organizer_id.data or self.organizer_id.data == 0
|
||||
) and not self.new_organizer.form.name.data:
|
||||
msg = gettext("Select existing organizer or enter new organizer")
|
||||
self.organizer_id.errors.append(msg)
|
||||
self.new_organizer.form.name.errors.append(msg)
|
||||
return False
|
||||
return True
|
||||
result = False
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class UpdateEventForm(BaseEventForm):
|
||||
|
||||
@ -11,7 +11,7 @@ from wtforms.fields.html5 import EmailField, TelField
|
||||
from wtforms.validators import DataRequired, Optional
|
||||
|
||||
from project.forms.common import get_accept_tos_markup
|
||||
from project.forms.event import SharedEventForm
|
||||
from project.forms.event import EventDateDefinitionFormMixin, SharedEventForm
|
||||
from project.forms.widgets import TagSelectField
|
||||
from project.models import (
|
||||
EventAttendanceMode,
|
||||
@ -21,7 +21,7 @@ from project.models import (
|
||||
)
|
||||
|
||||
|
||||
class CreateEventSuggestionForm(SharedEventForm):
|
||||
class CreateEventSuggestionForm(SharedEventForm, EventDateDefinitionFormMixin):
|
||||
contact_name = StringField(
|
||||
lazy_gettext("Name"),
|
||||
validators=[DataRequired()],
|
||||
@ -110,6 +110,14 @@ class CreateEventSuggestionForm(SharedEventForm):
|
||||
else:
|
||||
field.populate_obj(obj, name)
|
||||
|
||||
def validate(self):
|
||||
result = super().validate()
|
||||
|
||||
if not self.validate_date_definition():
|
||||
result = False
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class RejectEventSuggestionForm(FlaskForm):
|
||||
rejection_resaon = SelectField(
|
||||
|
||||
@ -113,7 +113,7 @@ def get_sd_for_event_date(event_date):
|
||||
if event.previous_start_date:
|
||||
result["previousStartDate"] = event.previous_start_date
|
||||
|
||||
if event.allday:
|
||||
if event_date.allday:
|
||||
result["startDate"] = get_date_from_datetime(result["startDate"])
|
||||
|
||||
if event_date.end:
|
||||
|
||||
@ -763,15 +763,6 @@ class EventReferenceRequest(db.Model, TrackableMixin):
|
||||
|
||||
class EventMixin(object):
|
||||
name = Column(Unicode(255), nullable=False)
|
||||
start = db.Column(db.DateTime(timezone=True), nullable=False)
|
||||
end = db.Column(db.DateTime(timezone=True), nullable=True)
|
||||
allday = db.Column(
|
||||
Boolean(),
|
||||
nullable=False,
|
||||
default=False,
|
||||
server_default="0",
|
||||
)
|
||||
recurrence_rule = Column(UnicodeText())
|
||||
external_link = Column(String(255))
|
||||
description = Column(UnicodeText(), nullable=True)
|
||||
|
||||
@ -802,8 +793,6 @@ class EventMixin(object):
|
||||
if self.photo and self.photo.is_empty():
|
||||
self.photo_id = None
|
||||
|
||||
sanitize_allday_instance(self)
|
||||
|
||||
|
||||
class EventSuggestion(db.Model, TrackableMixin, EventMixin):
|
||||
__tablename__ = "eventsuggestion"
|
||||
@ -815,6 +804,16 @@ class EventSuggestion(db.Model, TrackableMixin, EventMixin):
|
||||
)
|
||||
id = Column(Integer(), primary_key=True)
|
||||
|
||||
start = db.Column(db.DateTime(timezone=True), nullable=False)
|
||||
end = db.Column(db.DateTime(timezone=True), nullable=True)
|
||||
allday = db.Column(
|
||||
Boolean(),
|
||||
nullable=False,
|
||||
default=False,
|
||||
server_default="0",
|
||||
)
|
||||
recurrence_rule = Column(UnicodeText())
|
||||
|
||||
review_status = Column(IntegerEnum(EventReviewStatus))
|
||||
rejection_resaon = Column(IntegerEnum(EventRejectionReason))
|
||||
|
||||
@ -859,11 +858,13 @@ def purge_event_suggestion(mapper, connect, self):
|
||||
if self.event_place_id is not None:
|
||||
self.event_place_text = None
|
||||
self.purge_event_mixin()
|
||||
sanitize_allday_instance(self)
|
||||
|
||||
|
||||
class Event(db.Model, TrackableMixin, EventMixin):
|
||||
__tablename__ = "event"
|
||||
id = Column(Integer(), primary_key=True)
|
||||
|
||||
admin_unit_id = db.Column(db.Integer, db.ForeignKey("adminunit.id"), nullable=False)
|
||||
organizer_id = db.Column(
|
||||
db.Integer, db.ForeignKey("eventorganizer.id"), nullable=False
|
||||
@ -891,6 +892,56 @@ class Event(db.Model, TrackableMixin, EventMixin):
|
||||
previous_start_date = db.Column(db.DateTime(timezone=True), nullable=True)
|
||||
rating = Column(Integer(), default=50)
|
||||
|
||||
@property
|
||||
def min_start_definition(self):
|
||||
if self.date_definitions:
|
||||
return min(self.date_definitions, key=lambda d: d.start)
|
||||
else:
|
||||
return None
|
||||
|
||||
@hybrid_property
|
||||
def min_start(self):
|
||||
if self.date_definitions:
|
||||
return min(d.start for d in self.date_definitions)
|
||||
else:
|
||||
return None
|
||||
|
||||
@min_start.expression
|
||||
def min_start(cls):
|
||||
return (
|
||||
select([EventDateDefinition.end])
|
||||
.where(EventDateDefinition.event_id == cls.id)
|
||||
.order_by(EventDateDefinition.start)
|
||||
.as_scalar()
|
||||
)
|
||||
|
||||
@hybrid_property
|
||||
def is_recurring(self):
|
||||
if self.date_definitions:
|
||||
return any(d.recurrence_rule for d in self.date_definitions)
|
||||
else:
|
||||
return False
|
||||
|
||||
@is_recurring.expression
|
||||
def is_recurring(cls):
|
||||
return (
|
||||
select([func.count()])
|
||||
.select_from(EventDateDefinition.__table__)
|
||||
.where(
|
||||
and_(
|
||||
EventDateDefinition.event_id == cls.id,
|
||||
func.coalesce(EventDateDefinition.recurrence_rule, "") != "",
|
||||
)
|
||||
)
|
||||
.as_scalar()
|
||||
) > 0
|
||||
|
||||
date_definitions = relationship(
|
||||
"EventDateDefinition",
|
||||
backref=backref("event", lazy=False),
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
|
||||
dates = relationship(
|
||||
"EventDate", backref=backref("event", lazy=False), cascade="all, delete-orphan"
|
||||
)
|
||||
@ -938,13 +989,8 @@ class Event(db.Model, TrackableMixin, EventMixin):
|
||||
if self.event_place and self.event_place.admin_unit_id != self.admin_unit_id:
|
||||
raise make_check_violation("Invalid place.")
|
||||
|
||||
if self.start and self.end:
|
||||
if self.start > self.end:
|
||||
raise make_check_violation("The start must be before the end.")
|
||||
|
||||
max_end = self.start + relativedelta(days=14)
|
||||
if self.end > max_end:
|
||||
raise make_check_violation("An event can last a maximum of 14 days.")
|
||||
if not self.date_definitions or len(self.date_definitions) == 0:
|
||||
raise make_check_violation("At least one date defintion is required.")
|
||||
|
||||
|
||||
@listens_for(Event, "before_insert")
|
||||
@ -974,6 +1020,37 @@ def purge_event_date(mapper, connect, self):
|
||||
sanitize_allday_instance(self)
|
||||
|
||||
|
||||
class EventDateDefinition(db.Model):
|
||||
__tablename__ = "eventdatedefinition"
|
||||
id = Column(Integer(), primary_key=True)
|
||||
event_id = db.Column(db.Integer, db.ForeignKey("event.id"), nullable=False)
|
||||
start = db.Column(db.DateTime(timezone=True), nullable=False)
|
||||
end = db.Column(db.DateTime(timezone=True), nullable=True)
|
||||
allday = db.Column(
|
||||
Boolean(),
|
||||
nullable=False,
|
||||
default=False,
|
||||
server_default="0",
|
||||
)
|
||||
recurrence_rule = Column(UnicodeText())
|
||||
|
||||
def validate(self):
|
||||
if self.start and self.end:
|
||||
if self.start > self.end:
|
||||
raise make_check_violation("The start must be before the end.")
|
||||
|
||||
max_end = self.start + relativedelta(days=14)
|
||||
if self.end > max_end:
|
||||
raise make_check_violation("An event can last a maximum of 14 days.")
|
||||
|
||||
|
||||
@listens_for(EventDateDefinition, "before_insert")
|
||||
@listens_for(EventDateDefinition, "before_update")
|
||||
def before_saving_event_date_definition(mapper, connect, self):
|
||||
self.validate()
|
||||
sanitize_allday_instance(self)
|
||||
|
||||
|
||||
class EventEventCategories(db.Model):
|
||||
__tablename__ = "event_eventcategories"
|
||||
__table_args__ = (UniqueConstraint("event_id", "category_id"),)
|
||||
|
||||
@ -301,60 +301,63 @@ def get_events_query(params):
|
||||
joinedload(Event.admin_unit),
|
||||
)
|
||||
.filter(event_filter)
|
||||
.order_by(Event.start)
|
||||
.order_by(Event.min_start)
|
||||
)
|
||||
|
||||
|
||||
def get_recurring_events():
|
||||
return Event.query.filter(func.coalesce(Event.recurrence_rule, "") != "").all()
|
||||
return Event.query.filter(Event.is_recurring).all()
|
||||
|
||||
|
||||
def update_event_dates_with_recurrence_rule(event):
|
||||
sanitize_allday_instance(event)
|
||||
start = event.start
|
||||
end = event.end
|
||||
|
||||
if end:
|
||||
time_difference = relativedelta(end, start)
|
||||
|
||||
dates_to_add = list()
|
||||
dates_to_remove = list(event.dates)
|
||||
|
||||
if event.recurrence_rule:
|
||||
rr_dates = dates_from_recurrence_rule(start, event.recurrence_rule)
|
||||
else:
|
||||
rr_dates = [start]
|
||||
|
||||
for rr_date in rr_dates:
|
||||
rr_date_start = date_add_time(
|
||||
rr_date, start.hour, start.minute, start.second, rr_date.tzinfo
|
||||
)
|
||||
for date_definition in event.date_definitions:
|
||||
sanitize_allday_instance(date_definition)
|
||||
start = date_definition.start
|
||||
end = date_definition.end
|
||||
|
||||
if end:
|
||||
rr_date_end = rr_date_start + time_difference
|
||||
else:
|
||||
rr_date_end = None
|
||||
time_difference = relativedelta(end, start)
|
||||
|
||||
existing_date = next(
|
||||
(
|
||||
date
|
||||
for date in event.dates
|
||||
if date.start == rr_date_start
|
||||
and date.end == rr_date_end
|
||||
and date.allday == event.allday
|
||||
),
|
||||
None,
|
||||
)
|
||||
if existing_date:
|
||||
dates_to_remove.remove(existing_date)
|
||||
else:
|
||||
new_date = EventDate(
|
||||
event_id=event.id,
|
||||
start=rr_date_start,
|
||||
end=rr_date_end,
|
||||
allday=event.allday,
|
||||
if date_definition.recurrence_rule:
|
||||
rr_dates = dates_from_recurrence_rule(
|
||||
start, date_definition.recurrence_rule
|
||||
)
|
||||
dates_to_add.append(new_date)
|
||||
else:
|
||||
rr_dates = [start]
|
||||
|
||||
for rr_date in rr_dates:
|
||||
rr_date_start = date_add_time(
|
||||
rr_date, start.hour, start.minute, start.second, rr_date.tzinfo
|
||||
)
|
||||
|
||||
if end:
|
||||
rr_date_end = rr_date_start + time_difference
|
||||
else:
|
||||
rr_date_end = None
|
||||
|
||||
existing_date = next(
|
||||
(
|
||||
date
|
||||
for date in event.dates
|
||||
if date.start == rr_date_start
|
||||
and date.end == rr_date_end
|
||||
and date.allday == date_definition.allday
|
||||
),
|
||||
None,
|
||||
)
|
||||
if existing_date:
|
||||
dates_to_remove.remove(existing_date)
|
||||
else:
|
||||
new_date = EventDate(
|
||||
event_id=event.id,
|
||||
start=rr_date_start,
|
||||
end=rr_date_end,
|
||||
allday=date_definition.allday,
|
||||
)
|
||||
dates_to_add.append(new_date)
|
||||
|
||||
event.dates = [date for date in event.dates if date not in dates_to_remove]
|
||||
event.dates.extend(dates_to_add)
|
||||
|
||||
@ -11,9 +11,10 @@
|
||||
|
||||
tool = $.tools.recurrenceinput = {
|
||||
conf: {
|
||||
lang: 'en',
|
||||
lang: 'de',
|
||||
readOnly: false,
|
||||
firstDay: 0,
|
||||
firstDay: 1,
|
||||
prefix: '',
|
||||
|
||||
// "REMOTE" FIELD
|
||||
startField: null,
|
||||
@ -1169,6 +1170,7 @@
|
||||
|
||||
var self = this;
|
||||
var form, display, dialog;
|
||||
var prefix = conf.prefix;
|
||||
|
||||
// Extend conf with non-configurable data used by templates.
|
||||
var orderedWeekdays = [];
|
||||
@ -1513,21 +1515,21 @@
|
||||
}
|
||||
|
||||
function displayOn() {
|
||||
display.find('div[class=ridisplay-start]').text($('#start-user').val());
|
||||
display.find('div[class=ridisplay-start]').text($('#' + conf.prefix + 'start-user').val());
|
||||
|
||||
if ($('#allday').is(':checked')) {
|
||||
if ($('#' + conf.prefix + 'allday').is(':checked')) {
|
||||
display.find('div[class=ridisplay-times]').text(conf.i18n.reccAllDay);
|
||||
} else {
|
||||
var times = $('#start-time').val();
|
||||
var end_time = $('#end-time').val();
|
||||
var times = $('#' + conf.prefix + 'start-time').val();
|
||||
var end_time = $('#' + conf.prefix + 'end-time').val();
|
||||
if (end_time) {
|
||||
times += ' - ' + end_time
|
||||
}
|
||||
display.find('div[class=ridisplay-times]').text(times);
|
||||
}
|
||||
|
||||
$('#single-event-container').hide();
|
||||
$('#recc-event-container').show();
|
||||
$('#' + conf.prefix + 'single-event-container').hide();
|
||||
$('#' + conf.prefix + 'recc-event-container').show();
|
||||
}
|
||||
|
||||
function recurrenceOn() {
|
||||
@ -1546,8 +1548,8 @@
|
||||
}
|
||||
|
||||
function displayOff() {
|
||||
$('#single-event-container').show();
|
||||
$('#recc-event-container').hide();
|
||||
$('#' + conf.prefix + 'single-event-container').show();
|
||||
$('#' + conf.prefix + 'recc-event-container').hide();
|
||||
}
|
||||
|
||||
function recurrenceOff() {
|
||||
@ -1558,8 +1560,8 @@
|
||||
display.find('button[name="riedit"]').text(conf.i18n.add_rules);
|
||||
display.find('button[name="ridelete"]').hide();
|
||||
|
||||
set_picker_date($('#end-user'), null);
|
||||
hideLink(null, $("#end-hide-container a.hide-link"));
|
||||
set_picker_date($('#' + conf.prefix + 'end-user'), null);
|
||||
hideLink(null, $("#" + conf.prefix + "end-hide-container a.hide-link"));
|
||||
|
||||
displayOff();
|
||||
}
|
||||
@ -1661,10 +1663,10 @@
|
||||
// if no field errors, process the request
|
||||
if (checkFields(form)) {
|
||||
|
||||
var start_moment = get_moment_with_time('#recc-start');
|
||||
set_picker_date($('#start-user'), start_moment.toDate());
|
||||
var start_moment = get_moment_with_time_from_fields(form.find('input[name=recc-start]'), form.find('input[name=recc-start-time]'));
|
||||
set_picker_date($('#' + conf.prefix + 'start-user'), start_moment.toDate());
|
||||
|
||||
var end_time = $('#recc-fo-end-time').timepicker("getTime");
|
||||
var end_time = form.find('input[name=recc-fo-end-time]').timepicker("getTime");
|
||||
var end_datetime = null;
|
||||
if (end_time != null) {
|
||||
var end_moment = moment(start_moment).set({hour: end_time.getHours(), minute: end_time.getMinutes()});
|
||||
@ -1676,9 +1678,9 @@
|
||||
end_datetime = end_moment.toDate()
|
||||
}
|
||||
|
||||
set_picker_date($('#end-user'), end_datetime);
|
||||
set_picker_date($('#' + conf.prefix + 'end-user'), end_datetime);
|
||||
|
||||
$('#allday').prop('checked', $('#recc-allday').is(':checked'));
|
||||
$('#' + conf.prefix + 'allday').prop('checked', form.find('input[name=recc-allday]').is(':checked'));
|
||||
|
||||
recurrenceOn();
|
||||
|
||||
@ -1772,23 +1774,23 @@
|
||||
);
|
||||
|
||||
dialog.on('shown.bs.modal', function (e) {
|
||||
var recc_start_time = $("#recc-start-time");
|
||||
var recc_fo_end_time = $('#recc-fo-end-time');
|
||||
var recc_start_time = form.find('input[name=recc-start-time]');
|
||||
var recc_fo_end_time = form.find('input[name=recc-fo-end-time]');
|
||||
|
||||
recc_start_time.change(function() {
|
||||
recc_fo_end_time.timepicker('option', 'minTime', $(this).timepicker("getTime"));
|
||||
});
|
||||
|
||||
$('#recc-start-user').datepicker("setDate", $('#start-user').datepicker("getDate"));
|
||||
recc_start_time.timepicker('setTime', $('#start-time').timepicker("getTime"));
|
||||
form.find('input[id=recc-start-user]').datepicker("setDate", $('#' + conf.prefix + 'start-user').datepicker("getDate"));
|
||||
recc_start_time.timepicker('setTime', $('#' + conf.prefix + 'start-time').timepicker("getTime"));
|
||||
recc_start_time.change();
|
||||
recc_fo_end_time.timepicker('setTime', $('#end-time').timepicker("getTime"));
|
||||
recc_fo_end_time.timepicker('setTime', $('#' + conf.prefix + 'end-time').timepicker("getTime"));
|
||||
|
||||
var recc_allday = $('#recc-allday');
|
||||
recc_allday.prop('checked', $('#allday').is(':checked'));
|
||||
var recc_allday = form.find('input[name=recc-allday]');
|
||||
recc_allday.prop('checked', $('#' + conf.prefix + 'allday').is(':checked'));
|
||||
var allday_checked = recc_allday.is(':checked');
|
||||
var recc_start_time_group = $("#recc-start-time-group");
|
||||
var recc_fo_end_time_group = $('#recc-fo-end-time-group');
|
||||
var recc_start_time_group = form.find("#recc-start-time-group");
|
||||
var recc_fo_end_time_group = form.find('#recc-fo-end-time-group');
|
||||
recc_start_time_group.toggle(!allday_checked);
|
||||
recc_fo_end_time_group.toggle(!allday_checked);
|
||||
|
||||
@ -1798,7 +1800,7 @@
|
||||
|
||||
onAlldayChecked(this, "recc-start");
|
||||
|
||||
var end_moment = get_moment_with_time("#recc-start");
|
||||
var end_moment = get_moment_with_time_from_fields(form.find('input[name=recc-start]'), form.find('input[name=recc-start-time]'));
|
||||
if (this.checked) {
|
||||
end_moment = end_moment.endOf('day');
|
||||
} else {
|
||||
@ -1807,11 +1809,11 @@
|
||||
recc_fo_end_time.timepicker('setTime', end_moment.toDate());
|
||||
});
|
||||
|
||||
$('#occurences-show-container .show-link').click(function(e){
|
||||
form.find('#occurences-show-container .show-link').click(function(e){
|
||||
showLink(e, this);
|
||||
});
|
||||
|
||||
$('#occurences-hide-container .hide-link').click(function(e){
|
||||
form.find('#occurences-hide-container .hide-link').click(function(e){
|
||||
hideLink(e, this);
|
||||
});
|
||||
|
||||
@ -1873,7 +1875,7 @@
|
||||
form.find('.ricancelbutton').click(cancel);
|
||||
form.find('.risavebutton').click(save);
|
||||
|
||||
$('#recc-button').click(function() {
|
||||
$('#' + conf.prefix + 'recc-button').click(function() {
|
||||
$('button[name=riedit]').click();
|
||||
});
|
||||
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
moment.locale("de");
|
||||
|
||||
function get_moment_with_time(field_id) {
|
||||
var date_time_string = $(field_id).val();
|
||||
var time_string = $(field_id + "-time").val();
|
||||
function get_moment_with_time_from_fields(date_field, time_field) {
|
||||
var date_time_string = $(date_field).val();
|
||||
var time_string = $(time_field).val();
|
||||
|
||||
if (time_string != undefined && time_string != "") {
|
||||
date_time_string += " " + time_string;
|
||||
@ -11,6 +11,12 @@ function get_moment_with_time(field_id) {
|
||||
return moment(date_time_string);
|
||||
}
|
||||
|
||||
function get_moment_with_time(field_id) {
|
||||
var date_field = $(field_id);
|
||||
var time_field = $(field_id + "-time");
|
||||
return get_moment_with_time_from_fields(date_field, time_field)
|
||||
}
|
||||
|
||||
function set_date_bounds(picker) {
|
||||
var data_range_to_attr = picker.attr("data-range-to");
|
||||
var data_allday_attr = picker.attr("data-allday");
|
||||
@ -98,6 +104,10 @@ function onAlldayChecked(checkbox, hidden_field_id) {
|
||||
}
|
||||
|
||||
function start_datepicker(input) {
|
||||
if (input.data('picker') !== undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
var hidden_field = input;
|
||||
var hidden_field_id = hidden_field.attr("id");
|
||||
|
||||
@ -342,6 +352,84 @@ function scroll_to_element(element, complete) {
|
||||
);
|
||||
}
|
||||
|
||||
(function($) {
|
||||
function OvedaDateDefinition(element, options) {
|
||||
var self = this;
|
||||
var container = $(element);
|
||||
var prefix = container.attr('data-prefix');
|
||||
|
||||
var startInput = container.find("input[id$='-start']");
|
||||
var endInput = container.find("input[id$='-end']");
|
||||
|
||||
var startTimeInput = container.find("input[id$='-start-time']");
|
||||
var endTimeInput = container.find("input[id$='-end-time']");
|
||||
|
||||
var endShowContainer = container.find("div[id$='-end-show-container']");
|
||||
var endContainer = container.find("div[id$='-end-container']");
|
||||
var alldayInput = container.find("input[id$='-allday']");
|
||||
var recurrenceRuleTextarea = container.find("textarea[id$='-recurrence_rule']");
|
||||
|
||||
start_datepicker(startInput);
|
||||
start_datepicker(endInput);
|
||||
|
||||
start_timepicker(startTimeInput);
|
||||
start_timepicker(endTimeInput);
|
||||
|
||||
recurrenceRuleTextarea.recurrenceinput({prefix: prefix, ajaxURL: "/events/rrule"});
|
||||
|
||||
var startUserInput = container.find("input[id$='-start-user']");
|
||||
var endUserInput = container.find("input[id$='-end-user']");
|
||||
|
||||
startInput.rules("add", { dateRange: ["#start", "#end"] });
|
||||
endInput.rules("add", { dateRangeDay: ["#start", "#end"] });
|
||||
|
||||
startTimeInput.rules("add", "time");
|
||||
endTimeInput.rules("add", "time");
|
||||
|
||||
endContainer.on('shown', function() {
|
||||
var end_moment = get_moment_with_time('#' + startInput.attr('id'));
|
||||
|
||||
if (alldayInput.is(':checked')) {
|
||||
end_moment = end_moment.endOf('day');
|
||||
} else {
|
||||
end_moment = end_moment.add(3, 'hours');
|
||||
}
|
||||
|
||||
set_picker_date(endUserInput, end_moment.toDate());
|
||||
});
|
||||
|
||||
endContainer.on('hidden', function() {
|
||||
set_picker_date(endUserInput, null);
|
||||
alldayInput.prop('checked', false).trigger("change");
|
||||
});
|
||||
|
||||
alldayInput.on('change', function() {
|
||||
if (this.checked && !endContainer.is(":visible")) {
|
||||
showLink(null, endShowContainer.find("a.show-link"));
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
$.fn.ovedaDateDefinition = function(options) {
|
||||
var defaults = {};
|
||||
var settings = $.extend({}, defaults, options);
|
||||
|
||||
if (this.length > 1) {
|
||||
this.each(function() { $(this).ovedaDateDefinition(options) });
|
||||
return this;
|
||||
}
|
||||
|
||||
if (this.data('ovedaDateDefinition')) {
|
||||
return this.data('ovedaDateDefinition');
|
||||
}
|
||||
|
||||
var ovedaDateDefinition = new OvedaDateDefinition(this, settings);
|
||||
this.data('ovedaDateDefinition', ovedaDateDefinition);
|
||||
return ovedaDateDefinition;
|
||||
}
|
||||
})(jQuery);
|
||||
|
||||
$(function () {
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
|
||||
|
||||
@ -126,42 +126,6 @@
|
||||
{% macro render_events_sub_menu() %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro render_events(events) %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-bordered table-hover table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ _('Date') }}</th>
|
||||
<th>{{ _('Name') }}</th>
|
||||
<th>{{ _('Host') }}</th>
|
||||
<th>{{ _('Location') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for event in events %}
|
||||
<tr>
|
||||
<td>{{ render_event_date_in_list(event) }}</td>
|
||||
<td>
|
||||
<a href="{{ url_for('event', event_id=event.id) }}">{{ event.name }}</a>
|
||||
{{ render_event_warning_pills(event) }}
|
||||
</td>
|
||||
<td>{{ render_event_organizer(event.organizer) }}</td>
|
||||
<td>{{ render_place(event.event_place) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="list-group my-4">
|
||||
<a href="{{ url_for('events') }}" class="list-group-item list-group-item-action list-group-item-primary">
|
||||
{{ _('Show all events') }}
|
||||
<i class="fa fa-caret-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% endmacro %}
|
||||
|
||||
{% macro render_location_card(location, place=None) %}
|
||||
{% if location %}
|
||||
<div class="card card-body">
|
||||
@ -862,9 +826,16 @@
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro render_event_date_in_list(event) %}
|
||||
{{ render_event_date_instance(event.start, event.allday) }}
|
||||
{% if event.recurrence_rule %}
|
||||
{% macro render_event_date_in_list(event_date) %}
|
||||
{{ render_event_date_instance(event_date.start, event_date.allday) }}
|
||||
{% if event_date.recurrence_rule %}
|
||||
<i class="fas fa-history"></i>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro render_event_in_list(event) %}
|
||||
{{ render_event_date_instance(event.min_start_definition.start, event.min_start_definition.allday) }}
|
||||
{% if event.is_recurring %}
|
||||
<i class="fas fa-history"></i>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
@ -21,12 +21,6 @@ $( function() {
|
||||
var form = $("#main-form");
|
||||
form.validate({
|
||||
rules: {
|
||||
start: {
|
||||
dateRange: ["#start", "#end"]
|
||||
},
|
||||
end: {
|
||||
dateRangeDay: ["#start", "#end"]
|
||||
},
|
||||
event_place_id: {
|
||||
required: {
|
||||
param: true,
|
||||
@ -62,8 +56,7 @@ $( function() {
|
||||
}
|
||||
});
|
||||
|
||||
$("#start-time").rules("add", "time");
|
||||
$("#end-time").rules("add", "time");
|
||||
$('.date-definition-container').ovedaDateDefinition();
|
||||
|
||||
function update_place_container(value) {
|
||||
switch (value) {
|
||||
@ -240,8 +233,6 @@ $( function() {
|
||||
placeholder: "{{ _('Enter organizer') }}"
|
||||
});
|
||||
|
||||
{{ render_end_container_handling() }}
|
||||
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@ -267,15 +258,19 @@ $( function() {
|
||||
{{ _('Event date') }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="single-event-container">
|
||||
{{ render_field_with_errors(form.start, **{"data-range-to":"#end", "data-range-max-days": "14", "data-allday": "#allday"}) }}
|
||||
{{ render_field_with_errors(form.allday, ri="checkbox") }}
|
||||
{{ render_field_with_errors(form.end, is_collapsible=1) }}
|
||||
<button type="button" id="recc-button" class="btn btn-outline-secondary"><i class="fas fa-history"></i> {{ _('Recurring event') }}</button>
|
||||
</div>
|
||||
<div id="recc-event-container">
|
||||
{{ render_field_with_errors(form.recurrence_rule, ri="rrule") }}
|
||||
</div>
|
||||
{% for date_definition in form.date_definitions %}
|
||||
<div class="date-definition-container" data-prefix="{{ date_definition.id }}-">
|
||||
<div id="{{ date_definition.id }}-single-event-container">
|
||||
{{ render_field_with_errors(date_definition.form.start, **{"data-range-to":"#"+date_definition.form.end.id, "data-range-max-days": "14", "data-allday": "#"+date_definition.form.allday.id}) }}
|
||||
{{ render_field_with_errors(date_definition.form.allday, ri="checkbox") }}
|
||||
{{ render_field_with_errors(date_definition.form.end, is_collapsible=1) }}
|
||||
<button type="button" id="{{ date_definition.id }}-recc-button" class="btn btn-outline-secondary"><i class="fas fa-history"></i> {{ _('Recurring event') }}</button>
|
||||
</div>
|
||||
<div id="{{ date_definition.id }}-recc-event-container">
|
||||
{{ render_field_with_errors(date_definition.form.recurrence_rule) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -6,6 +6,6 @@
|
||||
{% block content_container_attribs %}{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
{{ render_event_props_seo(event, event.start, event.end, event.allday, dates, user_rights['can_update_event'], user_rights=user_rights, share_links=share_links) }}
|
||||
{{ render_event_props_seo(event, event.min_start_definition.start, event.min_start_definition.end, event.min_start_definition.allday, dates, user_rights['can_update_event'], user_rights=user_rights, share_links=share_links) }}
|
||||
|
||||
{% endblock %}
|
||||
@ -22,19 +22,12 @@
|
||||
var form = $("#main-form");
|
||||
form.validate({
|
||||
rules: {
|
||||
start: {
|
||||
dateRange: ["#start", "#end"]
|
||||
},
|
||||
end: {
|
||||
dateRangeDay: ["#start", "#end"]
|
||||
},
|
||||
event_place_id: "required",
|
||||
organizer_id: "required"
|
||||
}
|
||||
});
|
||||
|
||||
$("#start-time").rules("add", "time");
|
||||
$("#end-time").rules("add", "time");
|
||||
$('.date-definition-container').ovedaDateDefinition();
|
||||
|
||||
// Organizer
|
||||
var organizer_select =$('#organizer_id');
|
||||
@ -131,8 +124,6 @@
|
||||
},
|
||||
placeholder: "{{ _('Enter organizer') }}"
|
||||
});
|
||||
|
||||
{{ render_end_container_handling() }}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@ -158,15 +149,19 @@
|
||||
{{ _('Event date') }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="single-event-container">
|
||||
{{ render_field_with_errors(form.start, **{"data-range-to":"#end", "data-range-max-days": "14", "data-allday": "#allday"}) }}
|
||||
{{ render_field_with_errors(form.allday, ri="checkbox") }}
|
||||
{{ render_field_with_errors(form.end, is_collapsible=1) }}
|
||||
<button type="button" id="recc-button" class="btn btn-outline-secondary"><i class="fas fa-history"></i> {{ _('Recurring event') }}</button>
|
||||
</div>
|
||||
<div id="recc-event-container">
|
||||
{{ render_field_with_errors(form.recurrence_rule, ri="rrule") }}
|
||||
</div>
|
||||
{% for date_definition in form.date_definitions %}
|
||||
<div class="date-definition-container" data-prefix="{{ date_definition.id }}-">
|
||||
<div id="{{ date_definition.id }}-single-event-container">
|
||||
{{ render_field_with_errors(date_definition.form.start, **{"data-range-to":"#"+date_definition.form.end.id, "data-range-max-days": "14", "data-allday": "#"+date_definition.form.allday.id}) }}
|
||||
{{ render_field_with_errors(date_definition.form.allday, ri="checkbox") }}
|
||||
{{ render_field_with_errors(date_definition.form.end, is_collapsible=1) }}
|
||||
<button type="button" id="{{ date_definition.id }}-recc-button" class="btn btn-outline-secondary"><i class="fas fa-history"></i> {{ _('Recurring event') }}</button>
|
||||
</div>
|
||||
<div id="{{ date_definition.id }}-recc-event-container">
|
||||
{{ render_field_with_errors(date_definition.form.recurrence_rule) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,51 +0,0 @@
|
||||
{% extends "layout.html" %}
|
||||
{% from "_macros.html" import render_place, render_events, render_location_card, render_link_prop, render_image %}
|
||||
{%- block title -%}
|
||||
{{ place.name }}
|
||||
{%- endblock -%}
|
||||
{% block content %}
|
||||
|
||||
<h1>{{ place.name }}</h1>
|
||||
|
||||
{% if can_update_place %}
|
||||
<div class="my-4">
|
||||
<a class="btn btn-primary my-1" href="{{ url_for('place_update', place_id=place.id) }}" role="button"><i class="fa fa-edit"></i> {{ _('Update place') }}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Nav tabs -->
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-toggle="tab" href="#info" role="tab" area-selected="true">{{ _('Info') }}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#events" role="tab">{{ _('Events') }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Tab panes -->
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane pt-4 active" id="info" role="tabpanel">
|
||||
|
||||
{{ render_location_card(place.location, place) }}
|
||||
|
||||
<div class="my-4">
|
||||
{{ render_link_prop(place.url) }}
|
||||
</div>
|
||||
|
||||
{% if place.photo_id %}
|
||||
<div class="my-4">{{ render_image(place.photo) }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if place.description %}
|
||||
<div class="my-4">{{ place.description }}</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
<div class="tab-pane pt-4" id="events" role="tabpanel">
|
||||
{{ render_events(place.events) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@ -1,6 +1,6 @@
|
||||
{% extends "layout.html" %}
|
||||
{% set active_id = "events" %}
|
||||
{% from "_macros.html" import render_event_date_in_list, render_manage_form_styles, render_manage_form_scripts, render_event_dates_filter_form, render_event_warning_pills, render_pagination, render_event_date, render_field_with_errors, render_event_organizer %}
|
||||
{% from "_macros.html" import render_event_in_list, render_manage_form_styles, render_manage_form_scripts, render_event_dates_filter_form, render_event_warning_pills, render_pagination, render_event_date, render_field_with_errors, render_event_organizer %}
|
||||
|
||||
{%- block title -%}
|
||||
{{ _('Events') }}
|
||||
@ -34,7 +34,7 @@
|
||||
<ul class="list-group">
|
||||
{% for event in events %}
|
||||
<li class="list-group-item">
|
||||
{{ render_event_date_in_list(event) }}
|
||||
{{ render_event_in_list(event) }}
|
||||
<div class="dropdown d-inline-block">
|
||||
<button class="btn btn-link dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">{{ event.name }}</button>
|
||||
<div class="dropdown-menu">
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{% extends "layout.html" %}
|
||||
{% set active_id = "reference_requests_incoming" %}
|
||||
{% from "_macros.html" import render_event_date_in_list, render_reference_request_review_status_pill, render_event_date, render_pagination, render_event_organizer %}
|
||||
{% from "_macros.html" import render_event_in_list, render_reference_request_review_status_pill, render_event_date, render_pagination, render_event_organizer %}
|
||||
{%- block title -%}
|
||||
{{ _('Reference requests') }}
|
||||
{%- endblock -%}
|
||||
@ -12,7 +12,7 @@
|
||||
<ul class="list-group mt-4">
|
||||
{% for request in requests %}
|
||||
<li class="list-group-item">
|
||||
{{ render_event_date_in_list(request.event) }}
|
||||
{{ render_event_in_list(request.event) }}
|
||||
<div class="dropdown d-inline-block">
|
||||
<button class="btn btn-link dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">{{ request.event.name }}</button>
|
||||
<div class="dropdown-menu">
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{% extends "layout.html" %}
|
||||
{% set active_id = "reference_requests_outgoing" %}
|
||||
{% from "_macros.html" import render_event_date_in_list, render_reference_request_review_status_pill, render_event_date, render_pagination, render_event_organizer %}
|
||||
{% from "_macros.html" import render_event_in_list, render_reference_request_review_status_pill, render_event_date, render_pagination, render_event_organizer %}
|
||||
{%- block title -%}
|
||||
{{ _('Reference requests') }}
|
||||
{%- endblock -%}
|
||||
@ -12,7 +12,7 @@
|
||||
<ul class="list-group mt-4">
|
||||
{% for request in requests %}
|
||||
<li class="list-group-item">
|
||||
{{ render_event_date_in_list(request.event) }}
|
||||
{{ render_event_in_list(request.event) }}
|
||||
<div class="dropdown d-inline-block">
|
||||
<button class="btn btn-link dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">{{ request.event.name }}</button>
|
||||
<div class="dropdown-menu">
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{% extends "layout.html" %}
|
||||
{% set active_id = "references_incoming" %}
|
||||
{% from "_macros.html" import render_event_date_in_list, render_event_warning_pills, render_event_date, render_pagination, render_event_organizer %}
|
||||
{% from "_macros.html" import render_event_in_list, render_event_warning_pills, render_event_date, render_pagination, render_event_organizer %}
|
||||
{%- block title -%}
|
||||
{{ _('References') }}
|
||||
{%- endblock -%}
|
||||
@ -12,7 +12,7 @@
|
||||
<ul class="list-group mt-4">
|
||||
{% for reference in references %}
|
||||
<li class="list-group-item">
|
||||
{{ render_event_date_in_list(reference.event) }}
|
||||
{{ render_event_in_list(reference.event) }}
|
||||
<div class="dropdown d-inline-block">
|
||||
<button class="btn btn-link dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">{{ reference.event.name }}</button>
|
||||
<div class="dropdown-menu">
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{% extends "layout.html" %}
|
||||
{% set active_id = "references_outgoing" %}
|
||||
{% from "_macros.html" import render_event_date_in_list, render_event_warning_pills, render_event_date, render_pagination, render_event_organizer %}
|
||||
{% from "_macros.html" import render_event_in_list, render_event_warning_pills, render_event_date, render_pagination, render_event_organizer %}
|
||||
{%- block title -%}
|
||||
{{ _('References') }}
|
||||
{%- endblock -%}
|
||||
@ -12,7 +12,7 @@
|
||||
<ul class="list-group mt-4">
|
||||
{% for reference in references %}
|
||||
<li class="list-group-item">
|
||||
{{ render_event_date_in_list(reference.event) }}
|
||||
{{ render_event_in_list(reference.event) }}
|
||||
<div class="dropdown d-inline-block">
|
||||
<button class="btn btn-link dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">{{ reference.event.name }}</button>
|
||||
<div class="dropdown-menu">
|
||||
|
||||
@ -38,7 +38,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ render_event_props(event, event.start, event.end, event.allday) }}
|
||||
{{ render_event_props(event, event.min_start_definition.start, event.min_start_definition.end, event.min_start_definition.allday) }}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@ -24,7 +24,7 @@ $( function() {
|
||||
|
||||
<div class="mt-3 w-normal">
|
||||
|
||||
{{ render_event_props(event, event.start, event.end, event.allday, dates) }}
|
||||
{{ render_event_props(event, event.min_start_definition.start, event.min_start_definition.end, event.min_start_definition.allday, dates) }}
|
||||
|
||||
{% if dates|length > 0 %}
|
||||
<div class="card mt-4">
|
||||
|
||||
@ -202,7 +202,7 @@ def event_update(event_id):
|
||||
event = Event.query.get_or_404(event_id)
|
||||
access_or_401(event.admin_unit, "event:update")
|
||||
|
||||
form = UpdateEventForm(obj=event, start=event.start, end=event.end)
|
||||
form = UpdateEventForm(obj=event)
|
||||
prepare_event_form(form)
|
||||
|
||||
if not form.is_submitted():
|
||||
@ -310,16 +310,16 @@ def prepare_event_form(form):
|
||||
prepare_organizer(form)
|
||||
prepare_event_place(form)
|
||||
|
||||
if not form.start.data:
|
||||
form.start.data = get_next_full_hour()
|
||||
if not form.date_definitions[0].start.data:
|
||||
form.date_definitions[0].start.data = get_next_full_hour()
|
||||
|
||||
|
||||
def prepare_event_form_for_suggestion(form, event_suggestion):
|
||||
form.name.data = event_suggestion.name
|
||||
form.start.data = event_suggestion.start
|
||||
form.end.data = event_suggestion.end
|
||||
form.recurrence_rule.data = event_suggestion.recurrence_rule
|
||||
form.allday.data = event_suggestion.allday
|
||||
form.date_definitions[0].start.data = event_suggestion.start
|
||||
form.date_definitions[0].end.data = event_suggestion.end
|
||||
form.date_definitions[0].recurrence_rule.data = event_suggestion.recurrence_rule
|
||||
form.date_definitions[0].allday.data = event_suggestion.allday
|
||||
form.external_link.data = event_suggestion.external_link
|
||||
form.description.data = event_suggestion.description
|
||||
|
||||
|
||||
@ -221,7 +221,7 @@ def get_calendar_links(event_date: EventDate) -> dict:
|
||||
end_date = event_date.end if event_date.end else start_date
|
||||
date_format = "%Y%m%dT%H%M%S"
|
||||
|
||||
if event_date.event.allday:
|
||||
if event_date.allday:
|
||||
date_format = "%Y%m%d"
|
||||
end_date = round_to_next_day(end_date)
|
||||
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import base64
|
||||
|
||||
import pytest
|
||||
|
||||
from project.models import PublicStatus
|
||||
|
||||
|
||||
@ -134,17 +136,29 @@ def test_dates_myUnverified(client, seeder, utils):
|
||||
|
||||
|
||||
def create_put(
|
||||
place_id, organizer_id, name="Neuer Name", start="2021-02-07T11:00:00.000Z"
|
||||
place_id,
|
||||
organizer_id,
|
||||
name="Neuer Name",
|
||||
start="2021-02-07T11:00:00.000Z",
|
||||
legacy=False,
|
||||
):
|
||||
return {
|
||||
data = {
|
||||
"name": name,
|
||||
"start": start,
|
||||
"place": {"id": place_id},
|
||||
"organizer": {"id": organizer_id},
|
||||
}
|
||||
|
||||
if legacy:
|
||||
data["start"] = start
|
||||
else:
|
||||
data["date_definitions"] = [{"start": start}]
|
||||
|
||||
def test_put(client, seeder, utils, app, mocker):
|
||||
return data
|
||||
|
||||
|
||||
@pytest.mark.parametrize("legacy", [True, False])
|
||||
def test_put(client, seeder, utils, app, mocker, legacy):
|
||||
user_id, admin_unit_id = seeder.setup_api_access()
|
||||
event_id = seeder.create_event(admin_unit_id)
|
||||
place_id = seeder.upsert_default_event_place(admin_unit_id)
|
||||
@ -170,9 +184,11 @@ def test_put(client, seeder, utils, app, mocker):
|
||||
put["booked_up"] = True
|
||||
put["expected_participants"] = 500
|
||||
put["price_info"] = "Erwachsene 5€, Kinder 2€."
|
||||
put["recurrence_rule"] = "RRULE:FREQ=DAILY;COUNT=7"
|
||||
put["public_status"] = "draft"
|
||||
|
||||
if not legacy:
|
||||
put["date_definitions"][0]["recurrence_rule"] = "RRULE:FREQ=DAILY;COUNT=7"
|
||||
|
||||
url = utils.get_url("api_v1_event", id=event_id)
|
||||
response = utils.put_json(url, put)
|
||||
utils.assert_response_no_content(response)
|
||||
@ -207,11 +223,18 @@ def test_put(client, seeder, utils, app, mocker):
|
||||
assert event.booked_up == put["booked_up"]
|
||||
assert event.expected_participants == put["expected_participants"]
|
||||
assert event.price_info == put["price_info"]
|
||||
assert event.recurrence_rule == put["recurrence_rule"]
|
||||
assert event.public_status == PublicStatus.draft
|
||||
|
||||
len_dates = len(event.dates)
|
||||
assert len_dates == 7
|
||||
|
||||
if legacy:
|
||||
assert len_dates == 1
|
||||
else:
|
||||
assert (
|
||||
event.date_definitions[0].recurrence_rule
|
||||
== put["date_definitions"][0]["recurrence_rule"]
|
||||
)
|
||||
assert len_dates == 7
|
||||
|
||||
|
||||
def test_put_invalidRecurrenceRule(client, seeder, utils, app):
|
||||
@ -221,7 +244,7 @@ def test_put_invalidRecurrenceRule(client, seeder, utils, app):
|
||||
organizer_id = seeder.upsert_default_event_organizer(admin_unit_id)
|
||||
|
||||
put = create_put(place_id, organizer_id)
|
||||
put["recurrence_rule"] = "RRULE:FREQ=SCHMAILY;COUNT=7"
|
||||
put["date_definitions"][0]["recurrence_rule"] = "RRULE:FREQ=SCHMAILY;COUNT=7"
|
||||
|
||||
url = utils.get_url("api_v1_event", id=event_id)
|
||||
response = utils.put_json(url, put)
|
||||
@ -365,8 +388,8 @@ def test_put_startAfterEnd(client, seeder, utils, app):
|
||||
organizer_id = seeder.upsert_default_event_organizer(admin_unit_id)
|
||||
|
||||
put = create_put(place_id, organizer_id)
|
||||
put["start"] = "2021-02-07T11:00:00.000Z"
|
||||
put["end"] = "2021-02-07T10:59:00.000Z"
|
||||
put["date_definitions"][0]["start"] = "2021-02-07T11:00:00.000Z"
|
||||
put["date_definitions"][0]["end"] = "2021-02-07T10:59:00.000Z"
|
||||
|
||||
url = utils.get_url("api_v1_event", id=event_id)
|
||||
response = utils.put_json(url, put)
|
||||
@ -380,8 +403,8 @@ def test_put_durationMoreThanMaxAllowedDuration(client, seeder, utils, app):
|
||||
organizer_id = seeder.upsert_default_event_organizer(admin_unit_id)
|
||||
|
||||
put = create_put(place_id, organizer_id)
|
||||
put["start"] = "2021-02-07T11:00:00.000Z"
|
||||
put["end"] = "2021-02-21T11:01:00.000Z"
|
||||
put["date_definitions"][0]["start"] = "2021-02-07T11:00:00.000Z"
|
||||
put["date_definitions"][0]["end"] = "2021-02-21T11:01:00.000Z"
|
||||
|
||||
url = utils.get_url("api_v1_event", id=event_id)
|
||||
response = utils.put_json(url, put)
|
||||
@ -429,7 +452,7 @@ def test_put_dateWithTimezone(client, seeder, utils, app):
|
||||
expected = create_berlin_date(2030, 12, 31, 14, 30)
|
||||
|
||||
event = Event.query.get(event_id)
|
||||
assert event.start == expected
|
||||
assert event.date_definitions[0].start == expected
|
||||
|
||||
|
||||
def test_put_dateWithoutTimezone(client, seeder, utils, app):
|
||||
@ -452,7 +475,7 @@ def test_put_dateWithoutTimezone(client, seeder, utils, app):
|
||||
expected = create_berlin_date(2030, 12, 31, 14, 30)
|
||||
|
||||
event = Event.query.get(event_id)
|
||||
assert event.start == expected
|
||||
assert event.date_definitions[0].start == expected
|
||||
|
||||
|
||||
def test_put_referencedEventUpdate_sendsMail(client, seeder, utils, app, mocker):
|
||||
@ -518,10 +541,15 @@ def test_patch_startAfterEnd(client, seeder, utils, app):
|
||||
event_id = seeder.create_event(admin_unit_id)
|
||||
|
||||
url = utils.get_url("api_v1_event", id=event_id)
|
||||
response = utils.patch_json(url, {"start": "2021-02-07T11:00:00.000Z"})
|
||||
utils.assert_response_no_content(response)
|
||||
response = utils.patch_json(
|
||||
url,
|
||||
{
|
||||
"date_definitions": [
|
||||
{"start": "2021-02-07T11:00:00.000Z", "end": "2021-02-07T10:59:00.000Z"}
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
response = utils.patch_json(url, {"end": "2021-02-07T10:59:00.000Z"})
|
||||
utils.assert_response_bad_request(response)
|
||||
|
||||
|
||||
|
||||
@ -156,7 +156,7 @@ def test_events(client, seeder, utils):
|
||||
assert len(response.json["items"]) == 2
|
||||
|
||||
|
||||
def prepare_events_post_data(seeder, utils):
|
||||
def prepare_events_post_data(seeder, utils, legacy=False):
|
||||
user_id, admin_unit_id = seeder.setup_api_access()
|
||||
place_id = seeder.upsert_default_event_place(admin_unit_id)
|
||||
organizer_id = seeder.upsert_default_event_organizer(admin_unit_id)
|
||||
@ -164,22 +164,28 @@ def prepare_events_post_data(seeder, utils):
|
||||
url = utils.get_url("api_v1_organization_event_list", id=admin_unit_id)
|
||||
data = {
|
||||
"name": "Fest",
|
||||
"start": "2021-02-07T11:00:00.000Z",
|
||||
"place": {"id": place_id},
|
||||
"organizer": {"id": organizer_id},
|
||||
"photo": {"image_base64": seeder.get_default_image_base64()},
|
||||
}
|
||||
|
||||
if legacy:
|
||||
data["start"] = "2021-02-07T11:00:00.000Z"
|
||||
else:
|
||||
data["date_definitions"] = [{"start": "2021-02-07T11:00:00.000Z"}]
|
||||
|
||||
return url, data, admin_unit_id, place_id, organizer_id
|
||||
|
||||
|
||||
@pytest.mark.parametrize("allday", [True, False])
|
||||
def test_events_post(client, seeder, utils, app, allday):
|
||||
@pytest.mark.parametrize("legacy", [True, False])
|
||||
def test_events_post(client, seeder, utils, app, allday, legacy):
|
||||
url, data, admin_unit_id, place_id, organizer_id = prepare_events_post_data(
|
||||
seeder, utils
|
||||
seeder, utils, legacy
|
||||
)
|
||||
|
||||
if allday:
|
||||
data["allday"] = "1"
|
||||
if allday and not legacy:
|
||||
data["date_definitions"][0]["allday"] = "1"
|
||||
|
||||
response = utils.post_json(url, data)
|
||||
utils.assert_response_created(response)
|
||||
@ -199,7 +205,7 @@ def test_events_post(client, seeder, utils, app, allday):
|
||||
assert event.photo is not None
|
||||
assert event.photo.encoding_format == "image/png"
|
||||
assert event.public_status == PublicStatus.published
|
||||
assert event.allday == allday
|
||||
assert event.date_definitions[0].allday == (allday and not legacy)
|
||||
|
||||
|
||||
def test_events_post_co_organizers(client, seeder, utils, app):
|
||||
|
||||
@ -242,7 +242,7 @@ class Seeder(object):
|
||||
client_secret = oauth2_client.client_secret
|
||||
scope = oauth2_client.scope
|
||||
|
||||
self._utils.login()
|
||||
self._utils.login(follow_redirects=False)
|
||||
self._utils.authorize(client_id, client_secret, scope)
|
||||
self._utils.logout()
|
||||
return (user_id, admin_unit_id)
|
||||
@ -285,18 +285,19 @@ class Seeder(object):
|
||||
event.categories = [upsert_event_category("Other")]
|
||||
event.name = name
|
||||
event.description = ("Beschreibung",)
|
||||
event.start = start if start else self.get_now_by_minute()
|
||||
event.end = end
|
||||
event.allday = allday
|
||||
event.event_place_id = self.upsert_default_event_place(admin_unit_id)
|
||||
event.organizer_id = self.upsert_default_event_organizer(admin_unit_id)
|
||||
event.recurrence_rule = recurrence_rule
|
||||
event.external_link = external_link
|
||||
event.ticket_link = ""
|
||||
event.tags = ""
|
||||
event.price_info = ""
|
||||
event.attendance_mode = EventAttendanceMode.offline
|
||||
|
||||
date_definition = self.create_event_date_definition(
|
||||
start, end, allday, recurrence_rule
|
||||
)
|
||||
event.date_definitions = [date_definition]
|
||||
|
||||
if draft:
|
||||
event.public_status = PublicStatus.draft
|
||||
|
||||
@ -311,6 +312,20 @@ class Seeder(object):
|
||||
event_id = event.id
|
||||
return event_id
|
||||
|
||||
def create_event_date_definition(
|
||||
self, start=None, end=None, allday=False, recurrence_rule=""
|
||||
):
|
||||
from project.models import EventDateDefinition
|
||||
|
||||
with self._app.app_context():
|
||||
date_definition = EventDateDefinition()
|
||||
date_definition.start = start if start else self.get_now_by_minute()
|
||||
date_definition.end = end
|
||||
date_definition.allday = allday
|
||||
date_definition.recurrence_rule = recurrence_rule
|
||||
|
||||
return date_definition
|
||||
|
||||
def create_event_unverified(self):
|
||||
user_id = self.create_user("unverified@test.de")
|
||||
admin_unit_id = self.create_admin_unit(user_id, "Unverified Crew")
|
||||
@ -336,7 +351,7 @@ class Seeder(object):
|
||||
{
|
||||
"name": "Name",
|
||||
"description": "Beschreibung",
|
||||
"start": ["2030-12-31", "23:59"],
|
||||
"date_definitions-0-start": ["2030-12-31", "23:59"],
|
||||
"event_place_id": place_id,
|
||||
"organizer_id": organizer_id,
|
||||
"photo-image_base64": self.get_default_image_upload_base64(),
|
||||
|
||||
@ -8,16 +8,17 @@ def test_update_event_dates_with_recurrence_rule(client, seeder, utils, app):
|
||||
from project.services.event import update_event_dates_with_recurrence_rule
|
||||
|
||||
event = Event.query.get(event_id)
|
||||
event.start = create_berlin_date(2030, 12, 31, 14, 30)
|
||||
event.end = create_berlin_date(2030, 12, 31, 16, 30)
|
||||
date_definition = event.date_definitions[0]
|
||||
date_definition.start = create_berlin_date(2030, 12, 31, 14, 30)
|
||||
date_definition.end = create_berlin_date(2030, 12, 31, 16, 30)
|
||||
update_event_dates_with_recurrence_rule(event)
|
||||
|
||||
len_dates = len(event.dates)
|
||||
assert len_dates == 1
|
||||
|
||||
event_date = event.dates[0]
|
||||
assert event_date.start == event.start
|
||||
assert event_date.end == event.end
|
||||
assert event_date.start == date_definition.start
|
||||
assert event_date.end == date_definition.end
|
||||
|
||||
# Update again
|
||||
update_event_dates_with_recurrence_rule(event)
|
||||
@ -26,23 +27,23 @@ def test_update_event_dates_with_recurrence_rule(client, seeder, utils, app):
|
||||
assert len_dates == 1
|
||||
|
||||
event_date = event.dates[0]
|
||||
assert event_date.start == event.start
|
||||
assert event_date.end == event.end
|
||||
assert event_date.start == date_definition.start
|
||||
assert event_date.end == date_definition.end
|
||||
|
||||
# All-day
|
||||
event.allday = True
|
||||
date_definition.allday = True
|
||||
update_event_dates_with_recurrence_rule(event)
|
||||
|
||||
len_dates = len(event.dates)
|
||||
assert len_dates == 1
|
||||
|
||||
event_date = event.dates[0]
|
||||
assert event_date.start == event.start
|
||||
assert event_date.end == event.end
|
||||
assert event_date.start == date_definition.start
|
||||
assert event_date.end == date_definition.end
|
||||
assert event_date.allday
|
||||
|
||||
# Wiederholt sich alle 1 Tage, endet nach 7 Ereigniss(en)
|
||||
event.recurrence_rule = "RRULE:FREQ=DAILY;COUNT=7"
|
||||
date_definition.recurrence_rule = "RRULE:FREQ=DAILY;COUNT=7"
|
||||
|
||||
update_event_dates_with_recurrence_rule(event)
|
||||
|
||||
@ -64,11 +65,12 @@ def test_update_event_dates_with_recurrence_rule_past(
|
||||
utils.mock_now(mocker, 2020, 1, 3)
|
||||
|
||||
event = Event.query.get(event_id)
|
||||
event.start = create_berlin_date(2020, 1, 2, 14, 30)
|
||||
event.end = create_berlin_date(2020, 1, 2, 16, 30)
|
||||
date_definition = event.date_definitions[0]
|
||||
date_definition.start = create_berlin_date(2020, 1, 2, 14, 30)
|
||||
date_definition.end = create_berlin_date(2020, 1, 2, 16, 30)
|
||||
|
||||
# Wiederholt sich alle 1 Tage, endet nach 7 Ereigniss(en)
|
||||
event.recurrence_rule = "RRULE:FREQ=DAILY;COUNT=7"
|
||||
date_definition.recurrence_rule = "RRULE:FREQ=DAILY;COUNT=7"
|
||||
update_event_dates_with_recurrence_rule(event)
|
||||
|
||||
# Es sollen nur 6 Daten vorhanden sein (das erste Date war gestern)
|
||||
@ -95,11 +97,12 @@ def test_update_event_dates_with_recurrence_rule_past_forever(
|
||||
utils.mock_now(mocker, 2020, 1, 3)
|
||||
|
||||
event = Event.query.get(event_id)
|
||||
event.start = create_berlin_date(2019, 1, 1, 14, 30)
|
||||
event.end = create_berlin_date(2019, 1, 1, 16, 30)
|
||||
date_definition = event.date_definitions[0]
|
||||
date_definition.start = create_berlin_date(2019, 1, 1, 14, 30)
|
||||
date_definition.end = create_berlin_date(2019, 1, 1, 16, 30)
|
||||
|
||||
# Wiederholt sich alle 1 Tage (unendlich)
|
||||
event.recurrence_rule = "RRULE:FREQ=DAILY"
|
||||
date_definition.recurrence_rule = "RRULE:FREQ=DAILY"
|
||||
update_event_dates_with_recurrence_rule(event)
|
||||
|
||||
# Es sollen 367 Daten vorhanden sein (Schaltjahr +1)
|
||||
@ -131,11 +134,12 @@ def test_update_event_dates_with_recurrence_rule_exdate(
|
||||
utils.mock_now(mocker, 2021, 6, 1)
|
||||
|
||||
event = Event.query.get(event_id)
|
||||
event.start = create_berlin_date(2021, 4, 21, 17, 0)
|
||||
event.end = create_berlin_date(2021, 4, 21, 18, 0)
|
||||
date_definition = event.date_definitions[0]
|
||||
date_definition.start = create_berlin_date(2021, 4, 21, 17, 0)
|
||||
date_definition.end = create_berlin_date(2021, 4, 21, 18, 0)
|
||||
|
||||
# Wiederholt sich jeden Mittwoch
|
||||
event.recurrence_rule = "RRULE:FREQ=WEEKLY;BYDAY=WE;UNTIL=20211231T000000\nEXDATE:20210216T000000,20210223T000000,20210602T000000"
|
||||
date_definition.recurrence_rule = "RRULE:FREQ=WEEKLY;BYDAY=WE;UNTIL=20211231T000000\nEXDATE:20210216T000000,20210223T000000,20210602T000000"
|
||||
update_event_dates_with_recurrence_rule(event)
|
||||
|
||||
# Das erste Date soll nicht der 02.06. sein (excluded), sondern der 09.06.
|
||||
|
||||
@ -12,6 +12,29 @@ def test_mail_server():
|
||||
app.testing = True
|
||||
|
||||
|
||||
def drop_db(db):
|
||||
db.drop_all()
|
||||
db.engine.execute("DROP TABLE IF EXISTS alembic_version;")
|
||||
|
||||
|
||||
def populate_db(db):
|
||||
sql = """
|
||||
DO $$
|
||||
DECLARE
|
||||
admin_unit_id adminunit.id%TYPE;
|
||||
event_place_id eventplace.id%TYPE;
|
||||
organizer_id eventorganizer.id%TYPE;
|
||||
event_id event.id%TYPE;
|
||||
BEGIN
|
||||
INSERT INTO adminunit (name) VALUES ('Org') RETURNING id INTO admin_unit_id;
|
||||
INSERT INTO eventplace (name, admin_unit_id) VALUES ('Place', admin_unit_id) RETURNING id INTO event_place_id;
|
||||
INSERT INTO eventorganizer (name, admin_unit_id) VALUES ('Organizer', admin_unit_id) RETURNING id INTO organizer_id;
|
||||
INSERT INTO event (name, admin_unit_id, event_place_id, organizer_id, start) VALUES ('Event', admin_unit_id, event_place_id, organizer_id, current_timestamp) RETURNING id INTO event_id;
|
||||
END $$;
|
||||
"""
|
||||
db.engine.execute(sqlalchemy.text(sql).execution_options(autocommit=True))
|
||||
|
||||
|
||||
def test_migrations(app, seeder):
|
||||
from flask_migrate import downgrade, upgrade
|
||||
|
||||
@ -19,8 +42,7 @@ def test_migrations(app, seeder):
|
||||
from project.init_data import create_initial_data
|
||||
|
||||
with app.app_context():
|
||||
db.drop_all()
|
||||
db.engine.execute("DROP TABLE IF EXISTS alembic_version;")
|
||||
drop_db(db)
|
||||
upgrade()
|
||||
create_initial_data()
|
||||
user_id, admin_unit_id = seeder.setup_base()
|
||||
@ -41,25 +63,9 @@ def test_migration_public_status(app, seeder):
|
||||
from project.models import Event, PublicStatus
|
||||
|
||||
with app.app_context():
|
||||
db.drop_all()
|
||||
db.engine.execute("DROP TABLE IF EXISTS alembic_version;")
|
||||
drop_db(db)
|
||||
upgrade(revision="1fb9f679defb")
|
||||
|
||||
sql = """
|
||||
DO $$
|
||||
DECLARE
|
||||
admin_unit_id adminunit.id%TYPE;
|
||||
event_place_id eventplace.id%TYPE;
|
||||
organizer_id eventorganizer.id%TYPE;
|
||||
event_id event.id%TYPE;
|
||||
BEGIN
|
||||
INSERT INTO adminunit (name) VALUES ('Org') RETURNING id INTO admin_unit_id;
|
||||
INSERT INTO eventplace (name, admin_unit_id) VALUES ('Place', admin_unit_id) RETURNING id INTO event_place_id;
|
||||
INSERT INTO eventorganizer (name, admin_unit_id) VALUES ('Organizer', admin_unit_id) RETURNING id INTO organizer_id;
|
||||
INSERT INTO event (name, admin_unit_id, event_place_id, organizer_id, start) VALUES ('Event', admin_unit_id, event_place_id, organizer_id, current_timestamp) RETURNING id INTO event_id;
|
||||
END $$;
|
||||
"""
|
||||
db.engine.execute(sqlalchemy.text(sql).execution_options(autocommit=True))
|
||||
populate_db(db)
|
||||
upgrade()
|
||||
|
||||
events = Event.query.all()
|
||||
@ -67,3 +73,22 @@ END $$;
|
||||
|
||||
for event in events:
|
||||
assert event.public_status == PublicStatus.published
|
||||
|
||||
|
||||
def test_migration_event_definitions(app, seeder):
|
||||
from flask_migrate import upgrade
|
||||
|
||||
from project import db
|
||||
from project.models import Event
|
||||
|
||||
with app.app_context():
|
||||
drop_db(db)
|
||||
upgrade(revision="920329927dc6")
|
||||
populate_db(db)
|
||||
upgrade()
|
||||
|
||||
events = Event.query.all()
|
||||
assert len(events) > 0
|
||||
|
||||
for event in events:
|
||||
assert len(event.date_definitions) == 1
|
||||
|
||||
@ -108,8 +108,9 @@ def test_get_sd_for_event_date(client, app, db, seeder, utils):
|
||||
from project.services.event import update_event
|
||||
|
||||
event = Event.query.get(event_id)
|
||||
event.start = create_berlin_date(2030, 12, 31, 14, 30)
|
||||
event.end = create_berlin_date(2030, 12, 31, 16, 30)
|
||||
date_definition = event.date_definitions[0]
|
||||
date_definition.start = create_berlin_date(2030, 12, 31, 14, 30)
|
||||
date_definition.end = create_berlin_date(2030, 12, 31, 16, 30)
|
||||
event.previous_start_date = create_berlin_date(2030, 12, 30, 14, 30)
|
||||
event.external_link = "www.goslar.de"
|
||||
event.ticket_link = "www.tickets.de"
|
||||
@ -126,8 +127,8 @@ def test_get_sd_for_event_date(client, app, db, seeder, utils):
|
||||
with app.test_request_context():
|
||||
result = get_sd_for_event_date(event_date)
|
||||
|
||||
assert result["startDate"] == event.start
|
||||
assert result["endDate"] == event.end
|
||||
assert result["startDate"] == date_definition.start
|
||||
assert result["endDate"] == date_definition.end
|
||||
assert result["previousStartDate"] == event.previous_start_date
|
||||
assert result["isAccessibleForFree"]
|
||||
assert result["url"][0] == utils.get_url("event_date", id=event_date.id)
|
||||
@ -149,9 +150,10 @@ def test_get_sd_for_event_date_allday(client, app, db, seeder, utils):
|
||||
from project.services.event import update_event
|
||||
|
||||
event = Event.query.get(event_id)
|
||||
event.start = create_berlin_date(2030, 12, 31, 14, 30)
|
||||
event.end = create_berlin_date(2030, 12, 31, 16, 30)
|
||||
event.allday = True
|
||||
date_definition = event.date_definitions[0]
|
||||
date_definition.start = create_berlin_date(2030, 12, 31, 14, 30)
|
||||
date_definition.end = create_berlin_date(2030, 12, 31, 16, 30)
|
||||
date_definition.allday = True
|
||||
|
||||
update_event(event)
|
||||
db.session.commit()
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
from project.models import AdminUnitInvitation, AdminUnitRelation
|
||||
import pytest
|
||||
|
||||
from project.models import EventDateDefinition
|
||||
|
||||
|
||||
def test_location_update_coordinate(client, app, db):
|
||||
@ -26,6 +28,29 @@ def test_event_category(client, app, db, seeder):
|
||||
assert event.category is None
|
||||
|
||||
|
||||
def test_event_properties(client, app, db, seeder):
|
||||
with app.app_context():
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from project.dateutils import create_berlin_date
|
||||
from project.models import Event, EventDateDefinition
|
||||
|
||||
event = Event()
|
||||
assert event.min_start_definition is None
|
||||
assert event.min_start is None
|
||||
assert event.is_recurring is False
|
||||
|
||||
with pytest.raises(IntegrityError) as e:
|
||||
event.validate()
|
||||
assert e.value.orig.message == "At least one date defintion is required."
|
||||
|
||||
start = create_berlin_date(2030, 12, 31, 14, 30)
|
||||
date_definition = EventDateDefinition()
|
||||
date_definition.start = start
|
||||
event.date_definitions = [date_definition]
|
||||
assert event.min_start == start
|
||||
|
||||
|
||||
def test_event_allday(client, app, db, seeder):
|
||||
from project.dateutils import create_berlin_date
|
||||
|
||||
@ -45,9 +70,10 @@ def test_event_allday(client, app, db, seeder):
|
||||
|
||||
# With Start
|
||||
event = Event.query.get(event_with_start_id)
|
||||
assert event.allday
|
||||
assert event.start == create_berlin_date(2030, 12, 31, 0, 0)
|
||||
assert event.end == create_berlin_date(2030, 12, 31, 23, 59, 59)
|
||||
date_definition = event.date_definitions[0]
|
||||
assert date_definition.allday
|
||||
assert date_definition.start == create_berlin_date(2030, 12, 31, 0, 0)
|
||||
assert date_definition.end == create_berlin_date(2030, 12, 31, 23, 59, 59)
|
||||
|
||||
event_date = event.dates[0]
|
||||
assert event_date.allday
|
||||
@ -56,9 +82,10 @@ def test_event_allday(client, app, db, seeder):
|
||||
|
||||
# With Start and End
|
||||
event = Event.query.get(event_with_start_and_end_id)
|
||||
assert event.allday
|
||||
assert event.start == create_berlin_date(2030, 12, 31, 0, 0)
|
||||
assert event.end == create_berlin_date(2031, 1, 1, 23, 59, 59)
|
||||
date_definition = event.date_definitions[0]
|
||||
assert date_definition.allday
|
||||
assert date_definition.start == create_berlin_date(2030, 12, 31, 0, 0)
|
||||
assert date_definition.end == create_berlin_date(2031, 1, 1, 23, 59, 59)
|
||||
|
||||
event_date = event.dates[0]
|
||||
assert event_date.allday
|
||||
@ -100,6 +127,41 @@ def test_admin_unit_relations(client, app, db, seeder):
|
||||
assert len(admin_unit.outgoing_relations) == 0
|
||||
|
||||
|
||||
def test_event_date_defintion_deletion(client, app, db, seeder):
|
||||
_, admin_unit_id = seeder.setup_base(log_in=False)
|
||||
event_id = seeder.create_event(admin_unit_id)
|
||||
|
||||
with app.app_context():
|
||||
from project.models import Event
|
||||
|
||||
# Initial eine Definition
|
||||
event = Event.query.get(event_id)
|
||||
assert len(event.date_definitions) == 1
|
||||
date_definition1 = event.date_definitions[0]
|
||||
|
||||
# Zweite Definition hinzufügen
|
||||
date_definition2 = seeder.create_event_date_definition()
|
||||
db.session.add(date_definition2)
|
||||
event.date_definitions = [date_definition1, date_definition2]
|
||||
db.session.commit()
|
||||
|
||||
event = Event.query.get(event_id)
|
||||
assert len(event.date_definitions) == 2
|
||||
assert len(EventDateDefinition.query.all()) == 2
|
||||
|
||||
# Erste Definition löschen
|
||||
date_definition1, date_definition2 = event.date_definitions
|
||||
date_definition2_id = date_definition2.id
|
||||
|
||||
db.session.delete(date_definition1)
|
||||
db.session.commit()
|
||||
|
||||
event = Event.query.get(event_id)
|
||||
assert len(event.date_definitions) == 1
|
||||
assert len(EventDateDefinition.query.all()) == 1
|
||||
assert event.date_definitions[0].id == date_definition2_id
|
||||
|
||||
|
||||
def test_admin_unit_deletion(client, app, db, seeder):
|
||||
user_id, admin_unit_id = seeder.setup_base(log_in=False)
|
||||
my_event_id = seeder.create_event(admin_unit_id)
|
||||
@ -132,6 +194,8 @@ def test_admin_unit_deletion(client, app, db, seeder):
|
||||
AdminUnitMemberInvitation,
|
||||
AdminUnitRelation,
|
||||
Event,
|
||||
EventDate,
|
||||
EventDateDefinition,
|
||||
EventOrganizer,
|
||||
EventPlace,
|
||||
EventReference,
|
||||
@ -142,12 +206,17 @@ def test_admin_unit_deletion(client, app, db, seeder):
|
||||
|
||||
admin_unit = get_admin_unit_by_id(admin_unit_id)
|
||||
other_admin_unit = get_admin_unit_by_id(other_admin_unit_id)
|
||||
my_event = Event.query.get(my_event_id)
|
||||
date_id = my_event.dates[0].id
|
||||
date_definition_id = my_event.date_definitions[0].id
|
||||
|
||||
db.session.delete(admin_unit)
|
||||
db.session.commit()
|
||||
assert len(other_admin_unit.outgoing_relations) == 0
|
||||
|
||||
assert Event.query.get(my_event_id) is None
|
||||
assert EventDate.query.get(date_id) is None
|
||||
assert EventDateDefinition.query.get(date_definition_id) is None
|
||||
assert AdminUnitRelation.query.get(incoming_relation_id) is None
|
||||
assert AdminUnitRelation.query.get(outgoing_relation_id) is None
|
||||
assert EventReference.query.get(incoming_reference_id) is None
|
||||
@ -194,7 +263,7 @@ def test_admin_unit_verification(client, app, db, seeder):
|
||||
other_admin_unit_id = seeder.create_admin_unit(other_user_id, "Other Crew")
|
||||
|
||||
with app.app_context():
|
||||
from project.models import AdminUnit
|
||||
from project.models import AdminUnit, AdminUnitRelation
|
||||
|
||||
new_admin_unit = AdminUnit()
|
||||
assert not new_admin_unit.is_verified
|
||||
@ -234,6 +303,7 @@ def test_admin_unit_invitations(client, app, db, seeder):
|
||||
invitation_id = seeder.create_admin_unit_invitation(admin_unit_id)
|
||||
|
||||
with app.app_context():
|
||||
from project.models import AdminUnitInvitation
|
||||
from project.services.admin_unit import get_admin_unit_by_id
|
||||
|
||||
admin_unit = get_admin_unit_by_id(admin_unit_id)
|
||||
|
||||
@ -111,10 +111,13 @@ class UtilActions(object):
|
||||
|
||||
return match.group(1).decode("utf-8")
|
||||
|
||||
def get_soup(self, response) -> BeautifulSoup:
|
||||
return BeautifulSoup(response.data, "html.parser")
|
||||
|
||||
def create_form_data(self, response, values: dict) -> dict:
|
||||
from tests.form import Form
|
||||
|
||||
soup = BeautifulSoup(response.data, "html.parser")
|
||||
soup = self.get_soup(response)
|
||||
form = Form(soup.find("form"))
|
||||
return form.fill(values)
|
||||
|
||||
@ -296,17 +299,30 @@ class UtilActions(object):
|
||||
redirect_url = "http://localhost" + self.get_url(endpoint, **values)
|
||||
assert response.headers["Location"] == redirect_url
|
||||
|
||||
def assert_response_contains_alert(self, response, category, message=None):
|
||||
assert response.status_code == 200
|
||||
|
||||
soup = self.get_soup(response)
|
||||
alerts = soup.find_all("div", class_="alert-" + category)
|
||||
assert len(alerts) > 0
|
||||
|
||||
if not message:
|
||||
return
|
||||
|
||||
for alert in alerts:
|
||||
if message in alert.text:
|
||||
return
|
||||
|
||||
assert False, "Alert not found"
|
||||
|
||||
def assert_response_error_message(self, response, message=None):
|
||||
self.assert_response_contains_alert(response, "danger", message)
|
||||
|
||||
def assert_response_db_error(self, response):
|
||||
assert response.status_code == 200
|
||||
assert b"MockException" in response.data
|
||||
self.assert_response_error_message(response, "MockException")
|
||||
|
||||
def assert_response_error_message(self, response, error_message=b"alert-danger"):
|
||||
assert response.status_code == 200
|
||||
assert error_message in response.data
|
||||
|
||||
def assert_response_success_message(self, response, error_message=b"alert-success"):
|
||||
assert response.status_code == 200
|
||||
assert error_message in response.data
|
||||
def assert_response_success_message(self, response, message=None):
|
||||
self.assert_response_contains_alert(response, "success", message)
|
||||
|
||||
def assert_response_permission_missing(self, response, endpoint, **values):
|
||||
self.assert_response_redirect(response, endpoint, **values)
|
||||
|
||||
@ -71,7 +71,7 @@ def test_create(client, app, utils, seeder, mocker, db_error):
|
||||
{
|
||||
"name": "Name",
|
||||
"description": "Beschreibung",
|
||||
"start": ["2030-12-31", "23:59"],
|
||||
"date_definitions-0-start": ["2030-12-31", "23:59"],
|
||||
"event_place_id": place_id,
|
||||
"organizer_id": organizer_id,
|
||||
"photo-image_base64": seeder.get_default_image_upload_base64(),
|
||||
@ -109,9 +109,9 @@ def test_create_allday(client, app, utils, seeder):
|
||||
{
|
||||
"name": "Name",
|
||||
"description": "Beschreibung",
|
||||
"start": ["2030-12-31", "00:00"],
|
||||
"end": ["2030-12-31", "23:59"],
|
||||
"allday": "y",
|
||||
"date_definitions-0-start": ["2030-12-31", "00:00"],
|
||||
"date_definitions-0-end": ["2030-12-31", "23:59"],
|
||||
"date_definitions-0-allday": "y",
|
||||
"event_place_id": place_id,
|
||||
"organizer_id": organizer_id,
|
||||
"photo-image_base64": seeder.get_default_image_upload_base64(),
|
||||
@ -129,7 +129,7 @@ def test_create_allday(client, app, utils, seeder):
|
||||
.first()
|
||||
)
|
||||
assert event is not None
|
||||
assert event.allday
|
||||
assert event.date_definitions[0].allday
|
||||
|
||||
|
||||
def test_create_newPlaceAndOrganizer(client, app, utils, seeder, mocker):
|
||||
@ -144,7 +144,7 @@ def test_create_newPlaceAndOrganizer(client, app, utils, seeder, mocker):
|
||||
{
|
||||
"name": "Name",
|
||||
"description": "Beschreibung",
|
||||
"start": ["2030-12-31", "23:59"],
|
||||
"date_definitions-0-start": ["2030-12-31", "23:59"],
|
||||
"organizer_choice": 2,
|
||||
"new_organizer-name": "Neuer Veranstalter",
|
||||
"event_place_choice": 2,
|
||||
@ -191,7 +191,7 @@ def test_create_missingPlace(client, app, utils, seeder, mocker):
|
||||
{
|
||||
"name": "Name",
|
||||
"description": "Beschreibung",
|
||||
"start": ["2030-12-31", "23:59"],
|
||||
"date_definitions-0-start": ["2030-12-31", "23:59"],
|
||||
},
|
||||
)
|
||||
|
||||
@ -211,7 +211,7 @@ def test_create_missingOrganizer(client, app, utils, seeder, mocker):
|
||||
{
|
||||
"name": "Name",
|
||||
"description": "Beschreibung",
|
||||
"start": ["2030-12-31", "23:59"],
|
||||
"date_definitions-0-start": ["2030-12-31", "23:59"],
|
||||
"event_place_id": place_id,
|
||||
},
|
||||
)
|
||||
@ -233,7 +233,7 @@ def test_create_invalidOrganizer(client, app, utils, seeder, mocker):
|
||||
{
|
||||
"name": "Name",
|
||||
"description": "Beschreibung",
|
||||
"start": ["2030-12-31", "23:59"],
|
||||
"date_definitions-0-start": ["2030-12-31", "23:59"],
|
||||
"event_place_id": place_id,
|
||||
"organizer_id": organizer_id,
|
||||
"co_organizer_ids": [organizer_id],
|
||||
@ -257,7 +257,7 @@ def test_create_invalidDateFormat(client, app, utils, seeder, mocker):
|
||||
{
|
||||
"name": "Name",
|
||||
"description": "Beschreibung",
|
||||
"start": ["2030-12-31", "23:59"],
|
||||
"date_definitions-0-start": ["2030-12-31", "23:59"],
|
||||
"event_place_id": place_id,
|
||||
},
|
||||
)
|
||||
@ -277,8 +277,8 @@ def test_create_startInvalid(client, app, utils, seeder, mocker):
|
||||
response,
|
||||
{
|
||||
"name": "Name",
|
||||
"start": ["31.12.2030", "23:59"],
|
||||
"end": ["2030-12-31", "23:58"],
|
||||
"date_definitions-0-start": ["31.12.2030", "23:59"],
|
||||
"date_definitions-0-end": ["2030-12-31", "23:58"],
|
||||
"event_place_id": place_id,
|
||||
},
|
||||
)
|
||||
@ -289,6 +289,7 @@ def test_create_startInvalid(client, app, utils, seeder, mocker):
|
||||
def test_create_startAfterEnd(client, app, utils, seeder, mocker):
|
||||
user_id, admin_unit_id = seeder.setup_base()
|
||||
place_id = seeder.upsert_default_event_place(admin_unit_id)
|
||||
organizer_id = seeder.upsert_default_event_organizer(admin_unit_id)
|
||||
|
||||
url = utils.get_url("event_create_for_admin_unit_id", id=admin_unit_id)
|
||||
response = utils.get_ok(url)
|
||||
@ -298,15 +299,16 @@ def test_create_startAfterEnd(client, app, utils, seeder, mocker):
|
||||
response,
|
||||
{
|
||||
"name": "Name",
|
||||
"start": ["2030-12-31", "23:59"],
|
||||
"end": ["2030-12-31", "23:58"],
|
||||
"date_definitions-0-start": ["2030-12-31", "23:59"],
|
||||
"date_definitions-0-end": ["2030-12-31", "23:58"],
|
||||
"event_place_id": place_id,
|
||||
"organizer_id": organizer_id,
|
||||
},
|
||||
)
|
||||
|
||||
utils.assert_response_error_message(
|
||||
response,
|
||||
b"Der Start muss vor dem Ende sein",
|
||||
"Der Start muss vor dem Ende sein",
|
||||
)
|
||||
|
||||
|
||||
@ -322,15 +324,15 @@ def test_create_durationMoreThanMaxAllowedDuration(client, app, utils, seeder, m
|
||||
response,
|
||||
{
|
||||
"name": "Name",
|
||||
"start": ["2030-12-30", "12:00"],
|
||||
"end": ["2031-01-13", "12:01"],
|
||||
"date_definitions-0-start": ["2030-12-30", "12:00"],
|
||||
"date_definitions-0-end": ["2031-01-13", "12:01"],
|
||||
"event_place_id": place_id,
|
||||
},
|
||||
)
|
||||
|
||||
utils.assert_response_error_message(
|
||||
response,
|
||||
b"Eine Veranstaltung darf maximal 14 Tage dauern",
|
||||
"Eine Veranstaltung darf maximal 14 Tage dauern",
|
||||
)
|
||||
|
||||
|
||||
@ -360,7 +362,9 @@ def test_duplicate(client, app, utils, seeder, mocker, allday):
|
||||
assert len(events) == 2
|
||||
|
||||
assert events[1].category.name == events[0].category.name
|
||||
assert events[1].allday == events[0].allday
|
||||
assert (
|
||||
events[1].date_definitions[0].allday == events[0].date_definitions[0].allday
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("free_text", [True, False])
|
||||
@ -388,7 +392,7 @@ def test_create_fromSuggestion(client, app, utils, seeder, mocker, free_text, al
|
||||
.first()
|
||||
)
|
||||
assert event is not None
|
||||
assert event.allday == allday
|
||||
assert event.date_definitions[0].allday == allday
|
||||
|
||||
suggestion = EventSuggestion.query.get(suggestion_id)
|
||||
assert suggestion is not None
|
||||
|
||||
@ -99,7 +99,7 @@ def test_delete(client, seeder, utils, app, mocker, db_error, non_match):
|
||||
|
||||
if non_match:
|
||||
utils.assert_response_error_message(
|
||||
response, b"Der eingegebene Name entspricht nicht dem Namen des Ortes"
|
||||
response, "Der eingegebene Name entspricht nicht dem Namen des Ortes"
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
@ -120,7 +120,7 @@ def test_delete(client, seeder, utils, app, mocker, db_error, non_match):
|
||||
if non_match:
|
||||
utils.assert_response_error_message(
|
||||
response,
|
||||
b"Der eingegebene Name entspricht nicht dem Namen des Veranstalters",
|
||||
"Der eingegebene Name entspricht nicht dem Namen des Veranstalters",
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
@ -51,17 +51,18 @@ def test_get_calendar_links(client, seeder, utils, app, db, mocker):
|
||||
utils.mock_now(mocker, 2020, 1, 3)
|
||||
|
||||
event = Event.query.get(event_id)
|
||||
date_definition = event.date_definitions[0]
|
||||
|
||||
event.end = None
|
||||
date_definition.end = None
|
||||
event.attendance_mode = EventAttendanceMode.online
|
||||
event_date = event.dates[0]
|
||||
links = get_calendar_links(event_date)
|
||||
assert "&location" not in links["google"]
|
||||
|
||||
# All-day single day
|
||||
event.start = create_berlin_date(2020, 1, 2, 14, 30)
|
||||
event.end = None
|
||||
event.allday = True
|
||||
date_definition.start = create_berlin_date(2020, 1, 2, 14, 30)
|
||||
date_definition.end = None
|
||||
date_definition.allday = True
|
||||
update_event_dates_with_recurrence_rule(event)
|
||||
db.session.commit()
|
||||
event_date = event.dates[0]
|
||||
@ -69,9 +70,9 @@ def test_get_calendar_links(client, seeder, utils, app, db, mocker):
|
||||
assert "&dates=20200102/20200103&" in links["google"]
|
||||
|
||||
# All-day multiple days
|
||||
event.start = create_berlin_date(2020, 1, 2, 14, 30)
|
||||
event.end = create_berlin_date(2020, 1, 3, 14, 30)
|
||||
event.allday = True
|
||||
date_definition.start = create_berlin_date(2020, 1, 2, 14, 30)
|
||||
date_definition.end = create_berlin_date(2020, 1, 3, 14, 30)
|
||||
date_definition.allday = True
|
||||
update_event_dates_with_recurrence_rule(event)
|
||||
db.session.commit()
|
||||
event_date = event.dates[0]
|
||||
|
||||
@ -264,6 +264,32 @@ def test_event_suggestion_create_for_admin_unit_allday(
|
||||
)
|
||||
|
||||
|
||||
def test_event_suggestion_create_for_admin_unit_startAfterEnd(
|
||||
client, app, seeder, utils, mocker
|
||||
):
|
||||
user_id = seeder.create_user()
|
||||
seeder.create_admin_unit(user_id, "Meine Crew")
|
||||
au_short_name = "meinecrew"
|
||||
|
||||
url = utils.get_url(
|
||||
"event_suggestion_create_for_admin_unit", au_short_name=au_short_name
|
||||
)
|
||||
response = utils.get_ok(url)
|
||||
|
||||
data = get_create_data()
|
||||
data["end"] = ["2030-12-31", "23:58"]
|
||||
|
||||
response = utils.post_form(
|
||||
url,
|
||||
response,
|
||||
data,
|
||||
)
|
||||
utils.assert_response_error_message(
|
||||
response,
|
||||
"Der Start muss vor dem Ende sein",
|
||||
)
|
||||
|
||||
|
||||
def test_event_suggestion_create_for_admin_unit_emptyFreeText(
|
||||
client, app, seeder, utils, mocker
|
||||
):
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user