[california/wip/731543-attendees] Parse and display organizers in Show Event



commit 643d089ad435a04a213544842927d0977dbabb7d
Author: Jim Nelson <jim yorba org>
Date:   Wed Nov 5 16:23:44 2014 -0800

    Parse and display organizers in Show Event

 src/backing/backing-source.vala                  |    8 ++-
 src/backing/eds/backing-eds-calendar-source.vala |   14 +++
 src/collection/collection-iterable.vala          |   12 +++
 src/component/component-instance.vala            |  104 ++++++++++++++++------
 src/component/component-person.vala              |   20 ++++-
 src/host/host-show-event.vala                    |   36 ++++++--
 src/rc/show-event.ui                             |   49 ++++++++--
 7 files changed, 196 insertions(+), 47 deletions(-)
---
diff --git a/src/backing/backing-source.vala b/src/backing/backing-source.vala
index 26a889f..9269e0e 100644
--- a/src/backing/backing-source.vala
+++ b/src/backing/backing-source.vala
@@ -22,6 +22,7 @@ public abstract class Source : BaseObject, Gee.Comparable<Source> {
     public const string PROP_VISIBLE = "visible";
     public const string PROP_READONLY = "read-only";
     public const string PROP_COLOR = "color";
+    public const string PROP_OWNER_MAILBOX = "owner-mailbox";
     
     /**
      * A unique identifier for the { link Source}.
@@ -52,6 +53,11 @@ public abstract class Source : BaseObject, Gee.Comparable<Source> {
     public string title { get; set; }
     
     /**
+     * The owner of this calendar's mailbox (email address), i.e. "bob example com".
+     */
+    public string? owner_mailbox { get; protected set; default = null; }
+    
+    /**
      * Whether the { link Source} should be visible to the user.
      *
      * The caller should monitor this setting to decide whether or not to display the Source's
@@ -151,7 +157,7 @@ public abstract class Source : BaseObject, Gee.Comparable<Source> {
     }
     
     public override string to_string() {
-        return title;
+        return !String.is_empty(owner_mailbox) ? "%s (%s)".printf(title, owner_mailbox) : title;
     }
 }
 
diff --git a/src/backing/eds/backing-eds-calendar-source.vala 
b/src/backing/eds/backing-eds-calendar-source.vala
index d3219fb..9f6407f 100644
--- a/src/backing/eds/backing-eds-calendar-source.vala
+++ b/src/backing/eds/backing-eds-calendar-source.vala
@@ -171,6 +171,20 @@ internal class EdsCalendarSource : CalendarSource {
         client.notify["readonly"].connect(() => {
             debug("%s readonly: %s", to_string(), client.readonly.to_string());
         });
+        
+        // TODO: This does not work for some reason, although it appears to work in Evolution ...
+        // this will be important for determining if the ORGANIZER is the calendar owner, i.e. user
+        try {
+            string prop_value;
+            if (yield client.get_backend_property("cal-email-address", cancellable, out prop_value))
+                owner_mailbox = !String.is_empty(prop_value) ? prop_value : null;
+            else
+                debug("Unable to fetch %s email address", to_string());
+        } catch (Error err) {
+            client = null;
+            
+            throw err;
+        }
     }
     
     // Invoked by EdsStore when closing and dropping all its refs
diff --git a/src/collection/collection-iterable.vala b/src/collection/collection-iterable.vala
index b5695bb..9a67a6d 100644
--- a/src/collection/collection-iterable.vala
+++ b/src/collection/collection-iterable.vala
@@ -183,6 +183,18 @@ public class Iterable<G> : Object {
         return new Iterable<G>(list.iterator());
     }
     
+    /**
+     * Sorts the elements of the { link Iterable} so that the next iteration they are in the
+     * comparator's order.
+     */
+    public Iterable<G> sort(owned Gee.EqualDataFunc<G>? equal_func = null,
+        owned CompareDataFunc<G>? compare_func = null) {
+        Gee.ArrayList<G> sorted = to_array_list(equal_func);
+        sorted.sort(compare_func);
+        
+        return new Iterable<G>(sorted.iterator());
+    }
+    
     public Iterable<A> map<A>(Gee.MapFunc<A, G> f) {
         return new Iterable<A>(i.map<A>(f));
     }
diff --git a/src/component/component-instance.vala b/src/component/component-instance.vala
index 0a12044..2bd0f30 100644
--- a/src/component/component-instance.vala
+++ b/src/component/component-instance.vala
@@ -39,6 +39,7 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
     public const string PROP_RDATES = "rdates";
     public const string PROP_SEQUENCE = "sequence";
     public const string PROP_MASTER = "master";
+    public const string PROP_ORGANIZERS = "organizers";
     public const string PROP_ATTENDEES = "attendees";
     
     protected const string PROP_IN_FULL_UPDATE = "in-full-update";
@@ -172,6 +173,19 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
     public int sequence { get; set; default = 0; }
     
     /**
+     * ORGANIZERs of a VEVENT, VTODO, or VJOURNAL.
+     *
+     * To add or remove organizers, use { link add_organizers}, { link remove_organizers}, and
+     * { link clear_organizers}. Because those methods always update the property itself and not
+     * merely modify the list, the property can be watched for changes with the "notify" and/or
+     * "altered" signals.
+     *
+     * See [[https://tools.ietf.org/html/rfc5545#section-3.8.4.3]]  In particular, note that the
+     * { link organizer} must be specified in group-scheduled calendar entity.
+     */
+    public Gee.Set<Person> organizers { get; private set; default = new Gee.HashSet<Person>(); }
+    
+    /**
      * ATTENDEEs for a VEVENT, VTODO, or VJOURNAL.
      *
      * To add or remove attendees, use { link add_attendees}, { link remove_attendees}, and
@@ -179,8 +193,6 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
      * merely modify the list, the property can be watched for changes with the "notify" and/or
      * "altered" signals.
      *
-     * No validity checking is performed on attendee mailto's.
-     *
      * See [[https://tools.ietf.org/html/rfc5545#section-3.8.4.1]]
      */
     public Gee.Set<Person> attendees { get; private set; default = new Gee.HashSet<Person>(); }
@@ -367,24 +379,30 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
         exdates = get_multiple_date_times(iCal.icalproperty_kind.EXDATE_PROPERTY);
         rdates = get_multiple_date_times(iCal.icalproperty_kind.RDATE_PROPERTY);
         
-        // Currently only storing mailto's for attendees for now
-        unowned iCal.icalproperty? attendee_prop = ical_component.get_first_property(
-            iCal.icalproperty_kind.ATTENDEE_PROPERTY);
-        while (attendee_prop != null) {
-            try {
-                attendees.add(new Person.from_property(attendee_prop));
-            } catch (ComponentError comperr) {
-                debug("Unable to parse ATTENDEE for %s: %s", to_string(), comperr.message);
-            }
-            
-            attendee_prop = ical_component.get_next_property(iCal.icalproperty_kind.ATTENDEE_PROPERTY);
-        }
+        persons_from_component(ical_component, organizers, iCal.icalproperty_kind.ORGANIZER_PROPERTY);
+        persons_from_component(ical_component, attendees, iCal.icalproperty_kind.ATTENDEE_PROPERTY);
         
         // save own copy of component; no ownership transferrance w/ current bindings
         if (_ical_component != ical_component)
             _ical_component = ical_component.clone();
     }
     
+    private void persons_from_component(iCal.icalcomponent ical_component, Gee.Set<Person> persons,
+        iCal.icalproperty_kind kind) {
+        persons.clear();
+        
+        unowned iCal.icalproperty? prop = ical_component.get_first_property(kind);
+        while (prop != null) {
+            try {
+                persons.add(new Person.from_property(prop));
+            } catch (Error err) {
+                debug("Unable to parse %s for %s: %s", kind.to_string(), to_string(), err.message);
+            }
+            
+            prop = ical_component.get_next_property(kind);
+        }
+    }
+    
     private void on_notify(ParamSpec pspec) {
         // don't worry if in full update, that call is supposed to update properties
         if (in_full_update)
@@ -426,6 +444,12 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
                     set_multiple_date_times(iCal.icalproperty_kind.RDATE_PROPERTY, rdates);
             break;
             
+            case PROP_ORGANIZERS:
+                remove_all_properties(iCal.icalproperty_kind.ORGANIZER_PROPERTY);
+                foreach (Person organizer in organizers)
+                    ical_component.add_property(organizer.as_ical_property());
+            break;
+            
             case PROP_ATTENDEES:
                 remove_all_properties(iCal.icalproperty_kind.ATTENDEE_PROPERTY);
                 foreach (Person attendee in attendees)
@@ -483,27 +507,38 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
     }
     
     /**
-     * Add one or more mailto: URIs as { link attendees}.
-     *
-     * No URI-format checking is performed here; it is up to the caller to convert and deal with
-     * formatting issues.  Also note that duplicates are allowed in the attendees list.
+     * Add one or more { link Person}s as { link organizers}.
+     */
+    public void add_organizers(Gee.Collection<Person> to_add) {
+        organizers = add_persons(organizers, to_add);
+    }
+    
+    /**
+     * Remove one or more { link Person}s as (@link organizers}.
+     */
+    public void remove_organizers(Gee.Collection<Person> to_remove) {
+        organizers = remove_persons(organizers, to_remove);
+    }
+    
+    /*
+     * Removes all { link organizers}.
+     */
+    public void clear_organizers() {
+        organizers = new Gee.HashSet<Person>();
+    }
+    
+    /**
+     * Add one or more { link Person}s as { link attendees}.
      */
     public void add_attendees(Gee.Collection<Person> to_add) {
-        Gee.Set<Person> copy = traverse<Person>(attendees).to_hash_set();
-        copy.add_all(to_add);
-        
-        attendees = copy;
+        attendees = add_persons(attendees, to_add);
     }
     
     /**
-     * Remove one or more mailto: URIs as (@link attendees}.
-     *
-     * See { link add_attendees} for notes about data validity and checking.
+     * Remove one or more { link Person}s as (@link attendees}.
      */
     public void remove_attendees(Gee.Collection<Person> to_remove) {
-        attendees = traverse<Person>(attendees)
-            .filter(attendee => !to_remove.contains(attendee))
-            .to_hash_set();
+        attendees = remove_persons(attendees, to_remove);
     }
     
     /*
@@ -513,6 +548,19 @@ public abstract class Instance : BaseObject, Gee.Hashable<Instance> {
         attendees = new Gee.HashSet<Person>();
     }
     
+    private Gee.Set<Person> add_persons(Gee.Set<Person> existing, Gee.Collection<Person> to_add) {
+        Gee.Set<Person> copy = traverse<Person>(attendees).to_hash_set();
+        copy.add_all(to_add);
+        
+        return copy;
+    }
+    
+    private Gee.Set<Person> remove_persons(Gee.Set<Person> existing, Gee.Collection<Person> to_remove) {
+        return traverse<Person>(existing)
+            .filter(person => !to_remove.contains(person))
+            .to_hash_set();
+    }
+    
     /**
      * Returns a single comma-delimited string for all attendees.
      *
diff --git a/src/component/component-person.vala b/src/component/component-person.vala
index dce480c..bf66cde 100644
--- a/src/component/component-person.vala
+++ b/src/component/component-person.vala
@@ -21,7 +21,7 @@ namespace California.Component {
  * [[https://tools.ietf.org/html/rfc5545#section-3.2.2]], and more.
  */
 
-public class Person : BaseObject, Gee.Hashable<Person> {
+public class Person : BaseObject, Gee.Hashable<Person>, Gee.Comparable<Person> {
     /**
      * The mailto: of the { link Person}, the only required value for the property.
      */
@@ -67,6 +67,10 @@ public class Person : BaseObject, Gee.Hashable<Person> {
         this.mailto = mailto;
         this.common_name = common_name;
         full_mailbox = make_full_address(mailto, common_name);
+        
+        // store in parameters in case object is serialized as an iCal property.
+        if (!String.is_empty(common_name))
+            parameters.add(new iCal.icalparameter.cn(common_name).as_ical_string());
     }
     
     internal Person.from_property(iCal.icalproperty prop) throws Error {
@@ -139,6 +143,20 @@ public class Person : BaseObject, Gee.Hashable<Person> {
         return (this != other) ? mailto.equal(other.mailto) : true;
     }
     
+    public int compare_to(Person other) {
+        if (this == other)
+            return 0;
+        
+        // if a common name is supplied, use that first, but need to stabilize sort
+        if (!String.is_empty(common_name) && !String.is_empty(other.common_name)) {
+            int compare = String.stricmp(common_name, other.common_name);
+            if (compare != 0)
+                return compare;
+        }
+        
+        return String.stricmp(mailbox, other.mailbox);
+    }
+    
     public override string to_string() {
         return mailto_text;
     }
diff --git a/src/host/host-show-event.vala b/src/host/host-show-event.vala
index 4c985b4..d19931a 100644
--- a/src/host/host-show-event.vala
+++ b/src/host/host-show-event.vala
@@ -45,10 +45,16 @@ public class ShowEvent : Gtk.Grid, Toolkit.Card {
     private Gtk.Label where_text;
     
     [GtkChild]
-    private Gtk.Label who_label;
+    private Gtk.Label organizers_label;
     
     [GtkChild]
-    private Gtk.Label who_text;
+    private Gtk.Label organizers_text;
+    
+    [GtkChild]
+    private Gtk.Label attendees_label;
+    
+    [GtkChild]
+    private Gtk.Label attendees_text;
     
     [GtkChild]
     private Gtk.Label calendar_label;
@@ -133,7 +139,8 @@ public class ShowEvent : Gtk.Grid, Toolkit.Card {
         summary_text.draw.connect(on_label_drawn);
         when_text.draw.connect(on_label_drawn);
         where_text.draw.connect(on_label_drawn);
-        who_text.draw.connect(on_label_drawn);
+        attendees_text.draw.connect(on_label_drawn);
+        organizers_text.draw.connect(on_label_drawn);
     }
     
     ~ShowEvent() {
@@ -170,8 +177,23 @@ public class ShowEvent : Gtk.Grid, Toolkit.Card {
         set_label(when_label, when_text, event.get_event_time_pretty_string(Calendar.Date.PrettyFlag.NONE,
             Calendar.ExactTimeSpan.PrettyFlag.NONE, Calendar.Timezone.local));
         
-        // attendees
-        set_label(who_label, who_text, 
traverse<Component.Person>(event.attendees).to_string(stringify_attendee));
+        // organizers and attendees
+        // remove organizers from attendee list
+        Gee.Set<Component.Person> attendee_list = traverse<Component.Person>(event.attendees)
+            .filter(person => !event.organizers.contains(person))
+            .to_hash_set();
+        
+        // convert to sorted LF-delimited strings
+        string organizers = traverse<Component.Person>(event.organizers)
+            .sort()
+            .to_string(stringify_person) ?? "";
+        
+        string attendees = traverse<Component.Person>(attendee_list)
+            .sort()
+            .to_string(stringify_person) ?? "";
+        
+        set_label(organizers_label, organizers_text, organizers);
+        set_label(attendees_label, attendees_text, attendees);
         
         // calendar
         set_label(calendar_label, calendar_text, event.calendar_source != null ? event.calendar_source.title 
: null);
@@ -215,8 +237,8 @@ public class ShowEvent : Gtk.Grid, Toolkit.Card {
         return true;
     }
     
-    private string? stringify_attendee(Component.Person attendee, bool is_first, bool is_last) {
-        return is_first ? attendee.mailbox : "%s%s".printf(EMAIL_SEPARATOR, attendee.full_mailbox);
+    private string? stringify_person(Component.Person person, bool is_first, bool is_last) {
+        return "%s%s".printf(person.full_mailbox, is_last ? "" : "\n");
     }
     
     // Note that text is not escaped, up to caller to determine if necessary or not.
diff --git a/src/rc/show-event.ui b/src/rc/show-event.ui
index 24b2063..310627e 100644
--- a/src/rc/show-event.ui
+++ b/src/rc/show-event.ui
@@ -134,6 +134,37 @@
           </packing>
         </child>
         <child>
+          <object class="GtkLabel" id="organizers_label">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="xalign">1</property>
+            <property name="yalign">0</property>
+            <property name="label" translatable="yes">Organizers</property>
+            <style>
+              <class name="dim-label"/>
+            </style>
+          </object>
+          <packing>
+            <property name="left_attach">0</property>
+            <property name="top_attach">2</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="organizers_text">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="xalign">0</property>
+            <property name="yalign">0</property>
+            <property name="label">(empty)</property>
+            <property name="selectable">True</property>
+            <property name="ellipsize">end</property>
+          </object>
+          <packing>
+            <property name="left_attach">1</property>
+            <property name="top_attach">2</property>
+          </packing>
+        </child>
+        <child>
           <object class="GtkLabel" id="calendar_label">
             <property name="visible">True</property>
             <property name="can_focus">False</property>
@@ -145,7 +176,7 @@
           </object>
           <packing>
             <property name="left_attach">0</property>
-            <property name="top_attach">3</property>
+            <property name="top_attach">4</property>
           </packing>
         </child>
         <child>
@@ -159,39 +190,37 @@
           </object>
           <packing>
             <property name="left_attach">1</property>
-            <property name="top_attach">3</property>
+            <property name="top_attach">4</property>
           </packing>
         </child>
         <child>
-          <object class="GtkLabel" id="who_label">
+          <object class="GtkLabel" id="attendees_label">
             <property name="visible">True</property>
             <property name="can_focus">False</property>
             <property name="xalign">1</property>
             <property name="yalign">0</property>
-            <property name="label" translatable="yes" comments="Label for presenting the list of attendees 
to an event.">Who</property>
+            <property name="label" translatable="yes">Attendees</property>
             <style>
               <class name="dim-label"/>
             </style>
           </object>
           <packing>
             <property name="left_attach">0</property>
-            <property name="top_attach">2</property>
+            <property name="top_attach">3</property>
           </packing>
         </child>
         <child>
-          <object class="GtkLabel" id="who_text">
+          <object class="GtkLabel" id="attendees_text">
             <property name="visible">True</property>
             <property name="can_focus">False</property>
             <property name="xalign">0</property>
+            <property name="yalign">0</property>
             <property name="label">(empty)</property>
             <property name="selectable">True</property>
-            <property name="ellipsize">end</property>
-            <property name="single_line_mode">True</property>
-            <property name="max_width_chars">60</property>
           </object>
           <packing>
             <property name="left_attach">1</property>
-            <property name="top_attach">2</property>
+            <property name="top_attach">3</property>
           </packing>
         </child>
       </object>


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