Merge pull request #332 from DanielGrams/issue/331

Datenmodell und API für EventDateDefinitions vorbereiten #331
This commit is contained in:
Daniel Grams 2021-11-10 20:35:38 +01:00 committed by GitHub
commit eed987524a
44 changed files with 1035 additions and 491 deletions

View File

@ -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");

View File

@ -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",

View File

@ -19,7 +19,7 @@ describe("Manage", () => {
});
});
it.skip("Events", () => {
it("Events", () => {
cy.login();
cy.createAdminUnit().then(function (adminUnitId) {
cy.createEvent(adminUnitId).then(function (eventId) {

View File

@ -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");
}
);

View 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 ###

View File

@ -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)

View 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()

View File

@ -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()

View File

@ -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)

View File

@ -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):

View File

@ -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(

View File

@ -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:

View File

@ -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"),)

View File

@ -301,27 +301,30 @@ 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
dates_to_add = list()
dates_to_remove = list(event.dates)
for date_definition in event.date_definitions:
sanitize_allday_instance(date_definition)
start = date_definition.start
end = date_definition.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)
if date_definition.recurrence_rule:
rr_dates = dates_from_recurrence_rule(
start, date_definition.recurrence_rule
)
else:
rr_dates = [start]
@ -341,7 +344,7 @@ def update_event_dates_with_recurrence_rule(event):
for date in event.dates
if date.start == rr_date_start
and date.end == rr_date_end
and date.allday == event.allday
and date.allday == date_definition.allday
),
None,
)
@ -352,7 +355,7 @@ def update_event_dates_with_recurrence_rule(event):
event_id=event.id,
start=rr_date_start,
end=rr_date_end,
allday=event.allday,
allday=date_definition.allday,
)
dates_to_add.append(new_date)

View File

@ -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();
});

View File

@ -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();

View File

@ -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 %}

View File

@ -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,16 +258,20 @@ $( 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>
{% 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="recc-event-container">
{{ render_field_with_errors(form.recurrence_rule, ri="rrule") }}
<div id="{{ date_definition.id }}-recc-event-container">
{{ render_field_with_errors(date_definition.form.recurrence_rule) }}
</div>
</div>
{% endfor %}
</div>
</div>
<div class="card mb-4">

View File

@ -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 %}

View File

@ -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,16 +149,20 @@
{{ _('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>
{% 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="recc-event-container">
{{ render_field_with_errors(form.recurrence_rule, ri="rrule") }}
<div id="{{ date_definition.id }}-recc-event-container">
{{ render_field_with_errors(date_definition.form.recurrence_rule) }}
</div>
</div>
{% endfor %}
</div>
</div>
<div class="card mb-4">

View File

@ -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 %}

View File

@ -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">

View File

@ -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">

View File

@ -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">

View File

@ -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">

View File

@ -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">

View File

@ -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 %}

View File

@ -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">

View File

@ -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

View File

@ -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)

View File

@ -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,10 +223,17 @@ 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)
if legacy:
assert len_dates == 1
else:
assert (
event.date_definitions[0].recurrence_rule
== put["date_definitions"][0]["recurrence_rule"]
)
assert len_dates == 7
@ -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)

View File

@ -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):

View File

@ -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(),

View File

@ -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.

View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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]

View File

@ -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
):