Merge lp:~charlesk/indicator-datetime/lp-1302004-fix-event-list-culling-and-sorting into lp:indicator-datetime/15.10

Proposed by Charles Kerr
Status: Merged
Approved by: Renato Araujo Oliveira Filho
Approved revision: 436
Merged at revision: 445
Proposed branch: lp:~charlesk/indicator-datetime/lp-1302004-fix-event-list-culling-and-sorting
Merge into: lp:indicator-datetime/15.10
Diff against target: 512 lines (+383/-17)
7 files modified
include/datetime/date-time.h (+2/-0)
include/datetime/menu.h (+6/-0)
src/date-time.cpp (+10/-0)
src/menu.cpp (+91/-17)
tests/CMakeLists.txt (+1/-0)
tests/print-to.h (+10/-0)
tests/test-menu-appointments.cpp (+263/-0)
To merge this branch: bzr merge lp:~charlesk/indicator-datetime/lp-1302004-fix-event-list-culling-and-sorting
Reviewer Review Type Date Requested Status
Renato Araujo Oliveira Filho (community) Approve
Review via email: mp+289517@code.launchpad.net

Commit message

Bring the code up-to-date with the spec wrt which calendar events are shown in the menu, and in what order.

Description of the change

Bring the code up-to-date with the spec wrt which calendar events are shown in the menu, and in what order. Add unit tests to cover event selection and display order.

https://wiki.ubuntu.com/TimeAndDate#Calendar_events

To post a comment you must log in.
Revision history for this message
Renato Araujo Oliveira Filho (renatofilho) wrote :

code looks good.

review: Approve
437. By Charles Kerr

sync with trunk

438. By Charles Kerr

fix r437 merge error

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'include/datetime/date-time.h'
2--- include/datetime/date-time.h 2015-04-06 18:16:24 +0000
3+++ include/datetime/date-time.h 2016-04-20 16:31:00 +0000
4@@ -68,7 +68,9 @@
5 int64_t to_unix() const;
6
7 bool operator<(const DateTime& that) const;
8+ bool operator>(const DateTime& that) const;
9 bool operator<=(const DateTime& that) const;
10+ bool operator>=(const DateTime& that) const;
11 bool operator!=(const DateTime& that) const;
12 bool operator==(const DateTime& that) const;
13 int64_t operator- (const DateTime& that) const;
14
15=== modified file 'include/datetime/menu.h'
16--- include/datetime/menu.h 2014-01-30 19:44:12 +0000
17+++ include/datetime/menu.h 2016-04-20 16:31:00 +0000
18@@ -21,6 +21,7 @@
19 #define INDICATOR_DATETIME_MENU_H
20
21 #include <datetime/actions.h>
22+#include <datetime/appointment.h>
23 #include <datetime/state.h>
24
25 #include <memory> // std::shared_ptr
26@@ -47,6 +48,11 @@
27 Profile profile() const;
28 GMenuModel* menu_model();
29
30+ static std::vector<Appointment> get_display_appointments(
31+ const std::vector<Appointment>&,
32+ const DateTime& start,
33+ unsigned int max_items=5);
34+
35 protected:
36 Menu (Profile profile_in, const std::string& name_in);
37 virtual ~Menu() =default;
38
39=== modified file 'src/date-time.cpp'
40--- src/date-time.cpp 2015-04-06 18:16:24 +0000
41+++ src/date-time.cpp 2016-04-20 16:31:00 +0000
42@@ -233,11 +233,21 @@
43 return g_date_time_compare(get(), that.get()) < 0;
44 }
45
46+bool DateTime::operator>(const DateTime& that) const
47+{
48+ return g_date_time_compare(get(), that.get()) > 0;
49+}
50+
51 bool DateTime::operator<=(const DateTime& that) const
52 {
53 return g_date_time_compare(get(), that.get()) <= 0;
54 }
55
56+bool DateTime::operator>=(const DateTime& that) const
57+{
58+ return g_date_time_compare(get(), that.get()) >= 0;
59+}
60+
61 bool DateTime::operator!=(const DateTime& that) const
62 {
63 // return true if this isn't set, or if it's not equal
64
65=== modified file 'src/menu.cpp'
66--- src/menu.cpp 2016-03-23 20:28:22 +0000
67+++ src/menu.cpp 2016-04-20 16:31:00 +0000
68@@ -25,6 +25,8 @@
69 #include <glib/gi18n.h>
70 #include <gio/gio.h>
71
72+#include <algorithm>
73+#include <iterator>
74 #include <vector>
75
76 namespace unity {
77@@ -56,12 +58,90 @@
78 return G_MENU_MODEL(m_menu);
79 }
80
81+/**
82+ * To avoid a giant menu on the PC, and to avoid pushing lower menu items
83+ * off-screen on the phone, the menu should the
84+ * next five calendar events, if any.
85+ *
86+ * The list might include multiple occurrences of the same event (bug 1515821).
87+ */
88+std::vector<Appointment>
89+Menu::get_display_appointments(const std::vector<Appointment>& appointments_in,
90+ const DateTime& now,
91+ unsigned int max_items)
92+{
93+ std::vector<Appointment> appointments;
94+ std::copy_if(appointments_in.begin(),
95+ appointments_in.end(),
96+ std::back_inserter(appointments),
97+ [now](const Appointment& a){return a.end >= now;});
98+
99+ if (appointments.size() > max_items)
100+ {
101+ const auto next_minute = now.add_full(0,0,0,0,1,-now.seconds());
102+ const auto start_of_day = now.start_of_day();
103+ const auto end_of_day = now.end_of_day();
104+
105+ /*
106+ * If there are more than five, the events shown should be, in order of priority:
107+ * 1. any events that start or end (bug 1329048) after the current minute today;
108+ * 2. any full-day events that span all of today (bug 1302004);
109+ * 3. any events that start or end tomorrow;
110+ * 4. any events that start or end the day after tomorrow; and so on.
111+ */
112+ auto compare = [next_minute, start_of_day, end_of_day](
113+ const Appointment& a,
114+ const Appointment& b)
115+ {
116+ const bool a_later_today = (a.begin >= next_minute) || (a.end <= end_of_day);
117+ const bool b_later_today = (b.begin >= next_minute) || (b.end <= end_of_day);
118+ if (a_later_today != b_later_today)
119+ return a_later_today;
120+
121+ const bool a_full_day_today = (a.begin <= start_of_day) && (end_of_day <= a.end);
122+ const bool b_full_day_today = (b.begin <= start_of_day) && (end_of_day <= b.end);
123+ if (a_full_day_today != b_full_day_today)
124+ return a_full_day_today;
125+
126+ const bool a_after_today = (a.begin > end_of_day) || (a.end > end_of_day);
127+ const bool b_after_today = (a.begin > end_of_day) || (a.end > end_of_day);
128+ if (a_after_today != b_after_today)
129+ return a_after_today;
130+ if (a.begin != b.begin)
131+ return a.begin < b.begin;
132+ if (b.end != b.end)
133+ return a.end < b.end;
134+
135+ return false;
136+ };
137+ std::sort(appointments.begin(), appointments.end(), compare);
138+ appointments.resize(max_items);
139+ }
140+
141+ /*
142+ * However, the display order should be the reverse: full-day events
143+ * first (since they start first), part-day events afterward in
144+ * chronological order. If multiple events have exactly the same start+end
145+ * time, they should be sorted alphabetically.
146+ */
147+ auto compare = [](const Appointment& a, const Appointment& b)
148+ {
149+ if (a.begin != b.begin)
150+ return a.begin < b.begin;
151+
152+ if (a.end != b.end)
153+ return a.end < b.end;
154+
155+ return a.summary < b.summary;
156+ };
157+ std::sort(appointments.begin(), appointments.end(), compare);
158+ return appointments;
159+}
160
161 /****
162 *****
163 ****/
164
165-
166 #define ALARM_ICON_NAME "alarm-clock"
167 #define CALENDAR_ICON_NAME "calendar"
168
169@@ -141,25 +221,19 @@
170
171 void update_upcoming()
172 {
173- // The usual case is on desktop (and /only/ case on phone)
174- // is that we're looking at the current date and want to see
175- // "the next five calendar events, if any."
176- //
177- // However on the Desktop when the user clicks onto a different
178- // calendar date, show the next five calendar events starting
179- // from the beginning of that clicked day.
180- DateTime begin;
181+ // The usual case is to show events germane to the current time.
182+ // However when the user clicks onto a different calendar date,
183+ // we pick events starting from the beginning of that clicked day.
184 const auto now = m_state->clock->localtime();
185 const auto calendar_day = m_state->calendar_month->month().get();
186- if ((profile() == Desktop) && !DateTime::is_same_day(now, calendar_day))
187- begin = calendar_day.start_of_day();
188- else
189- begin = now.start_of_minute();
190+ const auto begin = DateTime::is_same_day(now, calendar_day)
191+ ? now.start_of_minute()
192+ : calendar_day.start_of_day();
193
194- std::vector<Appointment> upcoming;
195- for(const auto& a : m_state->calendar_upcoming->appointments().get())
196- if (begin <= a.begin)
197- upcoming.push_back(a);
198+ auto upcoming = get_display_appointments(
199+ m_state->calendar_upcoming->appointments().get(),
200+ begin
201+ );
202
203 if (m_upcoming != upcoming)
204 {
205
206=== modified file 'tests/CMakeLists.txt'
207--- tests/CMakeLists.txt 2016-03-22 18:51:50 +0000
208+++ tests/CMakeLists.txt 2016-04-20 16:31:00 +0000
209@@ -57,6 +57,7 @@
210 add_test_by_name(test-formatter)
211 add_test_by_name(test-live-actions)
212 add_test_by_name(test-locations)
213+add_test_by_name(test-menu-appointments)
214 add_test_by_name(test-menus)
215 add_test_by_name(test-planner)
216 add_test_by_name(test-settings)
217
218=== modified file 'tests/print-to.h'
219--- tests/print-to.h 2015-06-18 04:52:52 +0000
220+++ tests/print-to.h 2016-04-20 16:31:00 +0000
221@@ -21,6 +21,7 @@
222 #define INDICATOR_DATETIME_TESTS_PRINT_TO
223
224 #include <algorithm>
225+#include <vector>
226
227 #include <datetime/appointment.h>
228
229@@ -71,6 +72,15 @@
230 *os << '}';
231 }
232
233+void
234+PrintTo(const std::vector<Appointment>& appointments, std::ostream* os)
235+{
236+ *os << '{';
237+ for (const auto& appointment : appointments)
238+ PrintTo(appointment, os);
239+ *os << '}';
240+}
241+
242 } // namespace datetime
243 } // namespace indicator
244 } // namespace unity
245
246=== added file 'tests/test-menu-appointments.cpp'
247--- tests/test-menu-appointments.cpp 1970-01-01 00:00:00 +0000
248+++ tests/test-menu-appointments.cpp 2016-04-20 16:31:00 +0000
249@@ -0,0 +1,263 @@
250+/*
251+ * Copyright 2016 Canonical Ltd.
252+ *
253+ * This program is free software: you can redistribute it and/or modify it
254+ * under the terms of the GNU General Public License version 3, as published
255+ * by the Free Software Foundation.
256+ *
257+ * This program is distributed in the hope that it will be useful, but
258+ * WITHOUT ANY WARRANTY; without even the implied warranties of
259+ * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
260+ * PURPOSE. See the GNU General Public License for more details.
261+ *
262+ * You should have received a copy of the GNU General Public License along
263+ * with this program. If not, see <http://www.gnu.org/licenses/>.
264+ *
265+ * Authors:
266+ * Charles Kerr <charles.kerr@canonical.com>
267+ */
268+
269+#include "glib-fixture.h"
270+#include "print-to.h"
271+
272+#include <datetime/appointment.h>
273+#include <datetime/menu.h>
274+
275+#include <vector>
276+
277+using MenuAppointmentFixture = GlibFixture;
278+
279+using namespace unity::indicator::datetime;
280+
281+namespace
282+{
283+ Appointment create_appointment(
284+ const Appointment::Type& type,
285+ const std::string& uid,
286+ const std::string& summary,
287+ const DateTime& begin,
288+ const DateTime& end)
289+ {
290+ Appointment a;
291+ a.type = type;
292+ a.uid = uid;
293+ a.summary = summary;
294+ a.begin = begin;
295+ a.end = end;
296+ return a;
297+ }
298+}
299+
300+TEST_F(MenuAppointmentFixture, DisplayEvents)
301+{
302+ const auto airport = create_appointment(
303+ Appointment::UBUNTU_ALARM,
304+ "uid-airport",
305+ "Pick Aunt Mabel up at the airport",
306+ DateTime::Local(2016,12,24,10,0,0),
307+ DateTime::Local(2016,12,24,10,0,0)
308+ );
309+
310+ const auto christmas_eve_candle_service = create_appointment(
311+ Appointment::EVENT,
312+ "uid-christmas-eve-candle-service",
313+ "Christmas Eve Candle Service",
314+ DateTime::Local(2016,12,24,22,0,0),
315+ DateTime::Local(2016,12,24,23,0,0)
316+ );
317+
318+ const auto christmas = create_appointment(
319+ Appointment::EVENT,
320+ "uid-christmas",
321+ "Christmas",
322+ DateTime::Local(2016,12,25,0,0,0),
323+ DateTime::Local(2016,12,26,0,0,0)
324+ );
325+
326+ const auto santa = create_appointment(
327+ Appointment::UBUNTU_ALARM,
328+ "uid-santa",
329+ "Time to set out cookies and milk for Santa",
330+ DateTime::Local(2016,12,25,1,0,0),
331+ DateTime::Local(2016,12,25,1,0,0)
332+ );
333+
334+ const auto bike = create_appointment(
335+ Appointment::UBUNTU_ALARM,
336+ "uid-bike",
337+ "Remember to put out the bike, it's in the garage",
338+ DateTime::Local(2016,12,25,1,0,0),
339+ DateTime::Local(2016,12,25,1,0,0)
340+ );
341+
342+
343+ const auto christmas_lunch = create_appointment(
344+ Appointment::EVENT,
345+ "uid-christmas-lunch",
346+ "Christmas Lunch at Grandma's",
347+ DateTime::Local(2016,12,25,12,0,0),
348+ DateTime::Local(2016,12,25,14,0,0)
349+ );
350+
351+ const auto boxing_day = create_appointment(
352+ Appointment::EVENT,
353+ "uid-boxing-day",
354+ "Boxing Day",
355+ DateTime::Local(2016,12,26,0,0,0),
356+ DateTime::Local(2016,12,27,0,0,0)
357+ );
358+
359+ const auto new_years_eve = create_appointment(
360+ Appointment::EVENT,
361+ "uid-new-years-eve",
362+ "New Years' Eve",
363+ DateTime::Local(2016,12,31,0,0,0),
364+ DateTime::Local(2017,1,1,0,0,0)
365+ );
366+
367+ const auto nye_party = create_appointment(
368+ Appointment::EVENT,
369+ "uid-new-years-party",
370+ "New Year Party at Ted's",
371+ DateTime::Local(2016,12,31,19,0,0),
372+ DateTime::Local(2017, 1, 1, 2,0,0)
373+ );
374+
375+ const auto new_years_day = create_appointment(
376+ Appointment::EVENT,
377+ "uid-new-years-day",
378+ "New Years' Day",
379+ DateTime::Local(2017,1,1,0,0,0),
380+ DateTime::Local(2017,1,2,0,0,0)
381+ );
382+
383+ const auto weekday_wakeup_alarm = create_appointment(
384+ Appointment::UBUNTU_ALARM,
385+ "wakeup-alarm",
386+ "Wake Up",
387+ DateTime::Local(2017,1,3,6,0,0),
388+ DateTime::Local(2017,1,3,6,0,0)
389+ );
390+
391+ const auto dentist_appointment = create_appointment(
392+ Appointment::EVENT,
393+ "dentist",
394+ "Dentist",
395+ DateTime::Local(2017,1,5,14,0,0),
396+ DateTime::Local(2017,1,5,15,0,0)
397+ );
398+
399+ const auto marcus_birthday = create_appointment(
400+ Appointment::EVENT,
401+ "uid-mlk",
402+ "Marcus' Birthday",
403+ DateTime::Local(2017,1,8,0,0,0),
404+ DateTime::Local(2017,1,9,0,0,0)
405+ );
406+
407+ const auto mlk_day = create_appointment(
408+ Appointment::EVENT,
409+ "uid-mlk",
410+ "Martin Luther King Day",
411+ DateTime::Local(2017,1,16,0,0,0),
412+ DateTime::Local(2017,1,17,0,0,0)
413+ );
414+
415+ const auto rodney_party = create_appointment(
416+ Appointment::EVENT,
417+ "uid-rodney",
418+ "Rodney's Party",
419+ DateTime::Local(2017,1,30,19,0,0),
420+ DateTime::Local(2017,1,30,23,0,0)
421+ );
422+
423+ const auto pub_with_pawel = create_appointment(
424+ Appointment::UBUNTU_ALARM,
425+ "uid-pawel",
426+ "Meet Pawel at the Pub",
427+ DateTime::Local(2017,2,4,19,0,0),
428+ DateTime::Local(2017,2,4,19,0,0)
429+ );
430+
431+ const struct
432+ {
433+ const char* const description;
434+ DateTime start;
435+ std::vector<Appointment> appointments;
436+ std::vector<Appointment> expected_display_appointments;
437+ int max_items;
438+ }
439+ tests[] =
440+ {
441+ {
442+ "test presentation order: full-day events come before part-day events",
443+ DateTime::Local(2016,12,25,6,0,0),
444+ std::vector<Appointment>({christmas, christmas_lunch, boxing_day, new_years_eve}),
445+ std::vector<Appointment>({christmas, christmas_lunch, boxing_day, new_years_eve}),
446+ 5
447+ },
448+ {
449+ "test presentation order: part-day events in chronological order",
450+ DateTime::Local(2016,12,24,0,0,0),
451+ std::vector<Appointment>({airport, christmas_eve_candle_service, christmas, santa, christmas_lunch}),
452+ std::vector<Appointment>({airport, christmas_eve_candle_service, christmas, santa, christmas_lunch}),
453+ 5
454+ },
455+ {
456+ "test presentation order: multiple events with the same start+end sorted alphabetically",
457+ DateTime::Local(2016,12,25,0,0,0),
458+ std::vector<Appointment>({christmas, bike, santa, christmas_lunch}),
459+ std::vector<Appointment>({christmas, bike, santa, christmas_lunch}),
460+ 5
461+ },
462+ {
463+ "test culling priority: today's part-day events outrank today's full-day events",
464+ DateTime::Local(2016,12,25,1,0,0),
465+ std::vector<Appointment>({christmas, santa, christmas_lunch}),
466+ std::vector<Appointment>({santa, christmas_lunch}),
467+ 2
468+ },
469+ {
470+ "test culling priority: events later today outrank both tomorrow's full-day and part-day events",
471+ DateTime::Local(2016,12,24,0,0,0),
472+ std::vector<Appointment>({christmas_eve_candle_service, christmas, santa}),
473+ std::vector<Appointment>({christmas_eve_candle_service}),
474+ 1
475+ },
476+ {
477+ "test edge cases: confirm <max events works ok",
478+ DateTime::Local(2016,12,24,0,0,0),
479+ std::vector<Appointment>({christmas, christmas_lunch}),
480+ std::vector<Appointment>({christmas, christmas_lunch}),
481+ 5
482+ },
483+ {
484+ "test edge cases: confirm 0 events works ok",
485+ DateTime::Local(2016,12,24,0,0,0),
486+ std::vector<Appointment>({}),
487+ std::vector<Appointment>({}),
488+ 5
489+ },
490+ {
491+ "test edge cases: confirm max-events of 0 doesn't crash",
492+ DateTime::Local(2016,12,24,0,0,0),
493+ std::vector<Appointment>({christmas, bike, santa, christmas_lunch}),
494+ std::vector<Appointment>({}),
495+ 0
496+ }
497+ };
498+
499+ for (const auto& test : tests)
500+ {
501+ g_debug("running test: %s", test.description);
502+
503+ // run the test...
504+ ASSERT_EQ(test.expected_display_appointments, Menu::get_display_appointments(test.appointments, test.start, test.max_items));
505+
506+ // ...and again with a reversed vector to confirm input order doesn't matter
507+ auto reversed = test.appointments;
508+ std::reverse(reversed.begin(), reversed.end());
509+ ASSERT_EQ(test.expected_display_appointments, Menu::get_display_appointments(reversed, test.start, test.max_items));
510+ }
511+}
512+

Subscribers

People subscribed via source and target branches