Bug 1492436 - Delete a recurring item if the last occurrence is deleted; r=philipp

This commit is contained in:
MakeMyDay 2018-09-19 14:17:49 +02:00
Родитель 8ac8015373
Коммит 123cb6336c
5 изменённых файлов: 472 добавлений и 14 удалений

Просмотреть файл

@ -10,6 +10,7 @@
*/
var { cal } = ChromeUtils.import("resource://calendar/modules/calUtils.jsm", null);
const { countOccurrences } = ChromeUtils.import("resource://calendar/modules/calRecurrenceUtils.jsm", null);
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://gre/modules/Preferences.jsm");
@ -115,23 +116,45 @@ var calendarViewController = {
// are readonly.
let occurrences = aOccurrences.filter(item => cal.acl.isCalendarWritable(item.calendar));
// we check how many occurrences the parent item has
let parents = new Map();
for (let occ of occurrences) {
if (!parents.has(occ.id)) {
parents.set(occ.id, countOccurrences(occ));
}
}
let promptUser = !aDoNotConfirm;
let previousResponse = 0;
for (let itemToDelete of occurrences) {
if (aUseParentItems) {
if (parents.get(itemToDelete.id) == -1) {
// we have scheduled the master item for deletion in a previous
// loop already
continue;
}
if (aUseParentItems ||
parents.get(itemToDelete.id) == 1 ||
previousResponse == 3) {
// Usually happens when ctrl-click is used. In that case we
// don't need to ask the user if he wants to delete an
// occurrence or not.
// if an occurrence is the only one of a series or the user
// decided so before, we delete the series, too.
itemToDelete = itemToDelete.parentItem;
} else if (!aDoNotConfirm && occurrences.length == 1) {
// Only give the user the selection if only one occurrence is
// selected. Otherwise he will get a dialog for each occurrence
// he deletes.
parents.set(itemToDelete.id, -1);
} else if (promptUser) {
let [targetItem, , response] = promptOccurrenceModification(itemToDelete, false, "delete");
if (!response) {
// The user canceled the dialog, bail out
break;
}
itemToDelete = targetItem;
// if we have multiple items and the user decided already for one
// item whether to delete the occurrence or the entire series,
// we apply that decission also to subsequent items
previoiusResponse = response;
promptUser = false;
}
// Now some dirty work: Make sure more than one occurrence can be

Просмотреть файл

@ -2,12 +2,17 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* exported recurrenceRule2String, splitRecurrenceRules, checkRecurrenceRule */
/* exported recurrenceRule2String, splitRecurrenceRules, checkRecurrenceRule
* countOccurrences
*/
ChromeUtils.import("resource://gre/modules/PluralForm.jsm");
const { cal } = ChromeUtils.import("resource://calendar/modules/calUtils.jsm", null);
this.EXPORTED_SYMBOLS = ["recurrenceRule2String", "splitRecurrenceRules", "checkRecurrenceRule"];
this.EXPORTED_SYMBOLS = [
"recurrenceRule2String", "splitRecurrenceRules", "checkRecurrenceRule",
"countOccurrences"
];
/**
* This function takes the recurrence info passed as argument and creates a
@ -406,3 +411,72 @@ function checkRecurrenceRule(aRule, aArray) {
}
return false;
}
/**
* Counts the occurrences of the parent item if any of a provided item
*
* @param {(calIEvent|calIToDo)} aItem item to count for
* @returns {(number|null)} number of occurrences or null if the
* passed item's parent item isn't a
* recurring item or its recurrence is
* infinite
*/
function countOccurrences(aItem) {
let occCounter = null;
let recInfo = aItem.parentItem.recurrenceInfo;
if (recInfo &&
recInfo.isFinite) {
occCounter = 0;
let excCounter = 0;
let byCount = false;
let ritems = recInfo.getRecurrenceItems({});
for (let ritem of ritems) {
if (ritem instanceof Ci.calIRecurrenceRule) {
if (ritem.isByCount) {
occCounter = occCounter + ritem.count;
byCount = true;
} else {
// the rule is limited by as an until date
let from = aItem.parentItem.startDate.clone();
let until = aItem.parentItem.endDate.clone();
if (until.compare(ritem.untilDate) == -1) {
until = ritem.untilDate.clone();
}
let exceptionIds = recInfo.getExceptionIds({});
for (let exceptionId of exceptionIds) {
let recur = recInfo.getExceptionFor(exceptionId);
if (from.compare(recur.startDate) == 1) {
from = recur.startDate.clone();
}
if (until.compare(recur.endDate) == -1) {
until = recur.endDate.clone();
}
}
// we add an extra day at beginning and end, so we don't
// neeed to take care of any timezone conversion
from.addDuration(cal.createDuration("-P1D"));
until.addDuration(cal.createDuration("P1D"));
let occurrences = recInfo.getOccurrences(from, until, 0, {});
occCounter = occCounter + occurrences.length;
}
} else if (ritem instanceof Ci.calIRecurrenceDate) {
if (ritem.isNegative) {
// this is an exdate
excCounter++;
} else {
// this is an (additional) rdate
occCounter++;
}
}
}
if (byCount) {
// for a rrule by count, we still need to substract exceptions if any
occCounter = occCounter - excCounter;
}
}
return occCounter;
}

Просмотреть файл

@ -19,7 +19,8 @@ ChromeUtils.import("resource://gre/modules/Services.jsm");
const {
recurrenceRule2String,
splitRecurrenceRules,
checkRecurrenceRule
checkRecurrenceRule,
countOccurrences
} = ChromeUtils.import("resource://calendar/modules/calRecurrenceUtils.jsm", null);
ChromeUtils.import("resource://gre/modules/PluralForm.jsm");
ChromeUtils.import("resource://gre/modules/Preferences.jsm");
@ -3225,11 +3226,19 @@ function onCommandDeleteItem() {
eventDialogCalendarObserver.cancel();
if (window.calendarItem.parentItem.recurrenceInfo && window.calendarItem.recurrenceId) {
// if this is a single occurrence of a recurring item
let newItem = window.calendarItem.parentItem.clone();
newItem.recurrenceInfo.removeOccurrenceAt(window.calendarItem.recurrenceId);
gMainWindow.doTransaction("modify", newItem, newItem.calendar,
window.calendarItem.parentItem, deleteListener);
if (countOccurrences(window.calendarItem) == 1) {
// this is the last occurrence, hence we delete the parent item
// to not leave a parent item without children in the calendar
gMainWindow.doTransaction("delete", window.calendarItem.parentItem,
window.calendarItem.calendar, null,
deleteListener);
} else {
// we just need to remove the occurrence
let newItem = window.calendarItem.parentItem.clone();
newItem.recurrenceInfo.removeOccurrenceAt(window.calendarItem.recurrenceId);
gMainWindow.doTransaction("modify", newItem, newItem.calendar,
window.calendarItem.parentItem, deleteListener);
}
} else {
gMainWindow.doTransaction("delete", window.calendarItem, window.calendarItem.calendar,
null, deleteListener);

Просмотреть файл

@ -0,0 +1,351 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const { countOccurrences } = ChromeUtils.import("resource://calendar/modules/calRecurrenceUtils.jsm", null);
function run_test() {
do_calendar_startup(run_next_test);
}
// tests for calRecurrenceUtils.jsm
/* Incomplete - still missing test coverage for:
* recurrenceRule2String
* splitRecurrenceRules
* checkRecurrenceRule
*/
function getIcs(aProperties) {
let calendar = [
"BEGIN:VCALENDAR",
"PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN",
"VERSION:2.0",
"BEGIN:VTIMEZONE",
"TZID:Europe/Berlin",
"BEGIN:DAYLIGHT",
"TZOFFSETFROM:+0100",
"TZOFFSETTO:+0200",
"TZNAME:CEST",
"DTSTART:19700329T020000",
"RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3",
"END:DAYLIGHT",
"BEGIN:STANDARD",
"TZOFFSETFROM:+0200",
"TZOFFSETTO:+0100",
"TZNAME:CET",
"DTSTART:19701025T030000",
"RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10",
"END:STANDARD",
"END:VTIMEZONE",
];
calendar = calendar.concat(aProperties);
calendar = calendar.concat(["END:VCALENDAR"]);
return calendar.join("\r\n");
}
add_task(async function countOccurrences_test() {
let data = [{
input: [
"BEGIN:VEVENT",
"CREATED:20180912T090539Z",
"LAST-MODIFIED:20180912T090539Z",
"DTSTAMP:20180912T090539Z",
"UID:5b47fa17-f2fe-4d96-8cc2-19ce5be98000",
"SUMMARY:Occurring 3 times until a date",
"RRULE:FREQ=DAILY;UNTIL=20180922T100000Z",
"DTSTART;TZID=Europe/Berlin:20180920T120000",
"DTEND;TZID=Europe/Berlin:20180920T130000",
"END:VEVENT",
],
expected: 3
}, {
input: [
"BEGIN:VEVENT",
"CREATED:20180912T090539Z",
"LAST-MODIFIED:20180912T090539Z",
"DTSTAMP:20180912T090539Z",
"UID:5b47fa17-f2fe-4d96-8cc2-19ce5be98001",
"SUMMARY:Occurring 3 times until a date with one exception in the middle",
"RRULE:FREQ=DAILY;UNTIL=20180922T100000Z",
"EXDATE;TZID=Europe/Berlin:20180921T120000",
"DTSTART;TZID=Europe/Berlin:20180920T120000",
"DTEND;TZID=Europe/Berlin:20180920T130000",
"END:VEVENT",
],
expected: 2
}, {
input: [
"BEGIN:VEVENT",
"CREATED:20180912T090539Z",
"LAST-MODIFIED:20180912T090539Z",
"DTSTAMP:20180912T090539Z",
"UID:5b47fa17-f2fe-4d96-8cc2-19ce5be98002",
"SUMMARY:Occurring 3 times until a date with one exception at the end",
"RRULE:FREQ=DAILY;UNTIL=20180922T100000Z",
"EXDATE;TZID=Europe/Berlin:20180922T120000",
"DTSTART;TZID=Europe/Berlin:20180920T120000",
"DTEND;TZID=Europe/Berlin:20180920T130000",
"END:VEVENT",
],
expected: 2
}, {
input: [
"BEGIN:VEVENT",
"CREATED:20180912T090539Z",
"LAST-MODIFIED:20180912T090539Z",
"DTSTAMP:20180912T090539Z",
"UID:5b47fa17-f2fe-4d96-8cc2-19ce5be98003",
"SUMMARY:Occurring 3 times until a date with one exception at the beginning",
"RRULE:FREQ=DAILY;UNTIL=20180922T100000Z",
"EXDATE;TZID=Europe/Berlin:20180920T120000",
"DTSTART;TZID=Europe/Berlin:20180920T120000",
"DTEND;TZID=Europe/Berlin:20180920T130000",
"END:VEVENT",
],
expected: 2
}, {
input: [
"BEGIN:VEVENT",
"CREATED:20180912T090539Z",
"LAST-MODIFIED:20180912T090539Z",
"DTSTAMP:20180912T090539Z",
"UID:5b47fa17-f2fe-4d96-8cc2-19ce5be98004",
"SUMMARY:Occurring 3 times until a date with the middle occurrence moved after the end",
"RRULE:FREQ=DAILY;UNTIL=20180922T100000Z",
"DTSTART;TZID=Europe/Berlin:20180920T120000",
"DTEND;TZID=Europe/Berlin:20180920T130000",
"END:VEVENT",
"BEGIN:VEVENT",
"CREATED:20180912T090539Z",
"LAST-MODIFIED:20180912T090539Z",
"DTSTAMP:20180912T090539Z",
"UID:5b47fa17-f2fe-4d96-8cc2-19ce5be98004",
"SUMMARY:The moved occurrence",
"RECURRENCE-ID:20180921T100000Z",
"DTSTART;TZID=Europe/Berlin:20180924T120000",
"DTEND;TZID=Europe/Berlin:20180924T130000",
"END:VEVENT",
],
expected: 3
}, {
input: [
"BEGIN:VEVENT",
"CREATED:20180912T090539Z",
"LAST-MODIFIED:20180912T090539Z",
"DTSTAMP:20180912T090539Z",
"UID:5b47fa17-f2fe-4d96-8cc2-19ce5be98005",
"SUMMARY:Occurring 3 times until a date with the middle occurrence moved before the beginning",
"RRULE:FREQ=DAILY;UNTIL=20180922T100000Z",
"DTSTART;TZID=Europe/Berlin:20180920T120000",
"DTEND;TZID=Europe/Berlin:20180920T130000",
"END:VEVENT",
"BEGIN:VEVENT",
"CREATED:20180912T090539Z",
"LAST-MODIFIED:20180912T090539Z",
"DTSTAMP:20180912T090539Z",
"UID:5b47fa17-f2fe-4d96-8cc2-19ce5be98005",
"SUMMARY:The moved occurrence",
"RECURRENCE-ID:20180921T100000Z",
"DTSTART;TZID=Europe/Berlin:20180918T120000",
"DTEND;TZID=Europe/Berlin:20180918T130000",
"END:VEVENT",
],
expected: 3
}, {
input: [
"BEGIN:VEVENT",
"CREATED:20180912T090539Z",
"LAST-MODIFIED:20180912T090539Z",
"DTSTAMP:20180912T090539Z",
"UID:5b47fa17-f2fe-4d96-8cc2-19ce5be98006",
"SUMMARY:Occurring 1 times until a date",
"RRULE:FREQ=DAILY;UNTIL=20180920T100000Z",
"DTSTART;TZID=Europe/Berlin:20180920T120000",
"DTEND;TZID=Europe/Berlin:20180920T130000",
"END:VEVENT",
],
expected: 1
}, {
input: [
"BEGIN:VEVENT",
"CREATED:20180912T090539Z",
"LAST-MODIFIED:20180912T090539Z",
"DTSTAMP:20180912T090539Z",
"UID:5b47fa17-f2fe-4d96-8cc2-19ce5be98007",
"SUMMARY:Occurring 1 times until a date with occernce removed",
"RRULE:FREQ=DAILY;UNTIL=20180920T100000Z",
"EXDATE;TZID=Europe/Berlin:20180920T120000",
"DTSTART;TZID=Europe/Berlin:20180920T120000",
"DTEND;TZID=Europe/Berlin:20180920T130000",
"END:VEVENT",
],
expected: 0
}, {
input: [
"BEGIN:VEVENT",
"CREATED:20180912T090539Z",
"LAST-MODIFIED:20180912T090539Z",
"DTSTAMP:20180912T090539Z",
"UID:5b47fa17-f2fe-4d96-8cc2-19ce5be98008",
"SUMMARY:Occurring for 3 times",
"RRULE:FREQ=DAILY;COUNT=3",
"DTSTART;TZID=Europe/Berlin:20180920T120000",
"DTEND;TZID=Europe/Berlin:20180920T130000",
"END:VEVENT",
],
expected: 3
}, {
input: [
"BEGIN:VEVENT",
"CREATED:20180912T090539Z",
"LAST-MODIFIED:20180912T090539Z",
"DTSTAMP:20180912T090539Z",
"UID:5b47fa17-f2fe-4d96-8cc2-19ce5be98009",
"SUMMARY:Occurring for 3 times with an exception in the middle",
"EXDATE;TZID=Europe/Berlin:20180921T120000",
"RRULE:FREQ=DAILY;COUNT=3",
"DTSTART;TZID=Europe/Berlin:20180920T120000",
"DTEND;TZID=Europe/Berlin:20180920T130000",
"END:VEVENT",
],
expected: 2
}, {
input: [
"BEGIN:VEVENT",
"CREATED:20180912T090539Z",
"LAST-MODIFIED:20180912T090539Z",
"DTSTAMP:20180912T090539Z",
"UID:5b47fa17-f2fe-4d96-8cc2-19ce5be98010",
"SUMMARY:Occurring for 3 times with an exception at the end",
"EXDATE;TZID=Europe/Berlin:20180922T120000",
"RRULE:FREQ=DAILY;COUNT=3",
"DTSTART;TZID=Europe/Berlin:20180920T120000",
"DTEND;TZID=Europe/Berlin:20180920T130000",
"END:VEVENT",
],
expected: 2
}, {
input: [
"BEGIN:VEVENT",
"CREATED:20180912T090539Z",
"LAST-MODIFIED:20180912T090539Z",
"DTSTAMP:20180912T090539Z",
"UID:5b47fa17-f2fe-4d96-8cc2-19ce5be98011",
"SUMMARY:Occurring for 3 times with an exception at the beginning",
"EXDATE;TZID=Europe/Berlin:20180920T120000",
"RRULE:FREQ=DAILY;COUNT=3",
"DTSTART;TZID=Europe/Berlin:20180920T120000",
"DTEND;TZID=Europe/Berlin:20180920T130000",
"END:VEVENT",
],
expected: 2
}, {
input: [
"BEGIN:VEVENT",
"CREATED:20180912T090539Z",
"LAST-MODIFIED:20180912T090539Z",
"DTSTAMP:20180912T090539Z",
"UID:5b47fa17-f2fe-4d96-8cc2-19ce5be98012",
"SUMMARY:Occurring for 1 time",
"RRULE:FREQ=DAILY;COUNT=1",
"DTSTART;TZID=Europe/Berlin:20180920T120000",
"DTEND;TZID=Europe/Berlin:20180920T130000",
"END:VEVENT",
],
expected: 1
}, {
input: [
"BEGIN:VEVENT",
"CREATED:20180912T090539Z",
"LAST-MODIFIED:20180912T090539Z",
"DTSTAMP:20180912T090539Z",
"UID:5b47fa17-f2fe-4d96-8cc2-19ce5be98013",
"SUMMARY:Occurring for 0 times",
"RRULE:FREQ=DAILY;COUNT=1",
"EXDATE;TZID=Europe/Berlin:20180920T120000",
"DTSTART;TZID=Europe/Berlin:20180920T120000",
"DTEND;TZID=Europe/Berlin:20180920T130000",
"END:VEVENT",
],
expected: 0
}, {
input: [
"BEGIN:VEVENT",
"CREATED:20180912T090539Z",
"LAST-MODIFIED:20180912T090539Z",
"DTSTAMP:20180912T090539Z",
"UID:5b47fa17-f2fe-4d96-8cc2-19ce5be98014",
"SUMMARY:Occurring infinitely",
"RRULE:FREQ=DAILY",
"DTSTART;TZID=Europe/Berlin:20180920T120000",
"DTEND;TZID=Europe/Berlin:20180920T130000",
"END:VEVENT",
],
expected: null
}, {
input: [
"BEGIN:VEVENT",
"CREATED:20180912T090539Z",
"LAST-MODIFIED:20180912T090539Z",
"DTSTAMP:20180912T090539Z",
"UID:5b47fa17-f2fe-4d96-8cc2-19ce5be98015",
"SUMMARY:Non-occurring item",
"DTSTART;TZID=Europe/Berlin:20180920T120000",
"DTEND;TZID=Europe/Berlin:20180920T130000",
"END:VEVENT",
],
expected: null
}, {
input: [
"BEGIN:VEVENT",
"CREATED:20180912T090539Z",
"LAST-MODIFIED:20180912T090539Z",
"DTSTAMP:20180912T090539Z",
"UID:5b47fa17-f2fe-4d96-8cc2-19ce5be98016",
"SUMMARY:Occurring for 3 time and 1 rdate",
"RRULE:FREQ=DAILY;COUNT=3",
"RDATE;TZID=Europe/Berlin:20180923T100000",
"DTSTART;TZID=Europe/Berlin:20180920T120000",
"DTEND;TZID=Europe/Berlin:20180920T130000",
"END:VEVENT",
],
expected: 4
}, {
input: [
"BEGIN:VEVENT",
"CREATED:20180912T090539Z",
"LAST-MODIFIED:20180912T090539Z",
"DTSTAMP:20180912T090539Z",
"UID:5b47fa17-f2fe-4d96-8cc2-19ce5be98017",
"SUMMARY:Occurring for 3 rdates",
"RDATE;TZID=Europe/Berlin:20180920T120000",
"RDATE;TZID=Europe/Berlin:20180921T100000",
"RDATE;TZID=Europe/Berlin:20180922T140000",
"DTSTART;TZID=Europe/Berlin:20180920T120000",
"DTEND;TZID=Europe/Berlin:20180920T130000",
"END:VEVENT",
],
expected: 3
}];
let i = 0;
for (let test of data) {
i++;
let ics = getIcs(test.input);
let parser = Cc["@mozilla.org/calendar/ics-parser;1"]
.createInstance(Ci.calIIcsParser);
parser.parseString(ics);
let items = parser.getItems({});
ok(items.length > 0, "parsing input suceeded (test #" + i + ")");
for (let item of items) {
equal(
countOccurrences(item),
test.expected,
"expected number of occurrences (test #" + i + " - '" + item.title + "')"
);
}
}
});

Просмотреть файл

@ -42,6 +42,7 @@ skip-if = true # See bug 1481180. requesttimeoutfactor = 2
[test_ltninvitationutils.js]
[test_providers.js]
[test_recur.js]
[test_recurrence_utils.js]
[test_relation.js]
[test_rfc3339_parser.js]
[test_search_service.js]