From fa9af99f04e8fbd5d8d74d6817edec51622b1e97 Mon Sep 17 00:00:00 2001
From: Owen Bush <ojb@ukhhf.co.uk>
Date: Sat, 2 Mar 2019 09:59:17 -0700
Subject: [PATCH] Added eventinstance entity type.

---
 src/Entity/EventInstance.php                  | 380 ++++++++++++++++++
 src/Entity/EventSeries.php                    |  39 +-
 src/EventInstanceAccessControlHandler.php     |  47 +++
 ...SeriesInterface.php => EventInterface.php} |   4 +-
 4 files changed, 448 insertions(+), 22 deletions(-)
 create mode 100644 src/Entity/EventInstance.php
 create mode 100644 src/EventInstanceAccessControlHandler.php
 rename src/{EventSeriesInterface.php => EventInterface.php} (88%)

diff --git a/src/Entity/EventInstance.php b/src/Entity/EventInstance.php
new file mode 100644
index 00000000..9a8c4451
--- /dev/null
+++ b/src/Entity/EventInstance.php
@@ -0,0 +1,380 @@
+<?php
+
+namespace Drupal\recurring_events\Entity;
+
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\Core\Entity\EditorialContentEntityBase;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\recurring_events\EventInterface;
+use Drupal\user\UserInterface;
+
+/**
+ * Defines the Event Instance entity.
+ *
+ * @ingroup recurring events
+ *
+ * This is the main definition of the entity type. From it, an entityType is
+ * derived. The most important properties in this example are listed below.
+ *
+ * id: The unique identifier of this entityType. It follows the pattern
+ * 'moduleName_xyz' to avoid naming conflicts.
+ *
+ * label: Human readable name of the entity type.
+ *
+ * handlers: Handler classes are used for different tasks. You can use
+ * standard handlers provided by D8 or build your own, most probably derived
+ * from the standard class. In detail:
+ *
+ * - view_builder: we use the standard controller to view an instance. It is
+ *   called when a route lists an '_entity_view' default for the entityType
+ *   (see routing.yml for details. The view can be manipulated by using the
+ *   standard drupal tools in the settings.
+ *
+ * - list_builder: We derive our own list builder class from the
+ *   entityListBuilder to control the presentation.
+ *   If there is a view available for this entity from the views module, it
+ *   overrides the list builder. @todo: any view? naming convention?
+ *
+ * - form: We derive our own forms to add functionality like additional fields,
+ *   redirects etc. These forms are called when the routing list an
+ *   '_entity_form' default for the entityType. Depending on the suffix
+ *   (.add/.edit/.delete) in the route, the correct form is called.
+ *
+ * - access: Our own accessController where we determine access rights based on
+ *   permissions.
+ *
+ * More properties:
+ *
+ *  - base_table: Define the name of the table used to store the data. Make sure
+ *    it is unique. The schema is automatically determined from the
+ *    BaseFieldDefinitions below. The table is automatically created during
+ *    installation.
+ *
+ *  - fieldable: Can additional fields be added to the entity via the GUI?
+ *    Analog to content types.
+ *
+ *  - entity_keys: How to access the fields. Analog to 'nid' or 'uid'.
+ *
+ *  - links: Provide links to do standard tasks. The 'edit-form' and
+ *    'delete-form' links are added to the list built by the
+ *    entityListController. They will show up as action buttons in an additional
+ *    column.
+ *
+ * There are many more properties to be used in an entity type definition. For
+ * a complete overview, please refer to the '\Drupal\Core\Entity\EntityType'
+ * class definition.
+ *
+ * The following construct is the actual definition of the entity type which
+ * is read and cached. Don't forget to clear cache after changes.
+ *
+ * @ContentEntityType(
+ *   id = "eventinstance",
+ *   label = @Translation("Event Instance entity"),
+ *   handlers = {
+ *     "storage" = "Drupal\Core\Entity\Sql\SqlContentEntityStorage",
+ *     "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
+ *     "views_data" = "Drupal\views\EntityViewsData",
+ *     "form" = {
+ *       "edit" = "Drupal\recurring_events\Form\EventInstanceForm",
+ *       "delete" = "Drupal\recurring_events\Form\EventInstanceDeleteForm",
+ *       "default" = "Drupal\recurring_events\Form\EventInstanceForm",
+ *       "clone" = "Drupal\recurring_events\Form\EventInstanceCloneForm",
+ *       "contact" = "Drupal\recurring_events\Form\EventInstanceContactRegistrationsForm",
+ *     },
+ *     "access" = "Drupal\recurring_events\EventInstanceAccessControlHandler",
+ *   },
+ *   base_table = "eventinstance",
+ *   data_table = "eventinstance_field_data",
+ *   revision_table = "eventinstance_revision",
+ *   revision_data_table = "eventinstance_field_revision",
+ *   show_revision_ui = TRUE,
+ *   translatable = TRUE,
+ *   admin_permission = "administer eventinstance entity",
+ *   fieldable = TRUE,
+ *   entity_keys = {
+ *     "id" = "id",
+ *     "revision" = "vid",
+ *     "published" = "status",
+ *     "langcode" = "langcode",
+ *     "uuid" = "uuid"
+ *   },
+ *   revision_metadata_keys = {
+ *     "revision_user" = "revision_uid",
+ *     "revision_created" = "revision_timestamp",
+ *     "revision_log_message" = "revision_log"
+ *   },
+ *   links = {
+ *     "canonical" = "/events/{eventinstance}",
+ *     "edit-form" = "/events/{eventinstance}/edit",
+ *     "delete-form" = "/events/{eventinstance}/delete",
+ *     "collection" = "/admin/content/events/instances",
+ *     "version-history" = "/events/{eventinstance}/revisions",
+ *     "revision" = "/events/{eventinstance}/revisions/{eventinstance_revision}/view",
+ *     "clone-form" = "/events/{eventinstance}/clone",
+ *     "contact-form" = "/events/{eventinstance}/registration/contact",
+ *   },
+ *   field_ui_base_route = "eventinstance.settings",
+ * )
+ *
+ * The 'links' above are defined by their path. For core to find the
+ * corresponding route, the route name must follow the correct pattern:
+ *
+ * entity.<entity-name>.<link-name> (replace dashes with underscores)
+ * Example: 'entity.eventinstance.canonical'
+ *
+ * See routing file above for the corresponding implementation
+ *
+ * The 'EventInstance' class defines methods and fields for the eventinstance
+ * entity.
+ *
+ * Being derived from the ContentEntityBase class, we can override the methods
+ * we want. In our case we want to provide access to the standard fields about
+ * creation and changed time stamps.
+ *
+ * Our interface (see EventSeriesInterface) also exposes the
+ * EntityOwnerInterface. This allows us to provide methods for setting
+ * and providing ownership information.
+ *
+ * The most important part is the definitions of the field properties for this
+ * entity type. These are of the same type as fields added through the GUI, but
+ * they can by changed in code. In the definition we can define if the user with
+ * the rights privileges can influence the presentation (view, edit) of each
+ * field.
+ */
+class EventInstance extends EditorialContentEntityBase implements EventInterface {
+
+  /**
+   * {@inheritdoc}
+   *
+   * When a new entity instance is added, set the user_id entity reference to
+   * the current user as the creator of the instance.
+   */
+  public static function preCreate(EntityStorageInterface $storage_controller, array &$values) {
+    parent::preCreate($storage_controller, $values);
+    $values += [
+      'uid' => \Drupal::currentUser()->id(),
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preSave(EntityStorageInterface $storage) {
+    parent::preSave($storage);
+
+    foreach (array_keys($this->getTranslationLanguages()) as $langcode) {
+      $translation = $this->getTranslation($langcode);
+
+      // If no owner has been set explicitly, make the anonymous user the owner.
+      if (!$translation->getOwner()) {
+        $translation->setOwnerId(0);
+      }
+    }
+
+    // If no revision author has been set explicitly, make the node owner the
+    // revision author.
+    if (!$this->getRevisionUser()) {
+      $this->setRevisionUserId($this->getOwnerId());
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preSaveRevision(EntityStorageInterface $storage, \stdClass $record) {
+    parent::preSaveRevision($storage, $record);
+
+    if (!$this->isNewRevision() && isset($this->original) && (!isset($record->revision_log) || $record->revision_log === '')) {
+      // If we are updating an existing eventinstance without adding a new
+      // revision, we need to make sure $entity->revision_log is reset whenever
+      // it is empty. Therefore, this code allows us to avoid clobbering an
+      // existing log entry with an empty one.
+      $record->revision_log = $this->original->revision_log->value;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRevisionAuthor() {
+    return $this->getRevisionUser();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setRevisionAuthorId($uid) {
+    $this->setRevisionUserId($uid);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCreatedTime() {
+    return $this->get('created')->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getChangedTime() {
+    return $this->get('changed')->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setChangedTime($timestamp) {
+    $this->set('changed', $timestamp);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getChangedTimeAcrossTranslations() {
+    $changed = $this->getUntranslated()->getChangedTime();
+    foreach ($this->getTranslationLanguages(FALSE) as $language) {
+      $translation_changed = $this->getTranslation($language->getId())
+        ->getChangedTime();
+      $changed = max($translation_changed, $changed);
+    }
+    return $changed;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getOwner() {
+    return $this->get('uid')->entity;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getOwnerId() {
+    return $this->get('uid')->target_id;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setOwnerId($uid) {
+    $this->set('uid', $uid);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setOwner(UserInterface $account) {
+    $this->setOwnerId($account->id());
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * Define the field properties here.
+   *
+   * Field name, type and size determine the table structure.
+   *
+   * In addition, we can define how the field and its content can be manipulated
+   * in the GUI. The behaviour of the widgets used can be determined here.
+   */
+  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
+    $fields = parent::baseFieldDefinitions($entity_type);
+
+    // Standard field, used as unique if primary index.
+    $fields['id'] = BaseFieldDefinition::create('integer')
+      ->setLabel(t('ID'))
+      ->setDescription(t('The ID of the event entity.'))
+      ->setReadOnly(TRUE);
+
+    $fields['uid'] = BaseFieldDefinition::create('entity_reference')
+      ->setLabel(t('Authored by'))
+      ->setDescription(t('The username of the content author.'))
+      ->setRevisionable(TRUE)
+      ->setSetting('target_type', 'user')
+      ->setDefaultValueCallback('Drupal\recurring_events\Entity\EventInstance::getCurrentUserId')
+      ->setTranslatable(TRUE)
+      ->setDisplayOptions('form', [
+        'type' => 'entity_reference_autocomplete',
+        'weight' => 5,
+        'settings' => [
+          'match_operator' => 'CONTAINS',
+          'size' => '60',
+          'placeholder' => '',
+        ],
+      ])
+      ->setDisplayConfigurable('form', TRUE);
+
+    // Standard field, unique outside of the scope of the current project.
+    $fields['uuid'] = BaseFieldDefinition::create('uuid')
+      ->setLabel(t('UUID'))
+      ->setDescription(t('The UUID of the event entity.'))
+      ->setReadOnly(TRUE);
+
+    $fields['body'] = BaseFieldDefinition::create('text_long')
+      ->setLabel(t('Event Description'))
+      ->setTranslatable(TRUE)
+      ->setRevisionable(TRUE)
+      ->setRequired(TRUE)
+      ->setDisplayConfigurable('view', TRUE)
+      ->setDisplayOptions('form', [
+        'type' => 'text_textarea',
+        'weight' => -4,
+      ])
+      ->setDisplayConfigurable('form', TRUE);
+
+    $fields['date'] = BaseFieldDefinition::create('daterange')
+      ->setLabel(t('Event Description'))
+      ->setTranslatable(FALSE)
+      ->setRevisionable(TRUE)
+      ->setRequired(TRUE)
+      ->setDisplayOptions('view', array(
+        'label' => 'above',
+        'type' => 'string',
+        'weight' => -4,
+      ))
+      ->setDisplayOptions('form', array(
+        'type' => 'daterange_default',
+        'weight' => -4,
+      ))
+      ->setDisplayConfigurable('form', TRUE)
+      ->setDisplayConfigurable('view', TRUE);
+
+    $fields['eventseries_id'] = BaseFieldDefinition::create('entity_reference')
+      ->setLabel(t('Event Series ID'))
+      ->setDescription(t('The ID of the event series entity.'))
+      ->setSetting('target_type', 'eventseries');
+
+    $fields['langcode'] = BaseFieldDefinition::create('language')
+      ->setLabel(t('Language code'))
+      ->setDescription(t('The language code of event entity.'));
+
+    $fields['created'] = BaseFieldDefinition::create('created')
+      ->setLabel(t('Created'))
+      ->setDescription(t('The time that the entity was created.'));
+
+    $fields['changed'] = BaseFieldDefinition::create('changed')
+      ->setLabel(t('Changed'))
+      ->setRevisionable(TRUE)
+      ->setDescription(t('The time that the entity was last edited.'));
+
+    $fields['status']
+      ->setDisplayOptions('form', [
+        'type' => 'boolean_checkbox',
+        'settings' => [
+          'display_label' => TRUE,
+        ],
+        'weight' => 120,
+      ])
+      ->setDisplayConfigurable('form', TRUE);
+
+    return $fields;
+  }
+
+}
diff --git a/src/Entity/EventSeries.php b/src/Entity/EventSeries.php
index 1ece4c63..09e63a8e 100644
--- a/src/Entity/EventSeries.php
+++ b/src/Entity/EventSeries.php
@@ -6,7 +6,7 @@ use Drupal\Core\Entity\EntityStorageInterface;
 use Drupal\Core\Field\BaseFieldDefinition;
 use Drupal\Core\Entity\EditorialContentEntityBase;
 use Drupal\Core\Entity\EntityTypeInterface;
-use Drupal\recurring_events\EventSeriesInterface;
+use Drupal\recurring_events\EventInterface;
 use Drupal\user\UserInterface;
 
 /**
@@ -105,13 +105,13 @@ use Drupal\user\UserInterface;
  *     "revision_log_message" = "revision_log"
  *   },
  *   links = {
- *     "canonical" = "/events/series/{event}",
- *     "edit-form" = "/events/series/{event}/edit",
- *     "delete-form" = "/events/series/{event}/delete",
+ *     "canonical" = "/events/series/{eventseries}",
+ *     "edit-form" = "/events/series/{eventseries}/edit",
+ *     "delete-form" = "/events/series/{eventseries}/delete",
  *     "collection" = "/admin/content/events/series",
- *     "version-history" = "/events/series/{event}/revisions",
- *     "revision" = "/events/series/{event}/revisions/{event_revision}/view",
- *     "clone-form" = "/events/series/{event}/clone",
+ *     "version-history" = "/events/series/{eventseries}/revisions",
+ *     "revision" = "/events/series/{eventseries}/revisions/{eventseries_revision}/view",
+ *     "clone-form" = "/events/series/{eventseries}/clone",
  *   },
  *   field_ui_base_route = "eventseries.settings",
  * )
@@ -140,18 +140,18 @@ use Drupal\user\UserInterface;
  * the rights privileges can influence the presentation (view, edit) of each
  * field.
  */
-class EventSeries extends EditorialContentEntityBase implements EventSeriesInterface {
+class EventSeries extends EditorialContentEntityBase implements EventInterface {
 
   /**
    * {@inheritdoc}
    *
-   * When a new entity instance is added, set the user_id entity reference to
+   * When a new entity instance is added, set the uid entity reference to
    * the current user as the creator of the instance.
    */
   public static function preCreate(EntityStorageInterface $storage_controller, array &$values) {
     parent::preCreate($storage_controller, $values);
     $values += [
-      'user_id' => \Drupal::currentUser()->id(),
+      'uid' => \Drupal::currentUser()->id(),
     ];
   }
 
@@ -246,21 +246,21 @@ class EventSeries extends EditorialContentEntityBase implements EventSeriesInter
    * {@inheritdoc}
    */
   public function getOwner() {
-    return $this->get('user_id')->entity;
+    return $this->get('uid')->entity;
   }
 
   /**
    * {@inheritdoc}
    */
   public function getOwnerId() {
-    return $this->get('user_id')->target_id;
+    return $this->get('uid')->target_id;
   }
 
   /**
    * {@inheritdoc}
    */
   public function setOwnerId($uid) {
-    $this->set('user_id', $uid);
+    $this->set('uid', $uid);
     return $this;
   }
 
@@ -308,7 +308,6 @@ class EventSeries extends EditorialContentEntityBase implements EventSeriesInter
       ])
       ->setDisplayConfigurable('form', TRUE);
 
-    // Standard field, unique outside of the scope of the current project.
     $fields['uuid'] = BaseFieldDefinition::create('uuid')
       ->setLabel(t('UUID'))
       ->setDescription(t('The UUID of the event entity.'))
@@ -333,15 +332,15 @@ class EventSeries extends EditorialContentEntityBase implements EventSeriesInter
       ->setTranslatable(TRUE)
       ->setRequired(TRUE);
 
-    $fields['body'] = BaseFieldDefinition::create('text_with_summary')
-      ->setLabel(t('Description of the event.'))
+    $fields['body'] = BaseFieldDefinition::create('text_long')
+      ->setLabel(t('Event Description'))
       ->setTranslatable(TRUE)
       ->setRevisionable(TRUE)
       ->setRequired(TRUE)
       ->setDisplayConfigurable('view', TRUE)
       ->setDisplayOptions('form', [
-        'type' => 'text_textarea_with_summary',
-        'weight' => -5,
+        'type' => 'text_textarea',
+        'weight' => -4,
       ])
       ->setDisplayConfigurable('form', TRUE);
 
@@ -414,11 +413,11 @@ class EventSeries extends EditorialContentEntityBase implements EventSeriesInter
         'weight' => 4,
       ]);
 
-    $fields['instances'] = BaseFieldDefinition::create('entity_reference')
+    $fields['event_instances'] = BaseFieldDefinition::create('entity_reference')
       ->setLabel(t('Events in this Series'))
       ->setDescription(t('The events in this series.'))
       ->setRevisionable(FALSE)
-      ->setSetting('target_type', 'node')
+      ->setSetting('target_type', 'eventinstance')
       ->setTranslatable(FALSE)
       ->setDisplayOptions('view', [
         'label' => 'above',
diff --git a/src/EventInstanceAccessControlHandler.php b/src/EventInstanceAccessControlHandler.php
new file mode 100644
index 00000000..23f21b56
--- /dev/null
+++ b/src/EventInstanceAccessControlHandler.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace Drupal\recurring_events;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Entity\EntityAccessControlHandler;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Access controller for the event instance entity.
+ *
+ * @see \Drupal\recurring_events\Entity\EventInstance
+ */
+class EventInstanceAccessControlHandler extends EntityAccessControlHandler {
+
+  /**
+   * {@inheritdoc}
+   *
+   * Link the activities to the permissions. checkAccess is called with the
+   * $operation as defined in the routing.yml file.
+   */
+  protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
+    switch ($operation) {
+      case 'view':
+        $status = $entity->isPublished();
+        if (!$status) {
+          return AccessResult::allowedIfHasPermission($account, 'view unpublished eventinstance entity');
+        }
+        return AccessResult::allowedIfHasPermission($account, 'view eventinstance entity');
+
+      case 'edit':
+        return AccessResult::allowedIfHasPermission($account, 'edit eventinstance entity');
+
+      case 'delete':
+        return AccessResult::allowedIfHasPermission($account, 'delete eventinstance entity');
+
+      case 'clone':
+        return AccessResult::allowedIfHasPermission($account, 'clone eventinstance entity');
+
+      case 'contact':
+        return AccessResult::allowedIfHasPermission($account, 'contact eventinstance registration entities');
+    }
+    return AccessResult::allowed();
+  }
+
+}
diff --git a/src/EventSeriesInterface.php b/src/EventInterface.php
similarity index 88%
rename from src/EventSeriesInterface.php
rename to src/EventInterface.php
index cf57f9ce..6644e0c5 100644
--- a/src/EventSeriesInterface.php
+++ b/src/EventInterface.php
@@ -9,9 +9,9 @@ use Drupal\Core\Entity\EntityChangedInterface;
 use Drupal\Core\Entity\ContentEntityInterface;
 
 /**
- * Provides an interface defining an EventSeries entity.
+ * Provides an interface defining an EventSeries and EventInstance entities.
  */
-interface EventSeriesInterface extends ContentEntityInterface, EntityChangedInterface, EntityOwnerInterface, RevisionLogInterface, EntityPublishedInterface {
+interface EventInterface extends ContentEntityInterface, EntityChangedInterface, EntityOwnerInterface, RevisionLogInterface, EntityPublishedInterface {
   /**
    * Denotes that the event/eventinstance is not published.
    */
-- 
GitLab