diff --git flag.inc flag.inc index 1eb474b..39728c6 100644 --- flag.inc +++ flag.inc @@ -6,6 +6,8 @@ * of Views 2. */ +include_once dirname(__FILE__) . '/includes/flag.entity.inc'; + /** * Implements hook_flag_definitions(). * @@ -522,11 +524,15 @@ class flag_flag { * The user on whose behalf to flag. Leave empty for the current user. * @param $skip_permission_check * Flag the item even if the $account user don't have permission to do so. + * @param $flagging + * Optional. This method works in tandem with Drupal's Field subsystem. + * Pass in a flagging entity if you want operate on it as well. + * * @return * FALSE if some error occured (e.g., user has no permission, flag isn't * applicable to the item, etc.), TRUE otherwise. */ - function flag($action, $content_id, $account = NULL, $skip_permission_check = FALSE) { + function flag($action, $content_id, $account = NULL, $skip_permission_check = FALSE, $flagging = NULL) { if (!isset($account)) { $account = $GLOBALS['user']; } @@ -567,15 +573,25 @@ class flag_flag { } } + // @todo: Discuss: Should we call field_attach_validate()? None of the + // entities in core does this (fields entered through forms are already + // validated). + // + // @todo: Discuss: Core wraps everything in a try { }, should we? + // Perform the flagging or unflagging of this flag. - $flagged = $this->_is_flagged($content_id, $uid, $sid); + $existing_fcid = $this->_is_flagged($content_id, $uid, $sid); + $flagged = (bool) $existing_fcid; if ($action == 'unflag') { if ($this->uses_anonymous_cookies()) { $this->_unflag_anonymous($content_id); } if ($flagged) { - $fcid = $this->_unflag($content_id, $uid, $sid); - module_invoke_all('flag', 'unflag', $this, $content_id, $account, $fcid); + // Note the order: We delete the entity before calling _unflag() to + // delete the {flag_content} record. + $this->_delete_flagging($existing_fcid); + $this->_unflag($content_id, $uid, $sid); + module_invoke_all('flag', 'unflag', $this, $content_id, $account, $existing_fcid); } } elseif ($action == 'flag') { @@ -584,14 +600,74 @@ class flag_flag { } if (!$flagged) { $fcid = $this->_flag($content_id, $uid, $sid); + // We're writing out a flagging entity even when we aren't passed one + // (e.g., when flagging via JavaScript toggle links); in this case + // Field API will assign the fields their default values. + $this->_insert_flagging($flagging, $content_id, $fcid); module_invoke_all('flag', 'flag', $this, $content_id, $account, $fcid); } + else { + // Nothing to do. Item is already flagged. + // + // Except in the case a $flagging object is passed in: in this case + // we're, for example, arriving from an editing form and need to update + // the entity. + if ($flagging) { + $this->_update_flagging($flagging); + } + } } return TRUE; } /** + * The entity CRUD methods _{insert,update,delete}_flagging() are for private + * use by the flag() method. + * + * The reason programmers should not call them directly is because a flagging + * operation is also accompanied by some bookkeeping (calling hooks, updating + * counters) or access control. These tasks are handled by the flag() method. + */ + private function _insert_flagging($flagging, $content_id, $fcid) { + if (!$flagging) { + // Create a flagging entity if none was passed in. + $flagging = $this->new_flagging($content_id); + } + $flagging->fcid = $fcid; + field_attach_presave('flagging', $flagging); + field_attach_insert('flagging', $flagging); + } + private function _update_flagging($flagging) { + field_attach_presave('flagging', $flagging); + field_attach_update('flagging', $flagging); + // Update the cache. + flagging_load($flagging->fcid, TRUE); + } + private function _delete_flagging($fcid) { + if (($flagging = flagging_load($fcid))) { + field_attach_delete('flagging', $flagging); + // Remove from the cache. + flagging_load($fcid, TRUE); + } + } + + /** + * Helper: Constructs a new, empty flagging entity object. + * + * The returned object has at least the 'flag_name' property set, which + * enables Field API to figure out the bundle, but it's your responsibility + * to eventually populate 'content_id' and 'fcid'. + */ + function new_flagging($content_id = NULL) { + return (object) array( + 'fcid' => NULL, + 'flag_name' => $this->name, + 'content_id' => $content_id, + ); + } + + /** * Determines if a certain user has flagged this content. * * Thanks to using a cache, inquiring several different flags about the same @@ -631,6 +707,15 @@ class flag_flag { } /** + * Similar to is_flagged() excepts it returns the flagging entity. + */ + function get_flagging($content_id, $uid = NULL, $sid = NULL) { + if (($record = $this->get_flagging_record($content_id, $uid, $sid))) { + return flagging_load($record->fcid); + } + } + + /** * Determines if a certain user has flagged this content. * * You probably shouldn't call this raw private method: call the @@ -826,6 +911,11 @@ class flag_flag { * token contexts they understand. */ function replace_tokens($label, $contexts, $options, $content_id) { + if (strpos($label , 'flagging:') !== FALSE) { + if (($flagging = $this->get_flagging($content_id))) { + $contexts['flagging'] = $flagging; + } + } return token_replace($label, $contexts, $options); } @@ -837,7 +927,7 @@ class flag_flag { * Derived classes should override this. */ function get_labels_token_types() { - return array(); + return array('flagging'); } /** @@ -1312,9 +1402,9 @@ class flag_node extends flag_flag { return FALSE; } - function flag($action, $content_id, $account = NULL, $skip_permission_check = FALSE) { + function flag($action, $content_id, $account = NULL, $skip_permission_check = FALSE, $flagging = NULL) { $content_id = $this->get_translation_id($content_id); - return parent::flag($action, $content_id, $account, $skip_permission_check); + return parent::flag($action, $content_id, $account, $skip_permission_check, $flagging); } // Instead of overriding is_flagged() we override get_flagging_record(), diff --git flag.info flag.info index 62511b5..5a8a58b 100644 --- flag.info +++ flag.info @@ -6,6 +6,7 @@ configure = admin/structure/flags ; Files that contain classes. files[] = flag.inc +files[] = includes/flag.entity.inc files[] = includes/flag.rules.inc files[] = includes/flag_handler_argument_content_id.inc files[] = includes/flag_handler_field_ops.inc diff --git flag.module flag.module index a4a9fdf..ad74b6c 100644 --- flag.module +++ flag.module @@ -169,6 +169,16 @@ function flag_help($path, $arg) { case FLAG_ADMIN_PATH: $output = '

' . t('This page lists all the flags that are currently defined on this system.') . '

'; return $output; + case FLAG_ADMIN_PATH . '/manage/%/fields': + $output = '

' . t('Flags can have fields added to them. For example, a "Spam" flag could have a Reason field where a user could type in why he believes the item flagged is spam. A "Bookmarks" flag could have a Folder field into which a user could arrange her bookmarks.') . '

'; + $output .= '

' . t('On this page you can add fields to flags, delete them, and otherwise manage them.') . '

'; + $output .= '

'; + $output .= t('You will also want to pick the "Form" link type for your flag, or else users won\'t have a means to enter values for the fields. (In case a form isn\'t used, the fields are assigned their default values.)', array('@form-link-type-url' => url('admin/structure/flags/manage/' . $arg[4], array('fragment' => 'edit-link-type')))); + if (!module_exists('flag_form')) { + $output .= ' ' . t("Note: You don't have the Flag Form module enabled. You'll have to enable it to have the \"Form\" link-type.", array('@enable-url' => url('admin/modules'))) . ''; + } + $output .= '

'; + return $output; } } diff --git includes/flag.admin.inc includes/flag.admin.inc index 8ff9ffa..83c3881 100644 --- includes/flag.admin.inc +++ includes/flag.admin.inc @@ -26,9 +26,13 @@ function theme_flag_admin_page($variables) { foreach ($flags as $flag) { $ops = array( 'flags_edit' => array('title' => t('edit'), 'href' => $flag->admin_path('edit')), + 'flags_fields' => array('title' => t('manage fields'), 'href' => $flag->admin_path('fields')), 'flags_delete' => array('title' => t('delete'), 'href' => $flag->admin_path('delete')), 'flags_export' => array('title' => t('export'), 'href' => $flag->admin_path('export')), ); + if (!module_exists('field_ui')) { + unset($ops['flags_fields']); + } $roles = array_flip(array_intersect(array_flip(user_roles()), $flag->roles['flag'])); $rows[] = array( diff --git includes/flag.token.inc includes/flag.token.inc index 5cd1f4b..09e4464 100644 --- includes/flag.token.inc +++ includes/flag.token.inc @@ -27,6 +27,22 @@ function flag_token_info() { 'description' => t('The human-readable flag title.'), ); + // Flagging tokens. + // + // Attached fields are exposed as tokens via some contrib module, but we + // need to expose other fields ourselves. Currently, 'date' is the only such + // field we expose. + $types['flagging'] = array( + 'name' => t('Flaggings'), + 'description' => t('Tokens related to flaggings.'), + 'needs-data' => 'flagging', + ); + $tokens['flagging']['date'] = array( + 'name' => t('Flagging date'), + 'description' => t('The date an item was flagged.'), + 'type' => 'date', + ); + // Flage action tokens. $types['flag-action'] = array( 'name' => t('Flag actions'), @@ -81,6 +97,7 @@ function flag_token_info() { function flag_tokens($type, $tokens, array $data = array(), array $options = array()) { $replacements = array(); $sanitize = !empty($options['sanitize']); + $langcode = isset($options['language']) ? $options['language']->language : NULL; if ($type == 'flag' && !empty($data['flag'])) { $flag = $data['flag']; @@ -95,6 +112,19 @@ function flag_tokens($type, $tokens, array $data = array(), array $options = arr } } } + elseif ($type == 'flagging' && !empty($data['flagging'])) { + $flagging = $data['flagging']; + foreach ($tokens as $name => $original) { + switch ($name) { + case 'date': + $replacements[$original] = format_date($flagging->timestamp, 'medium', '', NULL, $langcode); + break; + } + } + if ($date_tokens = token_find_with_prefix($tokens, 'date')) { + $replacements += token_generate('date', $date_tokens, array('date' => $flagging->timestamp), $options); + } + } elseif ($type == 'flag-action' && !empty($data['flag-action'])) { $action = $data['flag-action']; foreach ($tokens as $name => $original) {