[california/wip/725786-edit-recurring] Final touches



commit a266b063c167033cbebfc298cce838560a9b0e08
Author: Jim Nelson <jim yorba org>
Date:   Wed Jul 16 18:24:47 2014 -0700

    Final touches

 src/Makefile.am                                    |    1 +
 .../backing-calendar-source-subscription.vala      |   33 ------
 src/backing/backing-calendar-source.vala           |    6 +-
 .../backing-eds-calendar-source-subscription.vala  |   54 +++++++---
 src/backing/eds/backing-eds-calendar-source.vala   |    7 +-
 src/collection/collection-iterable.vala            |   13 +++
 src/collection/collection.vala                     |   14 +++
 src/component/component-date-time.vala             |   57 ++++++-----
 src/component/component-instance.vala              |   93 +++++++++++++++++
 src/component/component-recurrence-rule.vala       |   25 +++++-
 src/host/host-create-update-event.vala             |   31 +++++-
 src/host/host-create-update-recurring.vala         |  105 +++++++++++++++-----
 src/rc/create-update-recurring.ui                  |   22 ++++
 src/tests/tests-iterable.vala                      |   45 +++++++++
 src/tests/tests.vala                               |    1 +
 15 files changed, 399 insertions(+), 108 deletions(-)
---
diff --git a/src/Makefile.am b/src/Makefile.am
index 14a1332..3976255 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -108,6 +108,7 @@ california_VALASOURCES = \
        tests/tests-calendar-month-of-year.vala \
        tests/tests-calendar-month-span.vala \
        tests/tests-calendar-wall-time.vala \
+       tests/tests-iterable.vala \
        tests/tests-quick-add.vala \
        tests/tests-quick-add-recurring.vala \
        tests/tests-string.vala \
diff --git a/src/backing/backing-calendar-source-subscription.vala 
b/src/backing/backing-calendar-source-subscription.vala
index bf4fbc4..0ffd6f7 100644
--- a/src/backing/backing-calendar-source-subscription.vala
+++ b/src/backing/backing-calendar-source-subscription.vala
@@ -337,39 +337,6 @@ public abstract class CalendarSourceSubscription : BaseObject {
         return instances.contains(uid) ? instances.get(uid) : null;
     }
     
-    /**
-     * Returns the master { link Component.Instance} for the { link Component.UID}.
-     *
-     * @returns null if the UID has not been sene.
-     */
-    public Component.Instance? master_for_uid(Component.UID uid) {
-        return traverse<Component.Instance>(instances.get(uid))
-            .first_matching(instance => instance.is_master_instance);
-    }
-    
-    /**
-     * Returns the seen { link Component.Instance} matching the supplied (possibly partially
-     * filled-out) Instance.
-     *
-     * This is for duplicate detection, especially if the { link Backing} is receiving raw iCal
-     * source and needs to verify if it's been parsed and introduced into the system.
-     *
-     * A blank Instance with partial fields filled out can be supplied.
-     */
-    public Component.Instance? has_instance(Component.Instance instance) {
-        Gee.Collection<Component.Instance>? seen_instances = for_uid(instance.uid);
-        if (seen_instances == null || seen_instances.size == 0)
-            return null;
-        
-        // for every instance matching its UID, look for the original
-        foreach (Component.Instance seen_instance in seen_instances) {
-            if (seen_instance.equal_to(instance))
-                return seen_instance;
-        }
-        
-        return null;
-    }
-    
     public override string to_string() {
         return "%s::%s".printf(calendar.to_string(), window.to_string());
     }
diff --git a/src/backing/backing-calendar-source.vala b/src/backing/backing-calendar-source.vala
index 7b2fac0..2e85e24 100644
--- a/src/backing/backing-calendar-source.vala
+++ b/src/backing/backing-calendar-source.vala
@@ -19,7 +19,7 @@ namespace California.Backing {
 
 public abstract class CalendarSource : Source {
     /**
-     * The affected range of a modification or removal operation.
+     * The affected range of a removal operation.
      *
      * Note that zero (0) does ''not'' mean "none", it means { link AffectedInstances.THIS}.  The
      * additional enums merely expand the scope of the default, which is the supplied instance.
@@ -64,6 +64,10 @@ public abstract class CalendarSource : Source {
     /**
      * Updates an existing { link Component} instance on the backing { link CalendarSource}.
      *
+     * To update all { link Instance}s of a recurring { link Instance}, submit the
+     * { link Component.Instance.master} with modifications rather than one of its generated
+     * instances.  Submit a generated instance to update only that one.
+     *
      * Outstanding { link CalendarSourceSubscriptions} will eventually report the changes when
      * ready.
      */
diff --git a/src/backing/eds/backing-eds-calendar-source-subscription.vala 
b/src/backing/eds/backing-eds-calendar-source-subscription.vala
index 30a314f..ab1d582 100644
--- a/src/backing/eds/backing-eds-calendar-source-subscription.vala
+++ b/src/backing/eds/backing-eds-calendar-source-subscription.vala
@@ -188,32 +188,56 @@ internal class EdsCalendarSourceSubscription : CalendarSourceSubscription {
             if (!has_uid(uid))
                 continue;
             
-            // find original for this one
+            Component.DateTime? rid = null;
+            try {
+                rid = new Component.DateTime(ical_component, iCal.icalproperty_kind.RECURRENCEID_PROPERTY);
+            } catch (ComponentError comperr) {
+                if (!(comperr is ComponentError.UNAVAILABLE)) {
+                    debug("Unable to get RID of modified component: %s", comperr.message);
+                    
+                    continue;
+                }
+            }
+            
+            // get all instances known for this UID to find original to alter
             Gee.Collection<Component.Instance>? instances = for_uid(uid);
-            if (instances == null || instances.size == 0)
-                continue;
             
-            foreach (Component.Instance instance in instances) {
-                Component.Event? known_event = instance as Component.Event;
-                if (known_event == null)
+            // if no RID, then only one should be returned
+            Component.Instance? instance = null;
+            if (rid == null) {
+                instance = traverse<Component.Instance>(instances).one();
+                if (instance == null) {
+                    debug("%d instances found for modified instance, expected 1", 
Collection.size(instances));
+                    
                     continue;
-                
-                try {
-                    known_event.full_update(ical_component, null);
-                } catch (Error err) {
-                    debug("Unable to update event %s: %s", known_event.to_string(), err.message);
+                }
+            } else {
+                // if RID != null, then find the matching instance
+                instance = traverse<Component.Instance>(instances)
+                    .first_matching(inst => inst.rid != null && inst.rid.equal_to(rid));
+                if (instance == null) {
+                    debug("Cannot find instance with UID %s RID %s, skipping", uid.to_string(), 
rid.to_string());
                     
                     continue;
                 }
+            }
+            
+            Component.Event? modified_event = instance as Component.Event;
+            if (modified_event == null)
+                continue;
+            
+            try {
+                modified_event.full_update(ical_component, null);
+            } catch (Error err) {
+                debug("Unable to update event %s: %s", modified_event.to_string(), err.message);
                 
-                notify_instance_altered(known_event);
+                continue;
             }
             
-            if (instances.size > 1)
-                debug("Warning: updated %d modified events, expecting only 1", instances.size);
+            notify_instance_altered(modified_event);
         }
         
-        // add any recurring events
+        // remove and re-add any recurring events
         on_objects_added(add_list);
     }
     
diff --git a/src/backing/eds/backing-eds-calendar-source.vala 
b/src/backing/eds/backing-eds-calendar-source.vala
index 92a7b1c..d6945d2 100644
--- a/src/backing/eds/backing-eds-calendar-source.vala
+++ b/src/backing/eds/backing-eds-calendar-source.vala
@@ -152,7 +152,10 @@ internal class EdsCalendarSource : CalendarSource {
         Cancellable? cancellable = null) throws Error {
         check_open();
         
-        yield client.modify_object(instance.ical_component, E.CalObjModType.THIS, cancellable);
+        E.CalObjModType modtype =
+            instance.can_generate_instances ? E.CalObjModType.ALL : E.CalObjModType.THIS;
+        
+        yield client.modify_object(instance.ical_component, modtype, cancellable);
     }
     
     public override async void remove_all_instances_async(Component.UID uid,
@@ -171,7 +174,7 @@ internal class EdsCalendarSource : CalendarSource {
         // include an EXDATE in the original iCal source ... I don't quite understand the benefit of
         // this, as this suggests (a) other calendar clients won't learn of the removal and (b) the
         // instance will be re-generated the next time the user runs an EDS calendar client.  In
-        // either case, ONLY maps to our desired effect by adding an EXDATE to the iCal source.
+        // either case, THIS maps to our desired effect by adding an EXDATE to the iCal source.
         switch (affected) {
             case CalendarSource.AffectedInstances.THIS:
                 yield client.remove_object(uid.value, rid.value, E.CalObjModType.THIS, cancellable);
diff --git a/src/collection/collection-iterable.vala b/src/collection/collection-iterable.vala
index 395247c..5648456 100644
--- a/src/collection/collection-iterable.vala
+++ b/src/collection/collection-iterable.vala
@@ -191,6 +191,19 @@ public class Iterable<G> : Object {
             .map<A>(g => { return (A) g; }));
     }
     
+    /**
+     * Returns the first element in the { link Iterable} if and only if it is the only one,
+     * otherwise returns null.
+     */
+    public G? one() {
+        if (!i.next())
+            return null;
+        
+        G element = i  get();
+        
+        return !i.next() ? element : null;
+    }
+    
     public G? first() {
         return (i.next() ? i  get() : null);
     }
diff --git a/src/collection/collection.vala b/src/collection/collection.vala
index ba28f21..6557566 100644
--- a/src/collection/collection.vala
+++ b/src/collection/collection.vala
@@ -18,5 +18,19 @@ public void terminate() {
         return;
 }
 
+/**
+ * Returns true if the Collection is null or empty (zero elements).
+ */
+public inline bool is_empty(Gee.Collection? c) {
+    return c == null || c.size == 0;
+}
+
+/**
+ * Returns the size of the Collection, zero if null.
+ */
+public inline int size(Gee.Collection? c) {
+    return !is_empty(c) ? c.size : 0;
+}
+
 }
 
diff --git a/src/component/component-date-time.vala b/src/component/component-date-time.vala
index 85825c5..1ad627c 100644
--- a/src/component/component-date-time.vala
+++ b/src/component/component-date-time.vala
@@ -62,13 +62,7 @@ public class DateTime : BaseObject, Gee.Hashable<DateTime>, Gee.Comparable<DateT
     public iCal.icalproperty_kind kind;
     
     /**
-     * Creates a new { link DateTime} for the iCal component of the property kind.
-     *
-     * Note that the only properties currently supported are:
-     * * DTSTART_PROPERTY
-     * * DTEND_PROPERTY
-     * * DTSTAMP_PROPERTY
-     * * RECURRENCEID_PROPERTY
+     * Creates a new { link DateTime} for the first iCal property of the kind.
      *
      * @throws ComponentError.UNAVAILABLE if not found
      * @throws ComponentError.INVALID if not a valid DATE or DATE-TIME
@@ -79,32 +73,33 @@ public class DateTime : BaseObject, Gee.Hashable<DateTime>, Gee.Comparable<DateT
         if (prop == null)
             throw new ComponentError.UNAVAILABLE("No property of kind %s", ical_prop_kind.to_string());
         
-        switch (ical_prop_kind) {
-            case iCal.icalproperty_kind.DTSTAMP_PROPERTY:
-                dt = prop.get_dtstamp();
-            break;
-            
-            case iCal.icalproperty_kind.DTSTART_PROPERTY:
-                dt = prop.get_dtstart();
-            break;
-            
-            case iCal.icalproperty_kind.DTEND_PROPERTY:
-                dt = prop.get_dtend();
+        init_from_property(prop);
+    }
+    
+    public DateTime.from_property(iCal.icalproperty prop) throws ComponentError {
+        init_from_property(prop);
+    }
+    
+    private void init_from_property(iCal.icalproperty prop) throws ComponentError {
+        unowned iCal.icalvalue prop_value = prop.get_value();
+        switch (prop_value.isa()) {
+            case iCal.icalvalue_kind.DATE_VALUE:
+                dt = prop_value.get_date();
             break;
             
-            case iCal.icalproperty_kind.RECURRENCEID_PROPERTY:
-                dt = prop.get_recurrenceid();
+            case iCal.icalvalue_kind.DATETIME_VALUE:
+                dt = prop_value.get_datetime();
             break;
             
             default:
-                assert_not_reached();
+                throw new ComponentError.INVALID("Not a DATE/DATE-TIME value: %s", 
prop_value.isa().to_string());
         }
         
         if (iCal.icaltime_is_null_time(dt) != 0)
-            throw new ComponentError.INVALID("DATE-TIME for %s is null time", ical_prop_kind.to_string());
+            throw new ComponentError.INVALID("DATE-TIME for %s is null time", prop.isa().to_string());
         
         if (iCal.icaltime_is_valid_time(dt) == 0)
-            throw new ComponentError.INVALID("DATE-TIME for %s is invalid", ical_prop_kind.to_string());
+            throw new ComponentError.INVALID("DATE-TIME for %s is invalid", prop.isa().to_string());
         
         unowned iCal.icalparameter? param = prop.get_first_parameter(iCal.icalparameter_kind.TZID_PARAMETER);
         if (param != null) {
@@ -120,7 +115,7 @@ public class DateTime : BaseObject, Gee.Hashable<DateTime>, Gee.Comparable<DateT
             zone = new Calendar.OlsonZone(dt.zone->get_location());
         }
         
-        kind = ical_prop_kind;
+        kind = prop.isa();
         value = prop.get_value_as_string();
     }
     
@@ -216,6 +211,20 @@ public class DateTime : BaseObject, Gee.Hashable<DateTime>, Gee.Comparable<DateT
     }
     
     /**
+     * Returns an iCal value for the { link DateTime}.
+     */
+    internal iCal.icalvalue to_ical_value() {
+        iCal.icalvalue prop_value = new iCal.icalvalue(
+            is_date ? iCal.icalvalue_kind.DATE_VALUE : iCal.icalvalue_kind.DATE_VALUE);
+        if (is_date)
+            prop_value.set_date(dt);
+        else
+            prop_value.set_datetime(dt);
+        
+        return prop_value;
+    }
+    
+    /**
      * Convert two { link DateTime}s into a { link Calendar.DateSpan} or a
      * { link Calendar.ExactTimeSpan} depending on what they represent.
      *
diff --git a/src/component/component-instance.vala b/src/component/component-instance.vala
index 002dd08..aff3575 100644
--- a/src/component/component-instance.vala
+++ b/src/component/component-instance.vala
@@ -35,6 +35,8 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
     public const string PROP_ICAL_COMPONENT = "ical-component";
     public const string PROP_RRULE = "rrule";
     public const string PROP_RID = "rid";
+    public const string PROP_EXDATES = "exdates";
+    public const string PROP_RDATES = "rdates";
     public const string PROP_SEQUENCE = "sequence";
     public const string PROP_MASTER = "master";
     
@@ -83,6 +85,40 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
     public Component.DateTime? rid { get; set; default = null; }
     
     /**
+     * All EXDATEs (DATE-TIME exceptions for recurring instances) in the { link Instance}.
+     *
+     * Returns a read-only set of { link DateTime}s.  Use { link set_exdates} to change.
+     *
+     * See [[https://tools.ietf.org/html/rfc5545#section-3.8.5.1]]
+     */
+    private Gee.SortedSet<DateTime>? _exdates = null;
+    public Gee.SortedSet<DateTime>? exdates {
+        owned get {
+            return Collection.is_empty(_exdates) ? null : _exdates.read_only_view;
+        }
+        
+        set {
+            _exdates = value;
+        }
+    }
+    
+    /**
+     * All RDATEs (DATE-TIMEs manually set for recurring instances) in the { link Instance}.
+     *
+     * See [[https://tools.ietf.org/html/rfc5545#section-3.8.5.2]]
+     */
+    private Gee.SortedSet<DateTime>? _rdates = null;
+    public Gee.SortedSet<DateTime>? rdates {
+        owned get {
+            return Collection.is_empty(_rdates) ? null : _rdates.read_only_view;
+        }
+        
+        set {
+            _rdates = value;
+        }
+    }
+    
+    /**
      * Returns true if the { link Instance} is a master instance.
      *
      * A master instance is one that has not been generated from another Instance's recurring
@@ -312,6 +348,9 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
                 debug("Unable to parse RRULE for %s: %s", to_string(), comperr.message);
         }
         
+        exdates = get_multiple_date_times(iCal.icalproperty_kind.EXDATE_PROPERTY);
+        rdates = get_multiple_date_times(iCal.icalproperty_kind.RDATE_PROPERTY);
+        
         // save own copy of component; no ownership transferrance w/ current bindings
         if (_ical_component != ical_component)
             _ical_component = ical_component.clone();
@@ -344,6 +383,20 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
                     rrule.add_to_ical(ical_component);
             break;
             
+            case PROP_EXDATES:
+                if (Collection.is_empty(exdates))
+                    remove_all_properties(iCal.icalproperty_kind.EXDATE_PROPERTY);
+                else
+                    set_multiple_date_times(iCal.icalproperty_kind.EXDATE_PROPERTY, exdates);
+            break;
+            
+            case PROP_RDATES:
+                if (Collection.is_empty(rdates))
+                    remove_all_properties(iCal.icalproperty_kind.RDATE_PROPERTY);
+                else
+                    set_multiple_date_times(iCal.icalproperty_kind.RDATE_PROPERTY, rdates);
+            break;
+            
             default:
                 altered = false;
             break;
@@ -416,6 +469,46 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
     }
     
     /**
+     * Convenience method to convert a collection of DATE/DATE-TIME properties into a SortedSet of
+     * { link DateTime}s.
+     *
+     * @see set_multiple_date_times
+     */
+    protected Gee.SortedSet<DateTime>? get_multiple_date_times(iCal.icalproperty_kind kind) {
+        Gee.SortedSet<DateTime> date_times = new Gee.TreeSet<DateTime>();
+        
+        unowned iCal.icalproperty? prop = ical_component.get_first_property(kind);
+        while (prop != null) {
+            try {
+                date_times.add(new DateTime.from_property(prop));
+            } catch (ComponentError comperr) {
+                debug("Unable to parse DATE/DATE-TIME for %s: %s", kind.to_string(), comperr.message);
+            }
+            
+            prop = ical_component.get_next_property(kind);
+        }
+        
+        return date_times.size > 0 ? date_times : null;
+    }
+    
+    /**
+     * Convenience method to set (replace) a collection of DATE/DATE-TIME properties from a
+     * Collection of { link DateTime}s.
+     *
+     * @see get_multiple_date_times
+     */
+    protected void set_multiple_date_times(iCal.icalproperty_kind kind, Gee.Collection<DateTime> date_times) 
{
+        remove_all_properties(kind);
+        
+        foreach (DateTime date_time in date_times) {
+            iCal.icalproperty prop = new iCal.icalproperty(kind);
+            prop.set_value(date_time.to_ical_value());
+            
+            ical_component.add_property(prop);
+        }
+    }
+    
+    /**
      * Convenience method to convert a { link Calendar.DateSpan} to a pair of iCal DATEs.
      *
      * dtend_inclusive indicates whether the dt_end should be treated as inclusive or exclusive
diff --git a/src/component/component-recurrence-rule.vala b/src/component/component-recurrence-rule.vala
index 845e94e..9bd45e3 100644
--- a/src/component/component-recurrence-rule.vala
+++ b/src/component/component-recurrence-rule.vala
@@ -24,7 +24,7 @@ public class RecurrenceRule : BaseObject {
      * Enumeration of various BY rules (BYSECOND, BYMINUTE, etc.)
      */
     public enum ByRule {
-        SECOND,
+        SECOND = 0,
         MINUTE,
         HOUR,
         DAY,
@@ -32,7 +32,11 @@ public class RecurrenceRule : BaseObject {
         YEAR_DAY,
         WEEK_NUM,
         MONTH,
-        SET_POS
+        SET_POS,
+        /**
+         * The number of { link ByRule}s, this is not a valid value.
+         */
+        COUNT;
     }
     
     /**
@@ -338,6 +342,8 @@ public class RecurrenceRule : BaseObject {
                 dow = Calendar.DayOfWeek.for(dow_value, Calendar.FirstOfWeek.SUNDAY);
             } catch (CalendarError calerr) {
                 debug("Unable to decode day of week value %d: %s", dow_value, calerr.message);
+                
+                return false;
             }
         }
         
@@ -487,6 +493,21 @@ public class RecurrenceRule : BaseObject {
     }
     
     /**
+     * Returns a Gee.Set of { link ByRule}s that are active, i.e. have defined rules.
+     */
+    public Gee.Set<ByRule> get_active_by_rules() {
+        Gee.Set<ByRule> active = new Gee.HashSet<ByRule>();
+        for (int ctr = 0; ctr < ByRule.COUNT; ctr++) {
+            ByRule by_rule = (ByRule) ctr;
+            
+            if (get_by_set(by_rule).size > 0)
+                active.add(by_rule);
+        }
+        
+        return active;
+    }
+    
+    /**
      * Converts a { link RecurrenceRule} into an iCalendar RRULE property and adds it to the
      * iCal component.
      *
diff --git a/src/host/host-create-update-event.vala b/src/host/host-create-update-event.vala
index 69d6731..8f0b45f 100644
--- a/src/host/host-create-update-event.vala
+++ b/src/host/host-create-update-event.vala
@@ -306,14 +306,37 @@ public class CreateUpdateEvent : Gtk.Grid, Toolkit.Card {
     // TODO: Now that a clone is being used for editing, can directly bind controls properties to
     // Event's properties and update that way ... doesn't quite work when updating the master event,
     // however
-    private void update_component(Component.Event target, bool update_dtstart) {
+    private void update_component(Component.Event target, bool replace_dtstart) {
         target.calendar_source = calendar_model.active;
         target.summary = summary_entry.text;
         target.location = location_entry.text;
         target.description = description_textview.buffer.text;
         
-        if (!update_dtstart)
+        // if updating the master, don't replace the dtstart/dtend, but do want to adjust it from
+        // DATE to DATE-TIME or vice-versa
+        if (!replace_dtstart) {
+            if (target.is_all_day != all_day_toggle.active) {
+                if (all_day_toggle.active) {
+                    target.set_event_date_span(target.get_event_date_span(null));
+                } else {
+                    // use existing timezone unless not specified in original event
+                    Calendar.DateSpan target_date_span = target.get_event_date_span(null);
+                    Calendar.Timezone tz = (target.exact_time_span != null)
+                        ? target.exact_time_span.start_exact_time.tz
+                        : Calendar.Timezone.local;
+                    target.set_event_exact_time_span(
+                        new Calendar.ExactTimeSpan(
+                            new Calendar.ExactTime(tz, target_date_span.start_date,
+                                time_map.get(dtstart_time_combo.get_active_text())),
+                            new Calendar.ExactTime(tz, target_date_span.end_date,
+                                time_map.get(dtend_time_combo.get_active_text()))
+                        )
+                    );
+                }
+            }
+            
             return;
+        }
         
         if (all_day_toggle.active) {
             target.set_event_date_span(selected_date_span);
@@ -333,8 +356,8 @@ public class CreateUpdateEvent : Gtk.Grid, Toolkit.Card {
         }
     }
     
-    private void create_update_event(Component.Event target, bool update_dtstart) {
-        update_component(target, update_dtstart);
+    private void create_update_event(Component.Event target, bool replace_dtstart) {
+        update_component(target, replace_dtstart);
         
         if (is_update)
             update_event_async.begin(target, null);
diff --git a/src/host/host-create-update-recurring.vala b/src/host/host-create-update-recurring.vala
index 30dc901..ce6c4eb 100644
--- a/src/host/host-create-update-recurring.vala
+++ b/src/host/host-create-update-recurring.vala
@@ -95,6 +95,9 @@ public class CreateUpdateRecurring : Gtk.Grid, Toolkit.Card {
     private Gtk.RadioButton ends_on_radiobutton;
     
     [GtkChild]
+    private Gtk.Label warning_label;
+    
+    [GtkChild]
     private Gtk.Button end_date_button;
     
     [GtkChild]
@@ -221,20 +224,9 @@ public class CreateUpdateRecurring : Gtk.Grid, Toolkit.Card {
         event = (Component.Event) message;
         master = event.is_master_instance ? event : (Component.Event) event.master;
         
-        // need to use the master component in order to update the master RRULE
-        if (!can_update_recurring(event)) {
-            jump_back();
-            
-            return;
-        }
-        
         update_controls();
     }
     
-    public static bool can_update_recurring(Component.Event event) {
-        return event.is_master_instance || (event.master is Component.Event);
-    }
-    
     private void update_controls() {
         make_recurring_checkbutton.active = (master.rrule != null);
         
@@ -257,45 +249,37 @@ public class CreateUpdateRecurring : Gtk.Grid, Toolkit.Card {
             repeats_combobox.active = Repeats.DAILY;
             every_entry.text = "1";
             never_radiobutton.active = true;
+            warning_label.visible = false;
             
             return;
         }
         
         // "Repeats" combobox
         switch (master.rrule.freq) {
-            case iCal.icalrecurrencetype_frequency.DAILY_RECURRENCE:
-                repeats_combobox.active = Repeats.DAILY;
-            break;
-            
-            // TODO: Don't allow for editing weekly rules with anything but BYDAY or BYDAY where
-            // the position value is non-zero
             case iCal.icalrecurrencetype_frequency.WEEKLY_RECURRENCE:
                 repeats_combobox.active = Repeats.WEEKLY;
             break;
             
-            // TODO: Don't support MONTHLY RRULEs with multiple ByRules or ByRules we can't
-            // represent ... basically, non-simple repeating rules
-            // TODO: BYDAY should be the exact month-day of week for the DTSTART, MONTH_DAY should
-            // be the month-day of the month for the DTSTART
             case iCal.icalrecurrencetype_frequency.MONTHLY_RECURRENCE:
                 bool by_day = master.rrule.get_by_rule(Component.RecurrenceRule.ByRule.DAY).size > 0;
                 bool by_monthday = master.rrule.get_by_rule(Component.RecurrenceRule.ByRule.MONTH_DAY).size 
0;
                 
-                if (by_day && !by_monthday)
-                    repeats_combobox.active = Repeats.DAY_OF_THE_WEEK;
-                else if (!by_day && by_monthday)
+                // fall back on month day of the week
+                if (!by_day && by_monthday)
                     repeats_combobox.active = Repeats.DAY_OF_THE_MONTH;
                 else
-                    assert_not_reached();
+                    repeats_combobox.active = Repeats.DAY_OF_THE_WEEK;
             break;
             
             case iCal.icalrecurrencetype_frequency.YEARLY_RECURRENCE:
                 repeats_combobox.active = Repeats.YEARLY;
             break;
             
-            // TODO: Don't support sub-day RRULEs
+            // Fall back on Daily for default, warning label is shown if anything not supported
+            case iCal.icalrecurrencetype_frequency.DAILY_RECURRENCE:
             default:
-                assert_not_reached();
+                repeats_combobox.active = Repeats.DAILY;
+            break;
         }
         
         // "Every" entry
@@ -328,6 +312,69 @@ public class CreateUpdateRecurring : Gtk.Grid, Toolkit.Card {
             ends_on_radiobutton.active = true;
             end_date = master.rrule.get_recurrence_end_date();
         }
+        
+        // look for RRULEs that our editor cannot deal with
+        string? supported = is_supported_rrule();
+        if (supported != null)
+            debug("Unsupported RRULE: %s", supported);
+        
+        warning_label.visible = supported != null;
+    }
+    
+    // Returns a logging string for why not reported, null if supported
+    private string? is_supported_rrule() {
+        // only some frequencies support, and in some of those, certain requirements
+        switch (master.rrule.freq) {
+            case iCal.icalrecurrencetype_frequency.DAILY_RECURRENCE:
+            case iCal.icalrecurrencetype_frequency.YEARLY_RECURRENCE:
+                // do nothing, continue
+            break;
+            
+            case iCal.icalrecurrencetype_frequency.WEEKLY_RECURRENCE:
+                // can only hold BYDAY rules and all BYDAY rules must be zero
+                Gee.Set<Component.RecurrenceRule.ByRule> active = master.rrule.get_active_by_rules();
+                if (!active.contains(Component.RecurrenceRule.ByRule.DAY))
+                    return "weekly-not-byday";
+                
+                if (active.size > 1)
+                    return "weekly-multiple-byrules";
+                
+                foreach (int day in master.rrule.get_by_rule(Component.RecurrenceRule.ByRule.DAY)) {
+                    int position;
+                    if (!Component.RecurrenceRule.decode_day(day, null, out position))
+                        return "weekly-undecodeable-byday";
+                    
+                    if (position != 0)
+                        return "weekly-nonzero-byday-position";
+                }
+            break;
+            
+            // Must be a "simple" monthly recurrence
+            case iCal.icalrecurrencetype_frequency.MONTHLY_RECURRENCE:
+                bool by_day = master.rrule.get_by_rule(Component.RecurrenceRule.ByRule.DAY).size > 0;
+                bool by_monthday = master.rrule.get_by_rule(Component.RecurrenceRule.ByRule.MONTH_DAY).size 
0;
+                
+                // can support one and only one
+                if (by_day == by_monthday)
+                    return "monthly-byday-and-bymonthday";
+                
+                if (master.rrule.get_active_by_rules().size > 1)
+                    return "monthly-multiple-byrules";
+            break;
+            
+            default:
+                return "unsupported-frequency";
+        }
+        
+        // do not support editing w/ EXDATEs
+        if (!Collection.is_empty(master.exdates))
+            return "exdates";
+        
+        // do not support editing w/ RDATEs
+        if (!Collection.is_empty(master.rdates))
+            return "rdates";
+        
+        return null;
     }
     
     [GtkCallback]
@@ -526,6 +573,10 @@ public class CreateUpdateRecurring : Gtk.Grid, Toolkit.Card {
             }
         }
         
+        // remove EXDATEs and RDATEs, those are not currently supported
+        master.exdates = null;
+        master.rdates = null;
+        
         master.make_recurring(rrule);
     }
 }
diff --git a/src/rc/create-update-recurring.ui b/src/rc/create-update-recurring.ui
index 302c5ec..205cef3 100644
--- a/src/rc/create-update-recurring.ui
+++ b/src/rc/create-update-recurring.ui
@@ -520,6 +520,28 @@
       </object>
       <packing>
         <property name="left_attach">0</property>
+        <property name="top_attach">3</property>
+        <property name="width">2</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="warning_label">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="no_show_all">True</property>
+        <property name="margin_left">16</property>
+        <property name="margin_top">8</property>
+        <property name="xalign">0</property>
+        <property name="label" translatable="yes">WARNING: California cannot edit this event's recurring 
criteria.
+• Press Cancel to keep the current criteria.
+• Press OK to overwrite the existing criteria with your changes.</property>
+        <attributes>
+          <attribute name="weight" value="bold"/>
+        </attributes>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
         <property name="top_attach">2</property>
         <property name="width">2</property>
         <property name="height">1</property>
diff --git a/src/tests/tests-iterable.vala b/src/tests/tests-iterable.vala
new file mode 100644
index 0000000..905c721
--- /dev/null
+++ b/src/tests/tests-iterable.vala
@@ -0,0 +1,45 @@
+/* Copyright 2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later).  See the COPYING file in this distribution.
+ */
+
+namespace California.Tests {
+
+private class Iterable : UnitTest.Harness {
+    public Iterable() {
+        add_case("one-zero", one_zero);
+        add_case("one-one", one_one);
+        add_case("one_many", one_many);
+    }
+    
+    protected override void setup() throws Error {
+        Collection.init();
+    }
+    
+    protected override void teardown() {
+        Collection.terminate();
+    }
+    
+    private bool one_zero() throws Error {
+        return traverse<int?>(new Gee.ArrayList<int?>()).one() == null;
+    }
+    
+    private bool one_one() throws Error {
+        Gee.ArrayList<int?> list = new Gee.ArrayList<int?>();
+        list.add(1);
+        
+        return traverse<int?>(list).one() == 1;
+    }
+    
+    private bool one_many() throws Error {
+        Gee.ArrayList<int?> list = new Gee.ArrayList<int?>();
+        list.add(1);
+        list.add(2);
+        
+        return traverse<int?>(list).one() == null;
+    }
+}
+
+}
+
diff --git a/src/tests/tests.vala b/src/tests/tests.vala
index b8dd74c..24c171e 100644
--- a/src/tests/tests.vala
+++ b/src/tests/tests.vala
@@ -12,6 +12,7 @@ public int run(string[] args) {
         LogLevelFlags.LEVEL_WARNING | LogLevelFlags.LEVEL_ERROR | LogLevelFlags.LEVEL_CRITICAL);
     
     UnitTest.Harness.register(new String());
+    UnitTest.Harness.register(new Iterable());
     UnitTest.Harness.register(new CalendarDate());
     UnitTest.Harness.register(new CalendarMonthSpan());
     UnitTest.Harness.register(new CalendarMonthOfYear());


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]