Veranstaltungen mit mehreren Terminen und unterschiedlichen Uhrzeiten #333

This commit is contained in:
Daniel Grams 2021-11-15 16:00:20 +01:00
parent eed987524a
commit 1167f2ce8d
22 changed files with 1128 additions and 920 deletions

View File

@ -8,6 +8,9 @@ describe("Event", () => {
cy.get("#name").type("Stadtfest");
cy.checkEventStartEnd(false, test.recurrence, "date_definitions-0-");
cy.get("#add-date-defintion-btn").click();
cy.checkEventStartEnd(false, test.recurrence, "date_definitions-1-");
cy.select2("event_place_id", "Neu");
cy.get("#new_event_place-location-city").type("Goslar");
cy.get("#new_place_container_search_link").click();
@ -28,6 +31,9 @@ describe("Event", () => {
cy.contains("a", "Veranstaltung bearbeiten").click();
cy.url().should("include", "/update");
cy.checkEventStartEnd(true, test.recurrence, "date_definitions-0-");
cy.get('div[data-prefix=date_definitions-1-] .remove-date-defintion-btn:visible').click()
cy.get("#submit").click();
cy.url().should(
"include",

View File

@ -223,7 +223,7 @@ Cypress.Commands.add(
if (update && recurrence) {
cy.get('#' + prefix + 'single-event-container').should("not.be.visible");
cy.get('#' + prefix + 'recc-event-container').should("be.visible");
cy.get('[name="riedit"]').click();
cy.get('div[data-prefix=' + prefix + '] [name="riedit"]').click();
} else {
cy.checkEventAllday(prefix);
@ -233,8 +233,8 @@ Cypress.Commands.add(
cy.get("#ui-datepicker-div").should("not.be.visible");
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-timepicker-wrapper:visible").should("be.visible");
cy.get(".ui-timepicker-wrapper:visible .ui-timepicker-am[data-time=0]").click(); // select 00:00
cy.get("#ui-datepicker-div").should("not.be.visible");
cy.get('#' + prefix + 'end-container').should("not.be.visible");
@ -254,17 +254,17 @@ Cypress.Commands.add(
}
cy.get(".modal-recurrence").should("be.visible");
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.inputsShouldHaveSameValue('#' + prefix + 'start-user', '#' + prefix + 'recc-start-user');
cy.inputsShouldHaveSameValue('#' + prefix + 'start-time', '#' + prefix + 'recc-start-time');
cy.get('#' + prefix + 'rirtemplate option').should("have.length", 4);
cy.get(".modal-recurrence:visible input[value=BYENDDATE]").should("be.checked");
cy.get(".modal-recurrence:visible .modal-footer .btn-primary").click();
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('[name="ridelete"]:visible').click();
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");
@ -284,14 +284,14 @@ Cypress.Commands.add(
// Recurrence
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");
cy.get('#recc-fo-end-time').should("not.be.visible");
cy.get('#' + prefix + 'recc-allday').should("be.checked");
cy.get('#' + prefix + 'recc-start-time').should("not.be.visible");
cy.get('#' + prefix + 'recc-fo-end-time').should("not.be.visible");
cy.get('#recc-allday').click();
cy.get('#recc-start-time').should("be.visible");
cy.get('#recc-fo-end-time').should("be.visible");
cy.get(".modal-recurrence .modal-footer .btn-secondary").click();
cy.get('#' + prefix + 'recc-allday').click();
cy.get('#' + prefix + 'recc-start-time').should("be.visible");
cy.get('#' + prefix + 'recc-fo-end-time').should("be.visible");
cy.get(".modal-recurrence:visible .modal-footer .btn-secondary").click();
// Turn off
cy.get('#' + prefix + 'allday').click();

File diff suppressed because it is too large Load Diff

View File

@ -37,13 +37,13 @@ class EventDateDefinitionFormMixin:
start = CustomDateTimeField(
lazy_gettext("Start"),
validators=[DataRequired()],
description=lazy_gettext("Indicate when the event will take place."),
description=lazy_gettext("Indicate when the event date will start."),
)
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."
"Indicate when the event date will end. An event can last a maximum of 14 days."
),
)
allday = BooleanField(
@ -235,6 +235,9 @@ class BaseEventForm(SharedEventForm):
FormField(EventDateDefinitionForm, default=lambda: EventDateDefinition()),
min_entries=1,
)
date_definition_template = FormField(
EventDateDefinitionForm, default=lambda: EventDateDefinition()
)
previous_start_date = CustomDateTimeField(
lazy_gettext("Previous start date"),
validators=[Optional()],
@ -330,6 +333,8 @@ class CreateEventForm(BaseEventForm):
field.populate_obj(obj, "organizer")
elif name == "photo" and not obj.photo:
obj.photo = Image()
elif name == "date_definition_template":
continue
field.populate_obj(obj, name)
obj.public_status = (
@ -405,6 +410,8 @@ class UpdateEventForm(BaseEventForm):
for name, field in self._fields.items():
if name == "photo" and not obj.photo:
obj.photo = Image()
elif name == "date_definition_template":
continue
field.populate_obj(obj, name)
if obj.photo and obj.photo.is_empty():

View File

@ -909,9 +909,10 @@ class Event(db.Model, TrackableMixin, EventMixin):
@min_start.expression
def min_start(cls):
return (
select([EventDateDefinition.end])
select([EventDateDefinition.start])
.where(EventDateDefinition.event_id == cls.id)
.order_by(EventDateDefinition.start)
.limit(1)
.as_scalar()
)
@ -938,6 +939,7 @@ class Event(db.Model, TrackableMixin, EventMixin):
date_definitions = relationship(
"EventDateDefinition",
order_by="EventDateDefinition.start",
backref=backref("event", lazy=False),
cascade="all, delete-orphan",
)

View File

@ -349,6 +349,7 @@ def update_event_dates_with_recurrence_rule(event):
None,
)
if existing_date:
if existing_date in dates_to_remove:
dates_to_remove.remove(existing_date)
else:
new_date = EventDate(

View File

@ -108,3 +108,7 @@ div.errorarea {
font-weight: bold;
padding: 2px 10px;
}
.recc-event-container .form-group {
margin-bottom: 0;
}

View File

@ -207,6 +207,7 @@
reccStartTime: 'Begin',
reccFoEndTime: 'End',
reccAllDay: 'All day',
removeEventDate: 'Remove event date',
});
@ -257,15 +258,16 @@
$.templates('occurrenceTmpl', OCCURRENCETMPL);
var DISPLAYTMPL = ['<div class="ridisplay">',
'<div class="rimain bg-light mt-3 p-3 rounded border">',
'<div class="rimain">',
'<div class="mb-2">',
'<div class="ridisplay-start"></div>',
'<div class="ridisplay-times"></div>',
'<div class="ridisplay">{{:i18n.displayUnactivate}}</div>',
'</div>',
'{{if !readOnly}}',
'<button type="button" name="riedit" class="btn btn-outline-secondary">{{:i18n.add_rules}}</button>',
'<button type="button" name="ridelete" class="btn btn-outline-secondary" style="display:none">{{:i18n.delete_rules}}</button>',
'<button type="button" name="riedit" class="btn btn-outline-secondary"><i class="fa fa-plus"></i> {{:i18n.add_rules}}</button>',
'<button type="button" name="ridelete" class="btn btn-outline-secondary" style="display:none"><i class="fa fa-times-circle"></i> {{:i18n.delete_rules}}</button>',
'<button type="button" class="btn btn-outline-secondary remove-date-defintion-btn d-none"><i class="fa fa-calendar-minus"></i> {{:i18n.removeEventDate}}</button>',
'{{/if}}',
'</div>',
'<div class="rioccurrences" style="display:none" /></div>'].join('\n');
@ -284,37 +286,37 @@
'<div class="modal-body">',
'<div class="riform">',
'<form>',
'<div id="messagearea" style="display: none;">',
'<div id="{{:prefix}}messagearea" style="display: none;">',
'</div>',
'<div class="form-row">',
'<div class="form-group col-md-4">',
'<label class="mb-0" for="recc-start">{{:i18n.reccStart}}</label>',
'<input type="text" class="form-control datepicker" data-range-to="#recc-end" data-allday="#recc-allday" id="recc-start" name="recc-start" required="" />',
'<label class="mb-0" for="{{:prefix}}recc-start">{{:i18n.reccStart}}</label>',
'<input type="text" class="form-control datepicker" data-range-to="#{{:prefix}}recc-end" data-allday="#{{:prefix}}recc-allday" id="{{:prefix}}recc-start" name="recc-start" required="" />',
'</div>',
'<div class="form-group col-md-4" id="recc-start-time-group">',
'<label class="mb-0" for="recc-start-time">{{:i18n.reccStartTime}}</label>',
'<input type="text" class="form-control timepicker" id="recc-start-time" name="recc-start-time" required="" />',
'<div class="form-group col-md-4" id="{{:prefix}}recc-start-time-group">',
'<label class="mb-0" for="{{:prefix}}recc-start-time">{{:i18n.reccStartTime}}</label>',
'<input type="text" class="form-control timepicker" id="{{:prefix}}recc-start-time" name="recc-start-time" required="" />',
'</div>',
'<div class="form-group col-md-4" id="recc-fo-end-time-group">',
'<label class="mb-0" for="recc-fo-end-time">{{:i18n.reccFoEndTime}}</label>',
'<input type="text" class="form-control timepicker" id="recc-fo-end-time" name="recc-fo-end-time" />',
'<div class="form-group col-md-4" id="{{:prefix}}recc-fo-end-time-group">',
'<label class="mb-0" for="{{:prefix}}recc-fo-end-time">{{:i18n.reccFoEndTime}}</label>',
'<input type="text" class="form-control timepicker" id="{{:prefix}}recc-fo-end-time" name="recc-fo-end-time" />',
'</div>',
'</div>',
'<div class="form-row">',
'<div class="form-group col-md">',
'<div class="form-check">',
'<input class="form-check-input" id="recc-allday" name="recc-allday" type="checkbox" value="y">',
'<label class="form-check-label" for="recc-allday">{{:i18n.reccAllDay}}</label>',
'<input class="form-check-input" id="{{:prefix}}recc-allday" name="recc-allday" type="checkbox" value="y">',
'<label class="form-check-label" for="{{:prefix}}recc-allday">{{:i18n.reccAllDay}}</label>',
'</div>',
'</div>',
'</div>',
'<div class="form-row">',
'<div id="rirangeoptions" class="form-group col-md">',
'<div id="{{:prefix}}rirangeoptions" class="form-group col-md">',
'<label class="mb-0">{{:i18n.range}}</label>',
'<div class="input-group mb-1">',
'<div class="input-group-prepend">',
@ -325,14 +327,14 @@
'value="BYENDDATE"',
'name="rirangetype"',
'class="form-check-inline"',
'id="{{:name}}rangetype:BYENDDATE"/>',
'id="{{:prefix}}{{:name}}rangetype:BYENDDATE"/>',
'{{:i18n.rangeByEndDate}}',
'</div>',
'</div>',
'<input',
'type="text"',
'class="form-control"',
'name="rirangebyenddatecalendar" id="recc-end" />',
'name="rirangebyenddatecalendar" id="{{:prefix}}recc-end" />',
'</div>',
'<div class="input-group mb-1">',
'<div class="input-group-prepend">',
@ -342,7 +344,7 @@
'value="BYOCCURRENCES"',
'name="rirangetype"',
'class="form-check-inline"',
'id="{{:name}}rangetype:BYOCCURRENCES"/>',
'id="{{:prefix}}{{:name}}rangetype:BYOCCURRENCES"/>',
'{{:i18n.rangeByOccurrences1}}',
'</div>',
'</div>',
@ -365,7 +367,7 @@
'value="NOENDDATE"',
'name="rirangetype"',
'class="form-check-inline"',
'id="{{:name}}rangetype:NOENDDATE"/>',
'id="{{:prefix}}{{:name}}rangetype:NOENDDATE"/>',
'{{:i18n.rangeNoEnd}}',
'</div>',
'</div>',
@ -378,7 +380,7 @@
'<label for="{{:name}}rtemplate" class="mb-0">',
'{{:i18n.recurrenceType}}',
'</label>',
'<select id="rirtemplate" name="rirtemplate" class="form-control">',
'<select id="{{:prefix}}rirtemplate" name="rirtemplate" class="form-control">',
'{{props rtemplate}}',
'<option value="{{>key}}">{{:~root.i18n.rtemplate[key]}}</value>',
'{{/props}}',
@ -386,7 +388,7 @@
'</div>',
'<div class="col-md">',
'<div id="ridailyinterval" class="form-group rifield">',
'<div id="{{:prefix}}ridailyinterval" class="form-group rifield">',
'<label for="{{:name}}dailyinterval" class="mb-0">',
'{{:i18n.dailyInterval1}}',
'</label>',
@ -395,7 +397,7 @@
'value="1"',
'name="ridailyinterval"',
'class="form-control"',
'id="{{:name}}dailyinterval" />',
'id="{{:prefix}}{{:name}}dailyinterval" />',
'<div class="input-group-append">',
'<span class="input-group-text">',
'{{:i18n.dailyInterval2}}',
@ -403,7 +405,7 @@
'</div>',
'</div>',
'</div>',
'<div id="riweeklyinterval" class="form-group rifield">',
'<div id="{{:prefix}}riweeklyinterval" class="form-group rifield">',
'<label for="{{:name}}weeklyinterval" class="mb-0">',
'{{:i18n.weeklyInterval1}}',
'</label>',
@ -412,7 +414,7 @@
'value="1"',
'name="riweeklyinterval"',
'class="form-control"',
'id="{{:name}}riweeklyinterval" />',
'id="{{:prefix}}{{:name}}riweeklyinterval" />',
'<div class="input-group-append">',
'<span class="input-group-text">',
'{{:i18n.weeklyInterval2}}',
@ -420,7 +422,7 @@
'</div>',
'</div>',
'</div>',
'<div id="rimonthlyinterval" class="form-group rifield">',
'<div id="{{:prefix}}rimonthlyinterval" class="form-group rifield">',
'<label for="rimonthlyinterval" class="mb-0">{{:i18n.monthlyInterval1}}</label>',
'<div class="input-group">',
'<input type="text" size="2"',
@ -434,7 +436,7 @@
'</div>',
'</div>',
'</div>',
'<div id="riyearlyinterval" class="form-group rifield">',
'<div id="{{:prefix}}riyearlyinterval" class="form-group rifield">',
'<label for="riyearlyinterval" class="mb-0">{{:i18n.yearlyInterval1}}</label>',
'<div class="input-group">',
'<input type="text" size="2"',
@ -451,14 +453,14 @@
'</div>',
'</div>',
'<div id="riweeklyweekdays" class="form-group rifield">',
'<div id="{{:prefix}}riweeklyweekdays" class="form-group rifield">',
'<label for="{{:name}}weeklyinterval" class="mb-0">{{:i18n.weeklyWeekdays}}</label>',
'<div>',
'{{for orderedWeekdays itemVar=\'~value\'}}',
'<div class="form-check form-check-inline">',
'<input type="checkbox"',
'name="riweeklyweekdays{{:~root.weekdays[~value]}}"',
'id="{{:name}}weeklyWeekdays{{:~root.weekdays[~value]}}"',
'id="{{:prefix}}{{:name}}weeklyWeekdays{{:~root.weekdays[~value]}}"',
'class="form-check-input"',
'value="{{:~root.weekdays[~value]}}" />',
'<label for="{{:name}}weeklyWeekdays{{:~root.weekdays[~value]}}" class="form-check-label">{{:~root.i18n.shortWeekdays[~value]}}</label>',
@ -466,7 +468,7 @@
'{{/for}}',
'</div>',
'</div>',
'<div id="rimonthlyoptions" class="form-group rifield">',
'<div id="{{:prefix}}rimonthlyoptions" class="form-group rifield">',
'<label for="rimonthlytype" class="mb-0">{{:i18n.monthlyRepeatOn}}</label>',
'<div>',
'<div class="input-group mb-1">',
@ -477,12 +479,12 @@
'value="DAYOFMONTH"',
'name="rimonthlytype"',
'class="form-check-inline"',
'id="{{:name}}monthlytype:DAYOFMONTH" />',
'id="{{:prefix}}{{:name}}monthlytype:DAYOFMONTH" />',
'{{:i18n.monthlyDayOfMonth1}}',
'</div>',
'</div>',
'<select name="rimonthlydayofmonthday" class="form-control"',
'id="{{:name}}monthlydayofmonthday">',
'id="{{:prefix}}{{:name}}monthlydayofmonthday">',
'{{for start=1 end=32}}',
'<option value="{{:}}">{{:}}</option>',
'{{/for}}',
@ -501,7 +503,7 @@
'value="WEEKDAYOFMONTH"',
'name="rimonthlytype"',
'class="form-check-inline"',
'id="{{:name}}monthlytype:WEEKDAYOFMONTH" />',
'id="{{:prefix}}{{:name}}monthlytype:WEEKDAYOFMONTH" />',
'{{:i18n.monthlyWeekdayOfMonth1}}',
'</div>',
'</div>',
@ -523,7 +525,7 @@
'</div>',
'</div>',
'</div>',
'<div id="riyearlyoptions" class="form-group rifield">',
'<div id="{{:prefix}}riyearlyoptions" class="form-group rifield">',
'<label for="riyearlyType" class="mb-0">{{:i18n.yearlyRepeatOn}}</label>',
'<div>',
'<div class="input-group mb-1">',
@ -534,7 +536,7 @@
'value="DAYOFMONTH"',
'name="riyearlyType"',
'class="form-check-inline"',
'id="{{:name}}yearlytype:DAYOFMONTH" />',
'id="{{:prefix}}{{:name}}yearlytype:DAYOFMONTH" />',
'{{:i18n.yearlyDayOfMonth1}}',
'</div>',
'</div>',
@ -557,7 +559,7 @@
'value="WEEKDAYOFMONTH"',
'name="riyearlyType"',
'class="form-check-inline"',
'id="{{:name}}yearlytype:WEEKDAYOFMONTH"/>',
'id="{{:prefix}}{{:name}}yearlytype:WEEKDAYOFMONTH"/>',
'{{:i18n.yearlyWeekdayOfMonth1}}',
'</div>',
'</div>',
@ -585,10 +587,10 @@
'</div>',
'</div>',
'<div id="occurences-show-container">',
'<a href="#" class="show-link" data-container="occurences-container" data-show-container="occurences-show-container"><i class="fa fa-chevron-down"></i> {{:i18n.preview}}</a>',
'<div id="{{:prefix}}occurences-show-container">',
'<a href="#" class="show-link" data-container="{{:prefix}}occurences-container" data-show-container="{{:prefix}}occurences-show-container"><i class="fa fa-chevron-down"></i> {{:i18n.preview}}</a>',
'</div>',
'<div id="occurences-container" style="display: none;">',
'<div id="{{:prefix}}occurences-container" style="display: none;">',
'<div class="rioccurrencesactions">',
'<div class="rioccurancesheader">',
'<h2 class="my-2">{{:i18n.preview}}',
@ -609,15 +611,15 @@
'<div class="input-group-prepend">',
'<span class="input-group-text">{{:i18n.addDate}}</span>',
'</div>',
'<input type="text" class="form-control" name="adddate" id="adddate" />',
'<input type="text" class="form-control" name="adddate" id="{{:prefix}}adddate" />',
'<div class="input-group-append">',
'<input type="button" class="btn btn-outline-secondary" name="addoccurencebtn" id="addoccurencebtn" value="{{:i18n.add}}" />',
'<input type="button" class="btn btn-outline-secondary" name="addoccurencebtn" id="{{:prefix}}addoccurencebtn" value="{{:i18n.add}}" />',
'</div>',
'</div>',
'</div>',
'</div>',
'<div class="mt-3" id="occurences-hide-container">',
'<a href="#" class="hide-link" data-container="occurences-container" data-show-container="occurences-show-container"><i class="fa fa-chevron-up"></i> {{:i18n.preview}}</a>',
'<div class="mt-3" id="{{:prefix}}occurences-hide-container">',
'<a href="#" class="hide-link" data-container="{{:prefix}}occurences-container" data-show-container="{{:prefix}}occurences-show-container"><i class="fa fa-chevron-up"></i> {{:i18n.preview}}</a>',
'</div>',
'</div>',
'</form>',
@ -649,9 +651,9 @@
var day, month, year, interval, yearlyType, occurrences, date;
for (i = 0; i < rtemplate.fields.length; i++) {
field = form.find('#' + rtemplate.fields[i]);
field = form.find('#' + conf.prefix + rtemplate.fields[i]);
switch (field.attr('id')) {
switch (rtemplate.fields[i]) {
case 'ridailyinterval':
interval = field.find('input[name=ridailyinterval]').val();
@ -1017,8 +1019,8 @@
}
for (i = 0; i < rtemplate.fields.length; i++) {
field = form.find('#' + rtemplate.fields[i]);
switch (field.attr('id')) {
field = form.find('#' + conf.prefix + rtemplate.fields[i]);
switch (rtemplate.fields[i]) {
case 'ridailyinterval':
field.find('input[name=ridailyinterval]').val(interval);
@ -1150,7 +1152,7 @@
}
}
var messagearea = form.find('#messagearea');
var messagearea = form.find('#' + conf.prefix + 'messagearea');
if (unsupportedFeatures.length !== 0) {
messagearea.text(conf.i18n.unsupportedFeatures + ' ' + unsupportedFeatures.join('; '));
messagearea.show();
@ -1200,7 +1202,7 @@
if (value) {
var rtemplate = conf.rtemplate[value];
for (i = 0; i < rtemplate.fields.length; i++) {
form.find('#' + rtemplate.fields[i]).show();
form.find('#' + conf.prefix + rtemplate.fields[i]).show();
}
}
}
@ -1241,7 +1243,7 @@
function occurrenceAdd(event) {
event.preventDefault();
var dateinput = form
.find('.riaddoccurrence input#adddate');
.find('.riaddoccurrence input#' + conf.prefix + 'adddate');
var datevalue = $.datepicker.formatDate("yymmdd", dateinput.data('picker').datepicker("getDate")) + 'T000000';
if (form.ical.RDATE === undefined) {
form.ical.RDATE = [];
@ -1382,7 +1384,7 @@
var startdate = null;
var startField, startFieldYear, startFieldMonth, startFieldDay;
var startField = 'recc-start'; // conf.startField;
var startField = conf.prefix + 'recc-start'; // conf.startField;
// Find the default byday and bymonthday from the start date, if any:
if (startField) {
@ -1541,7 +1543,7 @@
// if (startdate !== null) {
// loadOccurrences(startdate, widgetSaveToRfc5545(form, conf, false).result, 0, true);
// }
display.find('button[name="riedit"]').text(conf.i18n.edit_rules);
display.find('button[name="riedit"]').html('<i class="fa fa-edit"></i> ' + conf.i18n.edit_rules);
display.find('button[name="ridelete"]').show();
displayOn();
@ -1557,7 +1559,7 @@
label.text(conf.i18n.displayUnactivate);
textarea.val('').change(); // Clear the textarea.
display.find('.rioccurrences').hide();
display.find('button[name="riedit"]').text(conf.i18n.add_rules);
display.find('button[name="riedit"]').html('<i class="fa fa-plus"></i>' + conf.i18n.add_rules);
display.find('button[name="ridelete"]').hide();
set_picker_date($('#' + conf.prefix + 'end-user'), null);
@ -1571,7 +1573,7 @@
startDate = findStartDate();
// Hide any error message from before
messagearea = form.find('#messagearea');
messagearea = form.find('#' + conf.prefix + 'messagearea');
messagearea.text('');
messagearea.hide();
@ -1579,7 +1581,7 @@
form.find('.riaddoccurrence div.errorarea').text('').hide();
// Repeats Daily
if (form.find('#ridailyinterval').css('display') === 'block') {
if (form.find('#' + conf.prefix + 'ridailyinterval').css('display') === 'block') {
// Check repeat every field
num = findIntField('ridailyinterval', form);
if (!num || num < 1 || num > 1000) {
@ -1589,7 +1591,7 @@
}
// Repeats Weekly
if (form.find('#riweeklyinterval').css('display') === 'block') {
if (form.find('#' + conf.prefix + 'riweeklyinterval').css('display') === 'block') {
// Check repeat every field
num = findIntField('riweeklyinterval', form);
if (!num || num < 1 || num > 1000) {
@ -1599,7 +1601,7 @@
}
// Repeats Monthly
if (form.find('#rimonthlyinterval').css('display') === 'block') {
if (form.find('#' + conf.prefix + 'rimonthlyinterval').css('display') === 'block') {
// Check repeat every field
num = findIntField('rimonthlyinterval', form);
if (!num || num < 1 || num > 1000) {
@ -1615,7 +1617,7 @@
}
// Repeats Yearly
if (form.find('#riyearlyinterval').css('display') === 'block') {
if (form.find('#' + conf.prefix + 'riyearlyinterval').css('display') === 'block') {
// Check repeat every field
num = findIntField('riyearlyinterval', form);
if (!num || num < 1 || num > 1000) {
@ -1781,7 +1783,11 @@
recc_fo_end_time.timepicker('option', 'minTime', $(this).timepicker("getTime"));
});
form.find('input[id=recc-start-user]').datepicker("setDate", $('#' + conf.prefix + 'start-user').datepicker("getDate"));
var recc_start_picker = form.find('input[id=' + conf.prefix + 'recc-start-user]');
var start_date = $('#' + conf.prefix + 'start-user').datepicker("getDate");
recc_start_picker.datepicker("setDate", start_date);
set_date_bounds(recc_start_picker);
recc_start_time.timepicker('setTime', $('#' + conf.prefix + 'start-time').timepicker("getTime"));
recc_start_time.change();
recc_fo_end_time.timepicker('setTime', $('#' + conf.prefix + 'end-time').timepicker("getTime"));
@ -1789,8 +1795,8 @@
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 = form.find("#recc-start-time-group");
var recc_fo_end_time_group = form.find('#recc-fo-end-time-group');
var recc_start_time_group = form.find("#" + conf.prefix + "recc-start-time-group");
var recc_fo_end_time_group = form.find("#" + conf.prefix + "recc-fo-end-time-group");
recc_start_time_group.toggle(!allday_checked);
recc_fo_end_time_group.toggle(!allday_checked);
@ -1798,7 +1804,7 @@
recc_start_time_group.toggle(!this.checked);
recc_fo_end_time_group.toggle(!this.checked);
onAlldayChecked(this, "recc-start");
onAlldayChecked(this, conf.prefix + "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) {
@ -1809,11 +1815,11 @@
recc_fo_end_time.timepicker('setTime', end_moment.toDate());
});
form.find('#occurences-show-container .show-link').click(function(e){
form.find('#' + conf.prefix + 'occurences-show-container .show-link').click(function(e){
showLink(e, this);
});
form.find('#occurences-hide-container .hide-link').click(function(e){
form.find('#' + conf.prefix + 'occurences-hide-container .hide-link').click(function(e){
hideLink(e, this);
});
@ -1822,9 +1828,9 @@
});
// Pop up the little add form when clicking "Add"
var riaddoccurrence_input = form.find('div.riaddoccurrence input#adddate');
var riaddoccurrence_input = form.find('div.riaddoccurrence input#' + conf.prefix + 'adddate');
start_datepicker(riaddoccurrence_input).datepicker("setDate", moment().toDate());
form.find('input#addoccurencebtn').click(occurrenceAdd);
form.find('input#' + conf.prefix + 'addoccurencebtn').click(occurrenceAdd);
// When the reload button is clicked, reload
form.find('a.rirefreshbutton').click(
@ -1876,7 +1882,7 @@
form.find('.risavebutton').click(save);
$('#' + conf.prefix + 'recc-button').click(function() {
$('button[name=riedit]').click();
display.find('button[name="riedit"]').click();
});
/*
@ -1926,12 +1932,12 @@
jQuery.tools.recurrenceinput.localize("de", {
displayUnactivate: "Keine Wiederholungen",
displayActivate: "Alle ",
edit_rules: "Bearbeiten...",
edit_rules: "Serie bearbeiten...",
add_rules: "Hinzufügen...",
delete_rules: "Löschen",
delete_rules: "Serie zurücksetzen",
add: "Hinzufügen",
refresh: "Aktualisieren",
title: "Regelmäßige Veranstaltung",
title: "Serientermin",
preview: "Ausgewählte Termine",
addDate: "Termin hinzufügen",
recurrenceType: "Wiederholt sich",
@ -2050,4 +2056,5 @@ jQuery.tools.recurrenceinput.localize("de", {
reccStartTime: "Beginn",
reccFoEndTime: "Ende",
reccAllDay: "Ganztägig",
removeEventDate: 'Termin entfernen',
});

View File

@ -369,14 +369,15 @@ function scroll_to_element(element, complete) {
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);
start_datepicker(startInput);
start_datepicker(endInput);
recurrenceRuleTextarea.recurrenceinput({prefix: prefix, ajaxURL: "/events/rrule"});
var removeButton = container.find("button.remove-date-defintion-btn");
var startUserInput = container.find("input[id$='-start-user']");
var endUserInput = container.find("input[id$='-end-user']");
@ -409,6 +410,28 @@ function scroll_to_element(element, complete) {
}
});
removeButton.click(function () {
var count = $.find(".date-definition-container").length;
if (count > 1) {
var container = $(this).closest(".date-definition-container");
container.remove();
count--;
if (count == 1) {
$('.date-definition-container button.remove-date-defintion-btn').addClass("d-none");
}
}
return false;
});
container.find(".show-link").click(function (e) {
showLink(e, this);
});
container.find(".hide-link").click(function (e) {
hideLink(e, this);
});
}
$.fn.ovedaDateDefinition = function(options) {
@ -433,11 +456,11 @@ function scroll_to_element(element, complete) {
$(function () {
$('[data-toggle="tooltip"]').tooltip();
$(".datepicker").each(function (index, element) {
$("form .datepicker").each(function (index, element) {
start_datepicker($(element));
});
$(".timepicker").each(function (index, element) {
$("form .timepicker").each(function (index, element) {
start_timepicker($(element));
});

View File

@ -667,7 +667,7 @@
<h2 class="mt-0"><a name="event-dates">{{ _('Event Dates') }}</a></div>
<div class="list-group list-group-flush mb-4" style="max-height: 30vh; overflow: scroll; overflow-y: auto;">
{% for date in dates %}
<a href="{{ url_for('event_date', id=date.id) }}" class="list-group-item">{{ date.start | dateformat('full') }}</a>
<a href="{{ url_for('event_date', id=date.id) }}" class="list-group-item">{{ render_event_date(date.start, date.end, date.allday) }}</a>
{% endfor %}
</div>
</div>
@ -1645,4 +1645,64 @@ $('#allday').on('change', function() {
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='jquery.recurrenceinput.css')}}" />
{% endmacro %}
{% macro render_event_date_defintion_code() %}
$('.date-definition-container').ovedaDateDefinition();
var min_date_definition_index = $(".date-definition-container").length;
if (min_date_definition_index > 1) {
$('.date-definition-container button.remove-date-defintion-btn').removeClass("d-none");
}
$("#add-date-defintion-btn").click(function () {
var template = $(".date-definition-template");
var template_prefix = template.attr('data-prefix');
var last_container = $(".date-definition-container:last");
var last_prefix = last_container.attr('data-prefix');
var new_index = Math.max(min_date_definition_index, $(".date-definition-container").length);
min_date_definition_index++;
var new_prefix = last_prefix.replace(/-(\d+)-$/g, function(match, number) {
return '-' + new_index + '-';
});
var new_container = template.clone();
new_container.removeClass('date-definition-template');
new_container.removeClass('d-none');
new_container.addClass('date-definition-container');
new_container.attr('data-prefix', new_prefix);
new_container.find("*").each(function() {
var subelement = $(this);
$.each(this.attributes, function(i, attrib) {
subelement.attr(attrib.name, attrib.value.replace(template_prefix, new_prefix));
});
});
last_container.after(new_container);
new_container.ovedaDateDefinition();
if ($.find(".date-definition-container").length > 1) {
$('.date-definition-container button.remove-date-defintion-btn').removeClass("d-none");
}
return false;
});
{% endmacro %}
{% macro render_date_definition_container(date_definition, container_class="date-definition-container") %}
<div class="{{ container_class }} card mb-3 bg-light" data-prefix="{{ date_definition.id }}-">
<div class="card-body">
<div id="{{ date_definition.id }}-single-event-container">
{{ date_definition.form.hidden_tag() }}
{{ 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.end, is_collapsible=1) }}
{{ render_field_with_errors(date_definition.form.allday, ri="checkbox") }}
<button type="button" id="{{ date_definition.id }}-recc-button" class="btn btn-outline-secondary"><i class="fas fa-history"></i> {{ _('Recurring event') }}</button>
<button type="button" class="btn btn-outline-secondary remove-date-defintion-btn d-none"><i class="fa fa-calendar-minus"></i> {{ _('Remove event date') }}</button>
</div>
<div id="{{ date_definition.id }}-recc-event-container" class="recc-event-container">
{{ render_field_with_errors(date_definition.form.recurrence_rule, label_hidden=True) }}
</div>
</div>
</div>
{% endmacro %}

View File

@ -1,5 +1,5 @@
{% extends "layout.html" %}
{% from "_macros.html" import render_manage_form_styles, render_manage_form_scripts, render_cropper_header, render_end_container_handling, render_jquery_steps_header, render_cropper_header, render_cropper_code, render_crop_image_form_section, render_radio_buttons, render_field_with_errors, render_field %}
{% from "_macros.html" import render_event_date_defintion_code, render_date_definition_container, render_manage_form_styles, render_manage_form_scripts, render_cropper_header, render_end_container_handling, render_jquery_steps_header, render_cropper_header, render_cropper_code, render_crop_image_form_section, render_radio_buttons, render_field_with_errors, render_field %}
{%- block title -%}
{{ _('Create event') }}
@ -56,7 +56,7 @@ $( function() {
}
});
$('.date-definition-container').ovedaDateDefinition();
{{ render_event_date_defintion_code() }}
function update_place_container(value) {
switch (value) {
@ -240,6 +240,8 @@ $( function() {
<h1>{{ _('Create event') }}</h1>
{{ render_date_definition_container(form.date_definition_template, "date-definition-template d-none") }}
<form id="main-form" action="" method="POST" enctype="multipart/form-data">
{{ form.hidden_tag() }}
@ -255,22 +257,14 @@ $( function() {
<div class="card mb-4">
<div class="card-header">
{{ _('Event date') }}
{{ _('Event dates') }}
</div>
<div class="card-body">
<div class="card-body pb-3">
{% 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>
{{ render_date_definition_container(date_definition) }}
{% endfor %}
<button type="button" class="btn btn-outline-secondary btn-small" id="add-date-defintion-btn"><i class="fa fa-calendar-plus"></i> {{ _('Add event date') }}</button>
</div>
</div>

View File

@ -1,5 +1,5 @@
{% extends "layout.html" %}
{% from "_macros.html" import render_manage_form_styles, render_manage_form_scripts, render_cropper_header, render_end_container_handling, render_jquery_steps_header, render_cropper_code, render_crop_image_form_section, render_radio_buttons, render_field_with_errors, render_field %}
{% from "_macros.html" import render_event_date_defintion_code, render_date_definition_container, render_manage_form_styles, render_manage_form_scripts, render_cropper_header, render_end_container_handling, render_jquery_steps_header, render_cropper_code, render_crop_image_form_section, render_radio_buttons, render_field_with_errors, render_field %}
{%- block title -%}
{{ _('Update event') }}
@ -27,7 +27,7 @@
}
});
$('.date-definition-container').ovedaDateDefinition();
{{ render_event_date_defintion_code() }}
// Organizer
var organizer_select =$('#organizer_id');
@ -131,6 +131,8 @@
<h1>{{ _('Update event') }}</h1>
{{ render_date_definition_container(form.date_definition_template, "date-definition-template d-none") }}
<form id="main-form" action="{{ url_for('event_update', event_id=event.id) }}" method="POST" enctype="multipart/form-data">
{{ form.hidden_tag() }}
@ -146,22 +148,14 @@
<div class="card mb-4">
<div class="card-header">
{{ _('Event date') }}
{{ _('Event dates') }}
</div>
<div class="card-body">
<div class="card-body pb-3">
{% 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>
{{ render_date_definition_container(date_definition) }}
{% endfor %}
<button type="button" class="btn btn-outline-secondary btn-small" id="add-date-defintion-btn"><i class="fa fa-calendar-plus"></i> {{ _('Add event date') }}</button>
</div>
</div>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -149,6 +149,7 @@ def event_create_for_admin_unit_id(id):
prepare_event_form_for_suggestion(form, event_suggestion)
if form.is_submitted():
form.process(request.form)
prepare_date_definition(form)
if form.validate_on_submit():
event = Event()
@ -310,8 +311,15 @@ def prepare_event_form(form):
prepare_organizer(form)
prepare_event_place(form)
prepare_date_definition(form)
def prepare_date_definition(form):
next_full_hour = get_next_full_hour()
form.date_definition_template.start.data = next_full_hour
if not form.date_definitions[0].start.data:
form.date_definitions[0].start.data = get_next_full_hour()
form.date_definitions[0].start.data = next_full_hour
def prepare_event_form_for_suggestion(form, event_suggestion):

View File

@ -157,8 +157,10 @@ def create_put(
return data
@pytest.mark.parametrize("legacy", [True, False])
def test_put(client, seeder, utils, app, mocker, legacy):
@pytest.mark.parametrize(
"variant", ["normal", "legacy", "recurrence", "two_date_definitions"]
)
def test_put(client, seeder, utils, app, mocker, variant):
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)
@ -166,7 +168,7 @@ def test_put(client, seeder, utils, app, mocker, legacy):
utils.mock_now(mocker, 2020, 1, 1)
put = create_put(place_id, organizer_id)
put = create_put(place_id, organizer_id, legacy=(variant == "legacy"))
put["rating"] = 10
put["description"] = "Neue Beschreibung"
put["external_link"] = "http://www.google.de"
@ -186,9 +188,12 @@ def test_put(client, seeder, utils, app, mocker, legacy):
put["price_info"] = "Erwachsene 5€, Kinder 2€."
put["public_status"] = "draft"
if not legacy:
if variant == "recurrence":
put["date_definitions"][0]["recurrence_rule"] = "RRULE:FREQ=DAILY;COUNT=7"
if variant == "two_date_definitions":
put["date_definitions"].append({"start": "2021-02-07T12:00:00.000Z"})
url = utils.get_url("api_v1_event", id=event_id)
response = utils.put_json(url, put)
utils.assert_response_no_content(response)
@ -225,16 +230,23 @@ def test_put(client, seeder, utils, app, mocker, legacy):
assert event.price_info == put["price_info"]
assert event.public_status == PublicStatus.draft
if variant == "two_date_definitions":
assert len(event.date_definitions) == 2
else:
assert len(event.date_definitions) == 1
len_dates = len(event.dates)
if legacy:
assert len_dates == 1
else:
if variant == "recurrence":
assert (
event.date_definitions[0].recurrence_rule
== put["date_definitions"][0]["recurrence_rule"]
)
assert len_dates == 7
elif variant == "two_date_definitions":
assert len_dates == 2
else:
assert len_dates == 1
def test_put_invalidRecurrenceRule(client, seeder, utils, app):

View File

@ -177,16 +177,18 @@ def prepare_events_post_data(seeder, utils, legacy=False):
return url, data, admin_unit_id, place_id, organizer_id
@pytest.mark.parametrize("allday", [True, False])
@pytest.mark.parametrize("legacy", [True, False])
def test_events_post(client, seeder, utils, app, allday, legacy):
@pytest.mark.parametrize("variant", ["allday", "legacy", "two_date_definitions"])
def test_events_post(client, seeder, utils, app, variant):
url, data, admin_unit_id, place_id, organizer_id = prepare_events_post_data(
seeder, utils, legacy
seeder, utils, variant == "legacy"
)
if allday and not legacy:
if variant == "allday":
data["date_definitions"][0]["allday"] = "1"
if variant == "two_date_definitions":
data["date_definitions"].append({"start": "2021-02-07T12:00:00.000Z"})
response = utils.post_json(url, data)
utils.assert_response_created(response)
assert "id" in response.json
@ -205,7 +207,12 @@ def test_events_post(client, seeder, utils, app, allday, legacy):
assert event.photo is not None
assert event.photo.encoding_format == "image/png"
assert event.public_status == PublicStatus.published
assert event.date_definitions[0].allday == (allday and not legacy)
assert event.date_definitions[0].allday == (variant == "allday")
if variant == "two_date_definitions":
assert len(event.date_definitions) == 2
else:
assert len(event.date_definitions) == 1
def test_events_post_co_organizers(client, seeder, utils, app):

View File

@ -116,7 +116,9 @@ class Form:
for key, value in values.items():
if key in filled:
del filled[key]
if type(value) is list:
if value is None:
continue
elif type(value) is list:
filled.setlist(key, value)
else:
filled.add(key, value)

View File

@ -326,6 +326,31 @@ class Seeder(object):
return date_definition
def add_event_date_definition(
self, event_id, start=None, end=None, allday=False, recurrence_rule=""
):
from project.models import Event
from project.services.event import update_event
with self._app.app_context():
event = Event.query.get(event_id)
date_definition = self.create_event_date_definition(
start, end, allday, recurrence_rule
)
date_definition.event = event
self._db.session.add(date_definition)
date_definitions = event.date_definitions
date_definitions.append(date_definition)
event.date_definitions = date_definitions
update_event(event)
self._db.session.commit()
date_definition_id = date_definition.id
return date_definition_id
def create_event_unverified(self):
user_id = self.create_user("unverified@test.de")
admin_unit_id = self.create_admin_unit(user_id, "Unverified Crew")

View File

@ -53,8 +53,8 @@ def test_read_co_organizers(seeder, utils):
utils.assert_response_contains(response, "Organizer B")
@pytest.mark.parametrize("db_error", [True, False])
def test_create(client, app, utils, seeder, mocker, db_error):
@pytest.mark.parametrize("variant", ["normal", "db_error", "two_date_definitions"])
def test_create(client, app, utils, seeder, mocker, variant):
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)
@ -62,23 +62,28 @@ def test_create(client, app, utils, seeder, mocker, db_error):
url = utils.get_url("event_create_for_admin_unit_id", id=admin_unit_id)
response = utils.get_ok(url)
if db_error:
if variant == "db_error":
utils.mock_db_commit(mocker, UniqueViolation("MockException", "MockException"))
response = utils.post_form(
url,
response,
{
data = {
"name": "Name",
"description": "Beschreibung",
"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(),
},
}
if variant == "two_date_definitions":
data["date_definitions-1-start"] = ["2030-12-31", "14:00"]
response = utils.post_form(
url,
response,
data,
)
if db_error:
if variant == "db_error":
utils.assert_response_db_error(response)
return
@ -94,6 +99,11 @@ def test_create(client, app, utils, seeder, mocker, db_error):
)
assert event is not None
if variant == "two_date_definitions":
assert len(event.date_definitions) == 2
else:
assert len(event.date_definitions) == 1
def test_create_allday(client, app, utils, seeder):
user_id, admin_unit_id = seeder.setup_base()
@ -494,26 +504,43 @@ def test_actions_withReferenceLink(seeder, utils):
assert b"Veranstaltung empfehlen" in response.data
@pytest.mark.parametrize("db_error", [True, False])
def test_update(client, seeder, utils, app, mocker, db_error):
@pytest.mark.parametrize(
"variant", ["normal", "db_error", "add_date_definition", "remove_date_definition"]
)
def test_update(client, seeder, utils, app, mocker, variant):
user_id, admin_unit_id = seeder.setup_base()
event_id = seeder.create_event(admin_unit_id)
if variant == "remove_date_definition":
seeder.add_event_date_definition(event_id)
url = utils.get_url("event_update", event_id=event_id)
response = utils.get_ok(url)
if db_error:
if variant == "db_error":
utils.mock_db_commit(mocker)
data = {
"name": "Neuer Name",
}
if variant == "add_date_definition":
data["date_definitions-1-start"] = ["2030-12-31", "14:00"]
if variant == "remove_date_definition":
data["date_definitions-1-csrf_token"] = None
data["date_definitions-1-start"] = None
data["date_definitions-1-end"] = None
data["date_definitions-1-allday"] = None
data["date_definitions-1-recurrence_rule"] = None
response = utils.post_form(
url,
response,
{
"name": "Neuer Name",
},
data,
)
if db_error:
if variant == "db_error":
utils.assert_response_db_error(response)
return
@ -531,6 +558,11 @@ def test_update(client, seeder, utils, app, mocker, db_error):
)
assert event is not None
if variant == "add_date_definition":
assert len(event.date_definitions) == 2
else:
assert len(event.date_definitions) == 1
def test_update_co_organizers(client, seeder, utils, app):
user_id, admin_unit_id = seeder.setup_base()