Page MenuHomePhabricator

Give calendar events real invitee lists
Closed, ResolvedPublic

Description

Calendar was originally "Status", and let you list things like vacation/sick days. Consequently, it didn't support invitees.

It has been partially transitioned toward having real events with invitees. This transition should be completed.

Event Timeline

epriestley assigned this task to lpriestley.
epriestley raised the priority of this task from to Normal.
epriestley updated the task description. (Show Details)
epriestley added a project: Calendar.
epriestley added a subscriber: epriestley.

I think invitees are complex enough that we probably don't want to use edges to store them. Edges are good for simple relationships (like a user subscribing to an object) but not as good for more complex relationships. Invitees are likely to have a lot of state eventually (required/optional, invited/attending/declined, maybe annotations like "Speaker" for a talk, "Co-Host" for a party, or "Bringing Salad" vs "Bringing Desert" for a potluck, maybe tracking who invited you, and so on).

We can use a custom table to store a complex relationship like this, but we'll have to do more work for it since we get less stuff "for free".

Add Storage: Create a new PhabricatorCalendarEventInvitee class in calendar/storage/ and add a .sql patch to create it. Minimum fields are probably:

  • eventPHID
  • inviteePHID
  • status (invited, accepted, declined) -- probably VARCHAR(64) in the .sql file and text64 in the schema column spec.
  • dateCreated
  • dateModified
  • Add a unique key, key_event, on (eventPHID, inviteePHID) in the schema spec. This will let us find invitees for an event efficiently, and enforce that one user can't be invited to an event twice.
  • Add a nonunique key, key_invitee, on (inviteePHID) in the schema spec. This will let us find events a specific user is invited to efficiently.

If you think any of the other stuff is good and you want to implement it relatively soon, you can add fields for those now. Otherwise, you can wait until later:

  • role (or similar? For "Speaker" vs "Bringing Salad".)
  • invitedByPHID (for holding who invited you)
  • isRequired (or similar? For required/optional? But maybe we could just use "Role" for this?)

Those are just the first three ideas I came up with so we can ignore them for now if they aren't resonating with you as the greatest features in the world, and figure out if they make sense or not later on.

Write a Query Class: Write a PhabricatorCalendarEventInviteeQuery class which extends PhabricatorCursorPagedPolicyAwareQuery. For now, I think this just needs withEventPHIDs($phids) and withInviteePHIDs($phids). You'll have to implement PhabricatorPolicyInterface to support this. You should be able to use an implementation similar to the existing CalendarEvent implementation. You'll be able to test this class in the next step.

Write Stuff to the Database: Next, we want to start writing an invitee row when you create an event. This will invite you to the event.

Define a new transaction type, like PhabricatorCalendarEventTransaction::TYPE_INVITE, and add a transaction for it to the $xactions list in the EditController if a new event is being created.

The "new value" for these transactions will be a little bit complex. A structure like this might work well:

$new = array(
  $user_phid => PhabricatorCalendarEventInvitee::STATUS_ACCEPTED,
);

Meaning "set this user's invitation status to 'accepted', regardless of what their old status was". You'll pass this to setNewValue(...) on the $xaction in the EditController. We're using an array so we can invite a lot of people at once later on.

Inside the Editor, you'll build a similar structure by querying the database:

  • Get all the PHIDs which are changing status by examining the array_keys() of the new transaction value.
  • Use your new Query class to load the current status for those invitees.
  • For any invitees with no current status, use some status like STATUS_NOTINVITED.
  • Generate a map from each PHID to the old value:
$old = array(
  $user_phid => PhabricatorCalendarEventInvitee::STATUS_NOTINVITED,
);

So now the transaction means "this user wasn't invited before, but should have an invite with an accepted status after the transaction applies".

In applyApplicationTransactionInternalEffects(), actually apply the transaction:

  • Load the PhabricatorCalendarEventInvitee objects for any invitees you're updating.
  • For any users without an invitee object, create a new one.
  • Set all the invitee objects to the correct invited/accepted/rejected status.
  • Save the invitee objects.

When you create a new event, this should now write a row into the phabricator_calendar.calendar_eventinvitee row linking you (the event creator) to the event.

Load Invitees: Add a private $invitees = self::ATTACHABLE; property to CalendarEvent. We're going to load invitees unconditionally (every time we load an event) because we'll need them in order to do policy checks later (e.g., you can always see an event if you're invited to it). Add getInvitees() / attachInvitees() methods.

Add (or expand, if it already exitsts) willFilterPage() on CalendarEventQuery:

  • Extract all the event PHIDs for the page of events.
  • Load their invitees using InviteeQuery.
  • Group them by event (with $invitees = mgroup($invitees, 'getEventPHID');.
  • Iterate through the events and attach their invitees.

Show Invitees: On the detail page, use PHUIStatusListView to show invitees and their status. This is the element that Differential uses to show reviewers. We can refine this later.

Get through that first, then we can figure out how the UI should work for inviting people and RSVP'ing to an event.