Create and Read | AJAX (Telamonian) →

I’m sort of cheating with this section title: there is no deletion. For obvious data loss reasons, I want to make deletion as difficult as possible—of course, it will still be possible to simulate “move to recycle bin” deletion that keeps the items around where they can’t annoy you, but that kind of deletion is actually a simple update.

This does mean that there will be no way to decrease memory usage in the system, as the tables will get larger and any attempt to delete data will only result in more lines being added to the revision table. The problem with deletion is that:

  • Items can be linked to from other items. What happens to the links when the referenced item is permanently deleted?
  • Who can delete items? What if someone wanted to keep a copy of the item because it was interesting? The entire JITBrain philosophy is that you can cancel any mistakes your coworkers make, which is incompatible with deletion.

These problems will eventually have to  be solved—not everyone has several gigabytes of storage available—but they are not a priority.

The real priority is allowing the user to actually do things with their items. Namely:

  1. Create items (ItemModel references ItemRevisionModel for this, so it doesn’t work yet).
  2. Solve items or mark them back as unsolved.
  3. Delete and un-delete items.
  4. Archive or un-archive items.
  5. Make public, or remove public status.
  6. Attach a file to an item.
  7. Comment on an item.
  8. Assign an item to an user.
  9. Assign an item to a group.
  10. Reference an item from another (see also).
  11. Mark two items as duplicates of one another.
  12. Show an item to an user.
  13. Edit the original text of an item.
  14. Shake an item to reset its “last modified” timestamp.

Others will come later, but these are the elemental, fundamental operations we want to let the users do with their items. But what happens, exactly, when one of these operations is performed?

  • Save the operation as a line in the revision table. This includes at the very least the item, the user performing the operation and the type of operation. It may also include additional data, if the operation has parameters.
  • If the operation has any visible effects on items, update the corresponding fields in the item table. For instance, editing an item changes the text in the item table, attaching a file increments the attachment counter in the item table, and showing an item to an user has no effect on the item table.
  • If the operation is considered relevant, also change the ‘last modified’ timestamp in the item table. The key idea is, do you want the operation to push the item up to the top of the active item list? I know that an un-deleted item deserves being pushed up, but I’m not certain that adding duplicates, publishing or even solving items should push them up. Relevance is something to be determined when working on the system—and if your favourite operation doesn’t push your item up, you can always shake it.
  • Some operations, such as editing the original text of an item, may require moderator access to the item. Other operations merely require contributor access. In both cases, the access level must be checked (within a transaction, of course) right before the modification is applied.

So, it turns out, most operations will be performing these four steps, so I should provide a single database-altering function that performs them, and simply let every operation configure that function through an argument to have it behave appropriately. That function is ItemRevisionModel::_apply:

  // -> revision = null    : the type of revision, with arguments
  // -> relevant = false   : whether the altered_on is updated
  // -> update   = null    : the update query, with parameters and types
  // -> restrict = false   : moderators only?
  private static function _apply($item$user$config$transaction false)
  {
      $master DBConfig::Master();
      DBUtils::Begin($master$transaction);

      $restrict array_key_exists('restrict'$config)
                ? $config['restrict'] : false;

      if ($restrict) {
          if (!ItemModel::IsItemModerator($item$user)) {
              DBUtils::Rollback($master$transaction);
              return false;
          }
      }

      else {
          if (!ItemModel::IsItemContributor($item$user)) {
              DBUtils::Rollback($master$transaction);
              return false;
          }
      }

      $revision array_key_exists('revision'$config)
                ? $config['revision'] : null;

      if (isset($revision)) {
          $type array_shift($revision);
          $fields self::Fields($type);

          $params '';
          $inserted = array();
          $args '';
          $types 'iis';
          foreach ($fields as $field => $type) {
              $params    .= ',?';
              $args      .= ",$field";
              $types     .= $type;
              $inserted []= array_shift($revision);
          }

          $query "`revision` (`item_id`, `user_id`, `type`, `date`$args) " .
                   "VALUES(?,?,?,NOW()$params)";

          DBUtils::Insert($master$query$types,
                          $item$user$type$inserted);
      }

      $relevant array_key_exists('relevant'$config)
                ? $config['relevant'] : null;

      $update array_key_exists('update'$config)
                ? $config['update'] : null;

      if ($relevant || $update) {
          $do = array();
          if ($relevant) $do[] = '`altered_on` = NOW()';
          if ($update)   $do[] = array_shift($update);

          $query "`item` SET " implode(','$do) . " WHERE `item_id` = ?";
          $types = ($update array_shift($update) : '') . 'i';
          $param $update $update : array();

          DBUtils::Update($master$query$types$param$item);
      }

      DBUtils::Commit($master$transaction);
      return true;
  }

This function determines dynamically which fields of the revision table should be updated based on the type of revision being inserted. This is done using the ItemRevisionModel::Fields function:

  public static function Fields($type)
  {
      switch ($type) {
          case self::CREATED:
              return array();
          case self::SHOWN_TO_USER:
          case self::ASSIGNED_TO_USER:
              return array('user_to' => 'i');
          case self::ASSIGNED_TO_GROUP:
              return array('group_to' => 'i');
          case self::COMMENTED_ON:
          case self::FILE_ATTACHED:
          case self::EDITED:
              return array('text' => 's');
          case self::SOLVED:
          case self::DELETED:
          case self::PUBLISHED:
          case self::ARCHIVED:
              return array('status' => 'i');
          case self::RELATED_TO_ITEM:
              return array('relationship' => 's''item_to' => 'i');
          default:
              assert(false);
      }
  }

Once all of this has been set up, the functions that perform the actual operations tend to be a boring sequence of configuring the _apply function:

  public static function Create($item$user$transaction false)
  {
      $config = array(
        'revision' => array(self::CREATED)
      );

      return self::_apply($item$user$config$transaction);
  }

  public static function Solve($item$user$solved$transaction false)
  {
      $config = array(
        'revision' => array(self::SOLVED, (int)$solved),
        'update'   => array('`is_solved` = ?''i', (int)$solved)
      );

      return self::_apply($item$user$config$transaction);
  }

  public static function Delete($item$user$deleted$transaction false)
  {
      $config = array(
        'revision' => array(self::DELETED, (int)$deleted),
        'update'   => array('`is_deleted` = ?''i', (int)$deleted), 
        'relevant' => true
      );

      return self::_apply($item$user$config$transaction);
  }

  public static function Archive($item$user$archived$transaction false)
  {
      $config = array(
        'revision' => array(self::ARCHIVED, (int)$archived),
        'update'   => array('`is_archive` = ?''i', (int)$archived),
        'relevant' => true
      );

      return self::_apply($item$user$config$transaction);
  }

  public static function Publish($item$user$public$transaction false)
  {
      $config = array(
        'revision' => array(self::PUBLISHED, (int)$public), 
        'update'   => array('`is_public` = ?''i', (int)$public), 
        'relevant' => true
      );

      return self::_apply($item$user$config$transaction);
  }

  public static function Attach($item$user$uri$transaction false)
  {
      $config = array(
        'revision' => array(self::FILE_ATTACHED$uri),
        'update'   => array('`attached` = 1 + `attached`'),
        'relevant' => true
      );

      return self::_apply($item$user$config$transaction);
  }

  public static function Comment($item$user$comment$transaction false)
  {
      $config = array(
        'revision' => array(self::COMMENTED_ON$comment), 
        'update'   => array('`comments` = 1 + `comments`'), 
        'relevant' => true
      );

      return self::_apply($item$user$config$transaction);
  }

  public static function AssignToUser($item$user$to$transaction false)
  {
      $config = array(
        'revision' => array(self::ASSIGN_TO_USER$to),
        'update'   => array('`user_id` = ?''i'$to), 
        'relevant' => true
      );

      return self::_apply($item$user$config$transaction);
  }

  public static function AssignToGroup($item$user$to$transaction false)
  {
      $config = array(
        'revision' => array(self::ASSIGN_TO_GROUP$to),
        'update'   => array('`group_id` = ?''i'$to), 
        'relevant' => true
      );

      return self::_apply($item$user$config$transaction);
  }

  public static function SeeAlso($item$user$also$transaction false)
  {
      $config = array(
        'revision' => array(self::RELATED_TO_ITEMself::SEE_ALSO$also)
      );

      return self::_apply($item$user$config$transaction);
  }

  public static function Duplicate($item$user$dup$transaction false)
  {
      $master DBConfig::Get($transaction);
      DBUtils::Begin($master$transaction);

      $config_a = array(
        'revision' => array(self::RELATED_TO_ITEMself::DUPLICATE$dup)
      );

      $config_b = array(
        'revision' => array(self::RELATED_TO_ITEMself::DUPLICATE$item)
      );

      if (self::_apply($item$user$config_atrue) && 
          self::_apply($dup$user$config_btrue))
      {
          DBUtils::Commit($master$transaction);
          return true;
      }

      DBUtils::Rollback($master$transaction);
      return false;
  }

  public static function Show($item$user$to$transaction false)
  {
      $config = array(
        'revision' => array(self::SHOWN_TO_USER$to)
      );

      return self::_apply($item$user$config$transaction);
  }

  public static function Edit($item$user$text$transaction false)
  {
      $master DBConfig::Master();

      DBUtils::Begin($master$transaction);

      $current ItemModel::GetItem($item$usertrue);

      if (count($current) != 1) {
          DBUtils::Rollback($master$transaction);
          return false;
      }

      $config = array(
        'revision' => array(self::EDITED$current[0]['text']), 
        'update'   => array('`text` = ?''s'$text), 
        'relevant' => true,
        'restrict' => true
      );

      $success self::_apply($item$user$configtrue);

      if ($success)
          DBUtils::Commit($master$transaction);
      else
          DBUtils::Rollback($master$transaction);

      return $success;
  }

  public static function Shake($item$user$transaction false)
  {
      $config = array(
        'relevant' => true,
        'revision' => array(self::SHAKEN)
      );

      return self::_apply($item$user$config$transaction);
  }

The only creative bits are the “edit” function which needs to extract the current contents of the item so that they can be stored in the revision, and the “duplicate” function which needs to mark two items as duplicates instead of only one.

The ability to perform the operations in a transaction is golden: you don’t need to check beforehand that both items are accessible, since if one of the two attempts fails the other (if already done) will be canceled automatically.

Finally, since it will become useful soon, the ability to get all revisions for an item:

  // Get revisions for item. Limit applies if not null. Returns
  // null if item is not visible.
  public static function Get($item$user$limit null$transaction false)
  {
      $database DBConfig::Get($transaction);

      $visible IsItemContributor($user$item$transaction);

      if (!$visible)
          return null;

      $query '* FROM `revision` WHERE `item_id` = ? ' .
               'ORDER BY `revision_id` DESC' .
               (is_int($limit) ? " LIMIT $limit" '');

      return DBUtils::Select($database$query'i'$item);
  }

Since a given item may have a lot of revisions, it’s possible to specify a limit to get only the latest elements. The function returns elements in reverse order (most recent first) .

So, how does all this data get displayed, and how does the user perform the changes?

Create and Read | AJAX (Telamonian) →

0 Responses to “19. Update and Delete”


  1. No Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>



1170 feed subscribers
(readers who polled a feed this week)