user_reference.module 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221
  1. <?php
  2. /**
  3. * @file
  4. * Defines a field type for referencing a user from a node.
  5. */
  6. /**
  7. * Implements hook_menu().
  8. */
  9. function user_reference_menu() {
  10. $items['user_reference/autocomplete/%/%/%'] = array(
  11. 'page callback' => 'user_reference_autocomplete',
  12. 'page arguments' => array(2, 3, 4),
  13. 'access callback' => 'reference_autocomplete_access',
  14. 'access arguments' => array(2, 3, 4),
  15. 'type' => MENU_CALLBACK,
  16. );
  17. return $items;
  18. }
  19. /**
  20. * Implements hook_field_info().
  21. */
  22. function user_reference_field_info() {
  23. return array(
  24. 'user_reference' => array(
  25. 'label' => t('User reference'),
  26. 'description' => t('This field stores the ID of a related user as an integer value.'),
  27. 'settings' => array(
  28. 'referenceable_roles' => array(),
  29. 'referenceable_status' => array(),
  30. 'view' => array(
  31. 'view_name' => '',
  32. 'display_name' => '',
  33. 'args' => array(),
  34. ),
  35. ),
  36. 'default_widget' => 'user_reference_autocomplete',
  37. 'default_formatter' => 'user_reference_default',
  38. // Support hook_entity_property_info() from contrib "Entity API".
  39. 'property_type' => 'user',
  40. // Support default token formatter for field tokens.
  41. 'default_token_formatter' => 'user_reference_plain',
  42. ),
  43. );
  44. }
  45. /**
  46. * Implements hook_field_schema();
  47. */
  48. function user_reference_field_schema($field) {
  49. $columns = array(
  50. 'uid' => array(
  51. 'type' => 'int',
  52. 'unsigned' => TRUE,
  53. 'not null' => FALSE,
  54. ),
  55. );
  56. return array(
  57. 'columns' => $columns,
  58. 'indexes' => array('uid' => array('uid')),
  59. 'foreign keys' => array(
  60. 'uid' => array(
  61. 'table' => 'users',
  62. 'columns' => array('uid' => 'uid'),
  63. ),
  64. ),
  65. );
  66. }
  67. /**
  68. * Implements hook_field_settings_form().
  69. */
  70. function user_reference_field_settings_form($field, $instance, $has_data) {
  71. $settings = $field['settings'];
  72. $form = array();
  73. $form['referenceable_roles'] = array(
  74. '#type' => 'checkboxes',
  75. '#title' => t('User roles that can be referenced'),
  76. '#default_value' => is_array($settings['referenceable_roles'])
  77. ? array_filter($settings['referenceable_roles'])
  78. : array(),
  79. '#options' => user_roles(TRUE),
  80. );
  81. $form['referenceable_status'] = array(
  82. '#type' => 'checkboxes',
  83. '#title' => t('User status that can be referenced'),
  84. '#default_value' => is_array($settings['referenceable_status'])
  85. ? array_filter($settings['referenceable_status'])
  86. : array(1),
  87. '#options' => array(1 => t('Active'), 0 => t('Blocked')),
  88. );
  89. if (module_exists('views')) {
  90. $view_settings = $settings['view'];
  91. $description = '<p>' . t('The list of users that can be referenced can provided by a view (Views module) using the "References" display type.') . '</p>';
  92. // Special note for legacy fields migrated from D6.
  93. if (!empty($view_settings['view_name']) && $view_settings['display_name'] == 'default') {
  94. $description .= '<p><strong><span class="admin-missing">'. t("Important D6 migration note:") . '</span></strong>';
  95. $description .= '<br/>' . t("The field is currently configured to use the 'Master' display of the view %view_name.", array('%view_name' => $view_settings['view_name']));
  96. $description .= '<br/>' . t("It is highly recommended that you: <br/>- edit this view and create a new display using the 'References' display type, <br/>- update the field settings to explicitly select the correct view and display.");
  97. $description .= '<br/>' . t("The field will work correctly until then, but submitting this form might inadvertently change the field settings.") . '</p>';
  98. }
  99. $form['view'] = array(
  100. '#type' => 'fieldset',
  101. '#title' => t('Views - Users that can be referenced'),
  102. '#collapsible' => TRUE,
  103. '#collapsed' => empty($view_settings['view_name']),
  104. '#description' => $description,
  105. );
  106. $views_options = references_get_views_options('user');
  107. if ($views_options) {
  108. // The value of the 'view_and_display' select below will need to be split
  109. // into 'view_name' and 'view_display' in the final submitted values, so
  110. // we massage the data at validate time on the wrapping element (not
  111. // ideal).
  112. $form['view']['#element_validate'] = array('_user_reference_view_settings_validate');
  113. $views_options = array('' => '<' . t('none') . '>') + $views_options;
  114. $default = empty($view_settings['view_name']) ? '' : $view_settings['view_name'] . ':' .$view_settings['display_name'];
  115. $form['view']['view_and_display'] = array(
  116. '#type' => 'select',
  117. '#title' => t('View used to select the users'),
  118. '#options' => $views_options,
  119. '#default_value' => $default,
  120. '#description' => '<p>' . t('Choose the view and display that select the nodes that can be referenced.<br />Only views with a display of type "References" are eligible.') . '</p>' .
  121. t('Note:<ul><li>This will discard the "Referenceable Roles" and "Referenceable Status" settings above. Use the view\'s "filters" section instead.</li><li>Use the view\'s "fields" section to display additional informations about candidate users on user creation/edition form.</li><li>Use the view\'s "sort criteria" section to determine the order in which candidate users will be displayed.</li></ul>'),
  122. );
  123. $default = implode(', ', $view_settings['args']);
  124. $form['view']['args'] = array(
  125. '#type' => 'textfield',
  126. '#title' => t('View arguments'),
  127. '#default_value' => $default,
  128. '#required' => FALSE,
  129. '#description' => t('Provide a comma separated list of arguments to pass to the view.'),
  130. );
  131. }
  132. else {
  133. $form['view']['no_view_help'] = array(
  134. '#markup' => '<p>' . t('No eligible view was found.') .'</p>',
  135. );
  136. }
  137. }
  138. return $form;
  139. }
  140. /**
  141. * Validate callback for the 'view settings' fieldset.
  142. *
  143. * Puts back the various form values in the expected shape.
  144. */
  145. function _user_reference_view_settings_validate($element, &$form_state, $form) {
  146. // Split view name and display name from the 'view_and_display' value.
  147. if (!empty($element['view_and_display']['#value'])) {
  148. list($view, $display) = explode(':', $element['view_and_display']['#value']);
  149. }
  150. else {
  151. $view = '';
  152. $display = '';
  153. }
  154. // Explode the 'args' string into an actual array. Beware, explode() turns an
  155. // empty string into an array with one empty string. We'll need an empty array
  156. // instead.
  157. $args_string = trim($element['args']['#value']);
  158. $args = ($args_string === '') ? array() : array_map('trim', explode(',', $args_string));
  159. $value = array('view_name' => $view, 'display_name' => $display, 'args' => $args);
  160. form_set_value($element, $value, $form_state);
  161. }
  162. /**
  163. * Implements hook_field_validate().
  164. *
  165. * Possible error codes:
  166. * - 'invalid_uid': uid is not valid for the field (not a valid user id, or the user is not referenceable).
  167. */
  168. function user_reference_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) {
  169. // Extract uids to check.
  170. $ids = array();
  171. // First check non-numeric uid's to avoid losing time with them.
  172. foreach ($items as $delta => $item) {
  173. if (is_array($item) && !empty($item['uid'])) {
  174. if (is_numeric($item['uid'])) {
  175. $ids[] = $item['uid'];
  176. }
  177. else {
  178. $errors[$field['field_name']][$langcode][$delta][] = array(
  179. 'error' => 'invalid_uid',
  180. 'message' => t('%name: invalid input.',
  181. array('%name' => $instance['label'])),
  182. );
  183. }
  184. }
  185. }
  186. // Prevent performance hog if there are no ids to check.
  187. if ($ids) {
  188. $options = array(
  189. 'ids' => $ids,
  190. );
  191. $refs = user_reference_potential_references($field, $options);
  192. foreach ($items as $delta => $item) {
  193. if (is_array($item)) {
  194. if (!empty($item['uid']) && !isset($refs[$item['uid']])) {
  195. $errors[$field['field_name']][$langcode][$delta][] = array(
  196. 'error' => 'invalid_uid',
  197. 'message' => t("%name: this user can't be referenced.",
  198. array('%name' => $instance['label'])),
  199. );
  200. }
  201. }
  202. }
  203. }
  204. }
  205. /**
  206. * Implements hook_field_prepare_view().
  207. */
  208. function user_reference_field_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items) {
  209. $checked_ids = &drupal_static(__FUNCTION__, array());
  210. // Set an 'access' property on each item (TRUE if the user exists).
  211. // Extract ids to check.
  212. $ids = array();
  213. foreach ($items as $id => $entity_items) {
  214. foreach ($entity_items as $delta => $item) {
  215. if (is_array($item)) {
  216. // Default to 'not accessible'.
  217. $items[$id][$delta]['access'] = FALSE;
  218. if (!empty($item['uid']) && is_numeric($item['uid'])) {
  219. $ids[$item['uid']] = $item['uid'];
  220. }
  221. }
  222. }
  223. }
  224. if ($ids) {
  225. // Load information about ids that we haven't already loaded during this
  226. // page request.
  227. $ids_to_check = array_diff($ids, array_keys($checked_ids));
  228. if (!empty($ids_to_check)) {
  229. $query = db_select('users', 'u')
  230. ->fields('u', array('uid'))
  231. ->condition('u.uid', $ids_to_check, 'IN');
  232. $accessible_ids = $query->execute()->fetchAllAssoc('uid');
  233. // Populate our static list so that we do not query on those ids again.
  234. foreach ($ids_to_check as $id) {
  235. $checked_ids[$id] = isset($accessible_ids[$id]);
  236. }
  237. }
  238. foreach ($items as $id => $entity_items) {
  239. foreach ($entity_items as $delta => $item) {
  240. if (is_array($item) && !empty($item['uid']) && !empty($checked_ids[$item['uid']])) {
  241. $items[$id][$delta]['access'] = TRUE;
  242. }
  243. }
  244. }
  245. }
  246. }
  247. /**
  248. * Implements hook_field_is_empty().
  249. */
  250. function user_reference_field_is_empty($item, $field) {
  251. return empty($item['uid']);
  252. }
  253. /**
  254. * Implements hook_field_formatter_info().
  255. */
  256. function user_reference_field_formatter_info() {
  257. return array(
  258. 'user_reference_default' => array(
  259. 'label' => t('Default'),
  260. 'description' => t("Display the name of the referenced user as a link to the user's profile page."),
  261. 'field types' => array('user_reference'),
  262. ),
  263. 'user_reference_plain' => array(
  264. 'label' => t('Plain text'),
  265. 'description' => t('Display the name of the referenced user as plain text.'),
  266. 'field types' => array('user_reference'),
  267. ),
  268. 'user_reference_user' => array(
  269. 'label' => t('Rendered user'),
  270. 'description' => t('Display the referenced user in a specific view mode'),
  271. 'field types' => array('user_reference'),
  272. 'settings' => array('user_reference_view_mode' => 'full'),
  273. ),
  274. 'user_reference_uid' => array(
  275. 'label' => t('User ID'),
  276. 'description' => t('Display the referenced user ID'),
  277. 'field types' => array('user_reference'),
  278. ),
  279. 'user_reference_path' => array(
  280. 'label' => t('URL as plain text'),
  281. 'description' => t('Display the URL of the referenced user'),
  282. 'field types' => array('user_reference'),
  283. 'settings' => array(
  284. 'alias' => TRUE,
  285. 'absolute' => FALSE
  286. ),
  287. ),
  288. );
  289. }
  290. /**
  291. * Implements hook_field_formatter_settings_form().
  292. */
  293. function user_reference_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
  294. $display = $instance['display'][$view_mode];
  295. $settings = $display['settings'];
  296. $element = array();
  297. switch ($display['type']) {
  298. case 'user_reference_user':
  299. $entity_info = entity_get_info('user');
  300. $modes = $entity_info['view modes'];
  301. $options = array();
  302. foreach ($modes as $name => $mode) {
  303. $options[$name] = $mode['label'];
  304. }
  305. $element['user_reference_view_mode'] = array(
  306. '#title' => t('View mode'),
  307. '#type' => 'select',
  308. '#options' => $options,
  309. '#default_value' => $settings['user_reference_view_mode'],
  310. // Never empty, so no #empty_option
  311. );
  312. break;
  313. case 'user_reference_path':
  314. $element['alias'] = array(
  315. '#type' => 'checkbox',
  316. '#title' => t('Display the aliased path (if exists) instead of the system path'),
  317. '#default_value' => $settings['alias'],
  318. );
  319. $element['absolute'] = array(
  320. '#type' => 'checkbox',
  321. '#title' => t('Display an absolute URL'),
  322. '#default_value' => $settings['absolute'],
  323. );
  324. break;
  325. }
  326. return $element;
  327. }
  328. /**
  329. * Implements hook_field_formatter_settings_summary().
  330. */
  331. function user_reference_field_formatter_settings_summary($field, $instance, $view_mode) {
  332. $display = $instance['display'][$view_mode];
  333. $settings = $display['settings'];
  334. $summary = array();
  335. switch ($display['type']) {
  336. case 'user_reference_user':
  337. $entity_info = entity_get_info('user');
  338. $modes = $entity_info['view modes'];
  339. $mode = $modes[$settings['user_reference_view_mode']]['label'];
  340. $summary[] = t('View mode: %mode', array('%mode' => $mode));
  341. break;
  342. case 'user_reference_path':
  343. $summary[] = t('Aliased path: %yes_no', array('%yes_no' => $settings['alias'] ? t('Yes') : t('No')));
  344. $summary[] = t('Absolute URL: %yes_no', array('%yes_no' => $settings['absolute'] ? t('Yes') : t('No')));
  345. break;
  346. }
  347. return implode('<br />', $summary);
  348. }
  349. /**
  350. * Implements hook_field_formatter_prepare_view().
  351. *
  352. * Preload all user referenced by items using 'full entity' formatters.
  353. */
  354. function user_reference_field_formatter_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items, $displays) {
  355. // Load the referenced users, except for the 'user_reference_uid' which does
  356. // not need full objects.
  357. // Collect ids to load.
  358. $ids = array();
  359. foreach ($displays as $id => $display) {
  360. if ($display['type'] != 'user_reference_uid') {
  361. foreach ($items[$id] as $delta => $item) {
  362. if ($item['access']) {
  363. $ids[$item['uid']] = $item['uid'];
  364. }
  365. }
  366. }
  367. }
  368. $entities = user_load_multiple($ids);
  369. // Add the loaded user objects to the items.
  370. foreach ($displays as $id => $display) {
  371. if ($display['type'] != 'user_reference_uid') {
  372. foreach ($items[$id] as $delta => $item) {
  373. if ($item['access']) {
  374. $items[$id][$delta]['user'] = $entities[$item['uid']];
  375. }
  376. }
  377. }
  378. }
  379. }
  380. /**
  381. * Implements hook_field_formatter_view().
  382. */
  383. function user_reference_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
  384. $settings = $display['settings'];
  385. $result = array();
  386. // @todo Optimisation: use hook_field_formatter_prepare_view() to load
  387. // user names or full user entities in 'multiple' mode.
  388. // Collect the list of user ids.
  389. $uids = array();
  390. foreach ($items as $delta => $item) {
  391. $uids[$item['uid']] = $item['uid'];
  392. }
  393. switch ($display['type']) {
  394. case 'user_reference_default':
  395. case 'user_reference_plain':
  396. foreach ($items as $delta => $item) {
  397. if ($item['access']) {
  398. $user = $item['user'];
  399. $label = entity_label('user', $user);
  400. if ($display['type'] == 'user_reference_default') {
  401. $uri = entity_uri('user', $user);
  402. $result[$delta] = array(
  403. '#type' => 'link',
  404. '#title' => $label,
  405. '#href' => $uri['path'],
  406. '#options' => $uri['options'],
  407. );
  408. }
  409. else {
  410. $result[$delta] = array(
  411. '#markup' => check_plain($label),
  412. );
  413. }
  414. }
  415. }
  416. break;
  417. case 'user_reference_user':
  418. $view_mode = $display['settings']['user_reference_view_mode'];
  419. // To prevent infinite recursion caused by reference cycles, we store
  420. // diplayed accounts in a recursion queue.
  421. $recursion_queue = &drupal_static(__FUNCTION__, array());
  422. // If no 'referencing entity' is set, we are starting a new 'reference
  423. // thread' and need to reset the queue.
  424. // @todo Bug: $entity->referencing_entity on accounts referenced in a different
  425. // thread on the page. E.g: 1 references 1+2 / 2 references 1+2 / visit homepage.
  426. // We'd need a more accurate way...
  427. if (!isset($entity->referencing_entity)) {
  428. $recursion_queue = array();
  429. }
  430. // The recursion queue only needs to track nodes.
  431. if ($entity_type == 'user') {
  432. list($id) = entity_extract_ids($entity_type, $entity);
  433. $recursion_queue[$id] = $id;
  434. }
  435. // Check the recursion queue to determine which accounts should be fully
  436. // displayed, and which accounts will only be displayed as a username.
  437. $users_display = array();
  438. foreach ($items as $delta => $item) {
  439. if ($item['access'] && !isset($recursion_queue[$item['uid']])) {
  440. $users_display[$item['uid']] = $item['user'];
  441. }
  442. }
  443. // Load and build the fully displayed nodes.
  444. if ($users_display) {
  445. $users_built = array('users' => array('#sorted' => TRUE));
  446. foreach ($users_display as $uid => $account) {
  447. $users_display[$uid]->referencing_entity = $entity;
  448. $users_display[$uid]->referencing_field = $field['field_name'];
  449. $users_built['users'][$account->uid] = user_view($account, $settings['user_reference_view_mode']);
  450. }
  451. }
  452. // Assemble the render array.
  453. foreach ($items as $delta => $item) {
  454. if ($item['access']) {
  455. if (isset($users_display[$item['uid']])) {
  456. $result[$delta] = $users_built['users'][$item['uid']];
  457. }
  458. else {
  459. $account = $item['user'];
  460. $label = entity_label('user', $user);
  461. $uri = entity_uri('user', $account);
  462. $result[$delta] = array(
  463. '#type' => 'link',
  464. '#title' => $label,
  465. '#href' => $uri['path'],
  466. '#options' => $uri['options'],
  467. );
  468. if (!$account->status) {
  469. $result[$delta]['#prefix'] = '<span class="user-unpublished">';
  470. $result[$delta]['#suffix'] = '</span>';
  471. }
  472. }
  473. }
  474. }
  475. break;
  476. case 'user_reference_uid':
  477. foreach ($items as $delta => $item) {
  478. if ($item['access']) {
  479. $result[$delta] = array(
  480. '#markup' => $item['uid'],
  481. );
  482. }
  483. }
  484. break;
  485. case 'user_reference_path':
  486. foreach ($items as $delta => $item) {
  487. if ($item['access']) {
  488. $uri = entity_uri('user', $item['user']);
  489. $options = array(
  490. 'absolute' => $settings['absolute'],
  491. 'alias' => !$settings['alias'],
  492. );
  493. $options += $uri['options'];
  494. $result[$delta] = array(
  495. '#markup' => url($uri['path'], $options),
  496. );
  497. }
  498. }
  499. break;
  500. }
  501. return $result;
  502. }
  503. /**
  504. * Helper function for widgets and formatters.
  505. *
  506. * Store user names collected in the curent request.
  507. */
  508. function _user_reference_get_user_names($uids, $known_titles = array()) {
  509. $titles = &drupal_static(__FUNCTION__, array());
  510. // Save titles we receive.
  511. $titles += $known_titles;
  512. // Collect nids to retrieve from database.
  513. $uids_query = array();
  514. foreach ($uids as $uid) {
  515. if (!isset($titles[$uid])) {
  516. $uids_query[] = $uid;
  517. }
  518. }
  519. if ($uids_query) {
  520. $query = db_select('users', 'u')
  521. ->fields('u', array('uid', 'name'))
  522. ->condition('u.uid', $uids);
  523. $titles += $query->execute()->fetchAllKeyed();
  524. }
  525. // Build the results array.
  526. $return = array();
  527. foreach ($uids as $uid) {
  528. $return[$uid] = isset($titles[$uid]) ? $titles[$uid] : '';
  529. }
  530. return $return;
  531. }
  532. /**
  533. * Implements hook_field_widget_info().
  534. */
  535. function user_reference_field_widget_info() {
  536. return array(
  537. 'user_reference_autocomplete' => array(
  538. 'label' => t('Autocomplete text field'),
  539. 'description' => t('Display the list of referenceable users as a textfield with autocomplete behaviour.'),
  540. 'field types' => array('user_reference'),
  541. 'settings' => array(
  542. 'autocomplete_match' => 'contains',
  543. 'size' => 60,
  544. 'autocomplete_path' => 'user_reference/autocomplete',
  545. ),
  546. ),
  547. );
  548. }
  549. /**
  550. * Implements hook_field_widget_info_alter().
  551. */
  552. function user_reference_field_widget_info_alter(&$info) {
  553. $info['options_select']['field types'][] = 'user_reference';
  554. $info['options_buttons']['field types'][] = 'user_reference';
  555. }
  556. /**
  557. * Implements hook_field_widget_settings_form().
  558. */
  559. function user_reference_field_widget_settings_form($field, $instance) {
  560. $widget = $instance['widget'];
  561. $defaults = field_info_widget_settings($widget['type']);
  562. $settings = array_merge($defaults, $widget['settings']);
  563. $form = array();
  564. if ($widget['type'] == 'user_reference_autocomplete') {
  565. $form['autocomplete_match'] = array(
  566. '#type' => 'select',
  567. '#title' => t('Autocomplete matching'),
  568. '#default_value' => $settings['autocomplete_match'],
  569. '#options' => array(
  570. 'starts_with' => t('Starts with'),
  571. 'contains' => t('Contains'),
  572. ),
  573. '#description' => t('Select the method used to collect autocomplete suggestions. Note that <em>Contains</em> can cause performance issues on sites with thousands of users.'),
  574. );
  575. $form['size'] = array(
  576. '#type' => 'textfield',
  577. '#title' => t('Size of textfield'),
  578. '#default_value' => $settings['size'],
  579. '#element_validate' => array('_element_validate_integer_positive'),
  580. '#required' => TRUE,
  581. );
  582. }
  583. return $form;
  584. }
  585. /**
  586. * Implements hook_field_widget_form().
  587. */
  588. function user_reference_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
  589. switch ($instance['widget']['type']) {
  590. case 'user_reference_autocomplete':
  591. $element += array(
  592. '#type' => 'textfield',
  593. '#default_value' => isset($items[$delta]['uid']) ? $items[$delta]['uid'] : NULL,
  594. '#autocomplete_path' => $instance['widget']['settings']['autocomplete_path'] . '/' . $instance['entity_type'] . '/' . $instance['bundle'] . '/' . $field['field_name'],
  595. '#size' => $instance['widget']['settings']['size'],
  596. '#element_validate' => array('user_reference_autocomplete_validate'),
  597. '#value_callback' => 'user_reference_autocomplete_value',
  598. );
  599. break;
  600. }
  601. return array('uid' => $element);
  602. }
  603. /**
  604. * Value callback for a user_reference autocomplete element.
  605. *
  606. * Substitute in the user name for the uid.
  607. */
  608. function user_reference_autocomplete_value($element, $input = FALSE, $form_state) {
  609. if ($input === FALSE) {
  610. // We're building the displayed 'default value': expand the raw uid into
  611. // "user name [uid:n]".
  612. $uid = $element['#default_value'];
  613. if (!empty($uid)) {
  614. $q = db_select('users', 'u');
  615. $q->addField('u', 'name');
  616. $q->condition('u.uid', $uid)
  617. ->range(0, 1);
  618. $result = $q->execute();
  619. // @todo If no result (user doesn't exist).
  620. $value = $result->fetchField();
  621. $value .= ' [uid:' . $uid . ']';
  622. return $value;
  623. }
  624. }
  625. }
  626. /**
  627. * Validation callback for a user_reference autocomplete element.
  628. */
  629. function user_reference_autocomplete_validate($element, &$form_state, $form) {
  630. $field = field_widget_field($element, $form_state);
  631. $instance = field_widget_instance($element, $form_state);
  632. $value = $element['#value'];
  633. $uid = NULL;
  634. if (!empty($value)) {
  635. // Check whether we have an explicit "[uid:n]" input.
  636. preg_match('/^(?:\s*|(.*) )?\[\s*uid\s*:\s*(\d+)\s*\]$/', $value, $matches);
  637. if (!empty($matches)) {
  638. // Explicit uid. Check that the 'name' part matches the actual name for
  639. // the uid.
  640. list(, $name, $uid) = $matches;
  641. if (!empty($name)) {
  642. $names = _user_reference_get_user_names(array($uid));
  643. if ($name != $names[$uid]) {
  644. form_error($element, t('%name: name mismatch. Please check your selection.', array('%name' => $instance['label'])));
  645. }
  646. }
  647. }
  648. else {
  649. // No explicit uid (the submitted value was not populated by autocomplete
  650. // selection). Get the uid of a referencable user from the entered name.
  651. $options = array(
  652. 'string' => $value,
  653. 'match' => 'equals',
  654. 'limit' => 1,
  655. );
  656. $references = user_reference_potential_references($field, $options);
  657. if ($references) {
  658. // @todo The best thing would be to present the user with an
  659. // additional form, allowing the user to choose between valid
  660. // candidates with the same name. ATM, we pick the first
  661. // matching candidate...
  662. $uid = key($references);
  663. }
  664. else {
  665. form_error($element, t('%name: found no valid user with that name.', array('%name' => $instance['label'])));
  666. }
  667. }
  668. }
  669. // Set the element's value as the user id that was extracted from the entered
  670. // input.
  671. form_set_value($element, $uid, $form_state);
  672. }
  673. /**
  674. * Implements hook_field_widget_error().
  675. */
  676. function user_reference_field_widget_error($element, $error, $form, &$form_state) {
  677. form_error($element['uid'], $error['message']);
  678. }
  679. /**
  680. * Builds a list of referenceable users suitable for the '#option' FAPI property.
  681. *
  682. * Warning: the function does NOT take care of encoding or escaping the user
  683. * names. Proper massaging needs to be performed by the caller, according to
  684. * the destination FAPI '#type' (radios / checkboxes / select).
  685. *
  686. * @param $field
  687. * The field definition.
  688. *
  689. * @return
  690. * An array of referenceable user names, keyed by user id.
  691. */
  692. function _user_reference_options($field, $flat = TRUE) {
  693. $references = user_reference_potential_references($field);
  694. $options = array();
  695. foreach ($references as $key => $value) {
  696. if (empty($value['group']) || $flat) {
  697. $options[$key] = $value['rendered'];
  698. }
  699. else {
  700. // The group name, displayed in selects, cannot contain tags, and should
  701. // have HTML entities unencoded.
  702. $group = html_entity_decode(strip_tags($value['group']), ENT_QUOTES);
  703. $options[$group][$key] = $value['rendered'];
  704. }
  705. }
  706. return $options;
  707. }
  708. /**
  709. * Retrieves an array of candidate referenceable users.
  710. *
  711. * This info is used in various places (aloowed values, autocomplete results,
  712. * input validation...). Some of them only need the uids, others nid + names,
  713. * others yet uid + names + rendered row (for display in widgets).
  714. * The array we return contains all the potentially needed information, and lets
  715. * consumers use the parts they actually need.
  716. *
  717. * @param $field
  718. * The field definition.
  719. * @param $options
  720. * An array of options to limit the scope of the returned list. The following
  721. * key/value pairs are accepted:
  722. * - string: string to filter titles on (used by autocomplete).
  723. * - match: operator to match the above string against, can be any of:
  724. * 'contains', 'equals', 'starts_with'. Defaults to 'contains'.
  725. * - ids: array of specific node ids to lookup.
  726. * - limit: maximum size of the the result set. Defaults to 0 (no limit).
  727. *
  728. * @return
  729. * An array of valid users in the form:
  730. * array(
  731. * uid => array(
  732. * 'title' => The user name,
  733. * 'rendered' => The text to display in widgets (can be HTML)
  734. * ),
  735. * ...
  736. * )
  737. */
  738. function user_reference_potential_references($field, $options = array()) {
  739. // Fill in default options.
  740. $options += array(
  741. 'string' => '',
  742. 'match' => 'contains',
  743. 'ids' => array(),
  744. 'limit' => 0,
  745. );
  746. $results = &drupal_static(__FUNCTION__, array());
  747. // Create unique id for static cache.
  748. $cid = $field['field_name'] . ':' . $options['match'] . ':'
  749. . ($options['string'] !== '' ? $options['string'] : implode('-', $options['ids']))
  750. . ':' . $options['limit'];
  751. if (!isset($results[$cid])) {
  752. $references = FALSE;
  753. if (module_exists('views') && !empty($field['settings']['view']['view_name'])) {
  754. $references = _user_reference_potential_references_views($field, $options);
  755. }
  756. if ($references === FALSE) {
  757. $references = _user_reference_potential_references_standard($field, $options);
  758. }
  759. // Store the results.
  760. $results[$cid] = !empty($references) ? $references : array();
  761. }
  762. return $results[$cid];
  763. }
  764. /**
  765. * Helper function for user_reference_potential_references().
  766. *
  767. * Case of Views-defined referenceable users.
  768. */
  769. function _user_reference_potential_references_views($field, $options) {
  770. $settings = $field['settings']['view'];
  771. $options['title_field'] = 'name';
  772. return references_potential_references_view('user', $settings['view_name'], $settings['display_name'], $settings['args'], $options);
  773. }
  774. /**
  775. * Helper function for user_reference_potential_references().
  776. *
  777. * List of referenceable users defined by user role and status.
  778. */
  779. function _user_reference_potential_references_standard($field, $options) {
  780. // Avoid useless work.
  781. $filter_roles = array_filter($field['settings']['referenceable_roles']);
  782. $filter_status = array_filter($field['settings']['referenceable_status']);
  783. if (!count($filter_status) && !count($filter_roles)) {
  784. return array();
  785. }
  786. $query = db_select('users', 'u')
  787. // Select the whole record, so that format_username() has enough
  788. // information.
  789. ->fields('u')
  790. ->addMetaData('id', ' _user_reference_potential_references_standard')
  791. ->addMetaData('field', $field)
  792. ->addMetaData('options', $options);
  793. // Enable this filter only if any statuses checked (and not both).
  794. if (count($filter_status) == 1) {
  795. $query->condition('u.status', array_keys($filter_status), 'IN');
  796. }
  797. // Skip filter when "authenticated user" choosen.
  798. if ($filter_roles && !isset($filter_roles[DRUPAL_AUTHENTICATED_RID])) {
  799. $query->join('users_roles', 'r', 'u.uid = r.uid');
  800. $query->condition('r.rid', array_keys($filter_roles), 'IN');
  801. }
  802. if ($options['string'] !== '') {
  803. switch ($options['match']) {
  804. case 'contains':
  805. $query->condition('u.name', '%' . $options['string'] . '%', 'LIKE');
  806. break;
  807. case 'starts_with':
  808. $query->condition('u.name', $options['string'] . '%', 'LIKE');
  809. break;
  810. case 'equals':
  811. default: // no match type or incorrect match type: use "="
  812. $query->condition('u.name', $options['string'], '=');
  813. break;
  814. }
  815. }
  816. if ($options['ids']) {
  817. $query->condition('u.uid', $options['ids'], 'IN');
  818. }
  819. // Explicitly exclude the anonymous user.
  820. $query->condition('u.uid', 0, '<>');
  821. if ($options['limit']) {
  822. $query->range(0, $options['limit']);
  823. }
  824. $query->orderBy('u.name');
  825. $result = $query->execute()->fetchAll();
  826. $references = array();
  827. foreach ($result as $account) {
  828. $references[$account->uid] = array(
  829. 'title' => $account->name,
  830. 'rendered' => check_plain(format_username($account)),
  831. );
  832. }
  833. return $references;
  834. }
  835. /**
  836. * Menu callback; Retrieve a pipe delimited string of autocomplete suggestions for existing users
  837. */
  838. function user_reference_autocomplete($entity_type, $bundle, $field_name, $string = '') {
  839. $field = field_info_field($field_name);
  840. $instance = field_info_instance($entity_type, $field_name, $bundle);
  841. $options = array(
  842. 'string' => $string,
  843. 'match' => $instance['widget']['settings']['autocomplete_match'],
  844. 'limit' => 10,
  845. );
  846. $references = user_reference_potential_references($field, $options);
  847. $matches = array();
  848. foreach ($references as $id => $row) {
  849. // Markup is fine in autocompletion results (might happen when rendered
  850. // through Views) but we want to remove hyperlinks.
  851. $suggestion = preg_replace('/<a href="([^<]*)">([^<]*)<\/a>/', '$2', $row['rendered']);
  852. // Remove link tags Add a class wrapper for a few required CSS overrides.
  853. $matches[$row['title'] . " [uid:$id]"] = '<div class="reference-autocomplete">' . $suggestion . '</div>';
  854. }
  855. drupal_json_output($matches);
  856. }
  857. /**
  858. * Implements hook_options_list().
  859. */
  860. function user_reference_options_list($field) {
  861. return _user_reference_options($field, FALSE);
  862. }
  863. /**
  864. * Implementation of hook_user_load().
  865. */
  866. /*function user_reference_user_load($accounts) {
  867. // Only add links if we are on the user 'view' page.
  868. if (arg(0) != 'user' || arg(2)) {
  869. return;
  870. }
  871. foreach ($accounts as $uid => $account) {
  872. // find CCK user_reference field tables
  873. // search through them for matching user ids and load those nodes
  874. $additions = array();
  875. $fields = field_info_instances('user');
  876. // TODO : replace with field_attach_query() + synchronize with latest D6 code.
  877. // Find the table and columns to search through, if the same
  878. // table comes up in more than one field type, we only need
  879. // to search it once.
  880. $search_tables = array();
  881. $search_links = array();
  882. foreach ($fields as $field) {
  883. if ($field['type'] == 'user_reference' && !empty($field['widget']['reverse_link'])) {
  884. $db_info = content_database_info($field);
  885. $search_tables[$db_info['table']] = $db_info['columns']['uid']['column'];
  886. $search_links[$db_info['table']] = $field['widget']['reverse_link'];
  887. }
  888. }
  889. foreach ($search_tables as $table => $column) {
  890. $ids = db_query(db_rewrite_sql("SELECT DISTINCT(n.nid) FROM {node} n LEFT JOIN {". $table ."} f ON n.vid = f.vid WHERE f.". $column ."=". $account->uid. " AND n.status = 1"));
  891. while ($data = db_fetch_object($ids)) {
  892. // TODO, do we really want a complete node_load() here? We only need the title to create a link.
  893. $node = node_load($data->nid);
  894. $node->reverse_link = $search_links[$table];
  895. $additions[$node->type][] = $node;
  896. }
  897. }
  898. $accounts[$uid]->user_reference = $additions;
  899. }
  900. return;
  901. }*/
  902. /**
  903. * Implementation of hook_user_view().
  904. */
  905. /*function user_reference_user_view($account, $view_mode, $langcode) {
  906. if (!empty($account->user_reference)) {
  907. $node_types = content_types();
  908. $additions = array();
  909. $values = array();
  910. foreach ($account->user_reference as $node_type => $nodes) {
  911. foreach ($nodes as $node) {
  912. if ($node->reverse_link) {
  913. $values[$node_type][] = l($node->title, 'node/' . $node->nid);
  914. }
  915. }
  916. if (isset($values[$node_type])) {
  917. $additions[] = array(
  918. '#type' => 'user_profile_item',
  919. '#title' => check_plain($node_types[$node_type]['name']),
  920. '#value' => theme('item_list', $values[$node_type]),
  921. );
  922. }
  923. }
  924. if ($additions) {
  925. $account->content['user_reference'] = $additions + array(
  926. '#type' => 'user_profile_category',
  927. '#attributes' => array('class' => array('user-member')),
  928. '#title' => t('Related content'),
  929. '#weight' => 10,
  930. );
  931. }
  932. }
  933. }*/
  934. /**
  935. * Implements hook_content_migrate_field_alter().
  936. *
  937. * Use this to tweak the conversion of field settings
  938. * from the D6 style to the D7 style for specific
  939. * situations not handled by basic conversion,
  940. * as when field types or settings are changed.
  941. *
  942. * $field_value['widget_type'] is available to
  943. * see what widget type was originally used.
  944. */
  945. function user_reference_content_migrate_field_alter(&$field_value, $instance_value) {
  946. switch ($field_value['module']) {
  947. case 'userreference':
  948. $field_value['module'] = 'user_reference';
  949. $field_value['type'] = 'user_reference';
  950. // Translate 'view' settings.
  951. $view_name = isset($field_value['settings']['advanced_view']) ? $field_value['settings']['advanced_view'] : '';
  952. $view_args = isset($field_value['settings']['advanced_view_args']) ? $field_value['settings']['advanced_view_args'] : '';
  953. $view_args = array_map('trim', explode(',', $view_args));
  954. $field_value['settings']['view'] = array(
  955. 'view_name' => $view_name,
  956. 'display_name' => 'default',
  957. 'args' => $view_args,
  958. );
  959. if ($view_name) {
  960. $field_value['messages'][] = t("The field uses the view @view_name to determine referenceable users. You will need to manually edit the view and add a display of type 'References'.");
  961. }
  962. unset($field_value['settings']['advanced_view']);
  963. unset($field_value['settings']['advanced_view_args']);
  964. break;
  965. }
  966. }
  967. /**
  968. * Implements hook_content_migrate_instance_alter().
  969. *
  970. * Use this to tweak the conversion of instance or widget settings
  971. * from the D6 style to the D7 style for specific
  972. * situations not handled by basic conversion, as when
  973. * formatter or widget names or settings are changed.
  974. */
  975. function user_reference_content_migrate_instance_alter(&$instance_value, $field_value) {
  976. // The module name for the instance was corrected
  977. // by the change in user_reference_content_migrate_field_alter().
  978. switch ($field_value['type']) {
  979. case 'userreference':
  980. // The formatter names changed, all are prefixed
  981. // with 'user_reference_'.
  982. foreach ($instance_value['display'] as $context => $settings) {
  983. $instance_value['display'][$context]['type'] = 'user_reference_' . $settings['type'];
  984. }
  985. // Massage the widget.
  986. switch ($instance_value['widget']['type']) {
  987. case 'userreference_autocomplete':
  988. $instance_value['widget']['type'] = 'user_reference_autocomplete';
  989. $instance_value['widget']['module'] = 'user_reference';
  990. break;
  991. case 'userreference_select':
  992. $instance_value['widget']['type'] = 'options_select';
  993. $instance_value['widget']['module'] = 'options';
  994. break;
  995. case 'userreference_buttons':
  996. $instance_value['widget']['type'] = 'options_buttons';
  997. $instance_value['widget']['module'] = 'options';
  998. }
  999. break;
  1000. }
  1001. }
  1002. /**
  1003. * Implements hook_field_views_data().
  1004. *
  1005. * In addition to the default field information we add the relationship for
  1006. * views to connect back to the users table.
  1007. */
  1008. function user_reference_field_views_data($field) {
  1009. // No module_load_include(): this hook is invoked from
  1010. // views/modules/field.views.inc, which is where that function is defined.
  1011. $data = field_views_field_default_views_data($field);
  1012. $storage = $field['storage']['details']['sql'];
  1013. foreach ($storage as $age => $table_data) {
  1014. $table = key($table_data);
  1015. $columns = current($table_data);
  1016. $id_column = $columns['uid'];
  1017. if (isset($data[$table])) {
  1018. // Filter: swap the handler to the 'in' operator. The callback receives
  1019. // the field name instead of the whole $field structure to keep views
  1020. // data to a reasonable size.
  1021. $data[$table][$id_column]['filter']['handler'] = 'views_handler_filter_in_operator';
  1022. $data[$table][$id_column]['filter']['options callback'] = 'user_reference_views_filter_options';
  1023. $data[$table][$id_column]['filter']['options arguments'] = array($field['field_name']);
  1024. // Argument: display users.name in argument titles (handled in our custom
  1025. // handler) and summary lists (handled by the base views_handler_argument
  1026. // handler).
  1027. // Both mechanisms rely on the 'name table' and 'name field' information
  1028. // below, by joining to a separate copy of the base table from the field
  1029. // data table.
  1030. $data[$table][$id_column]['argument']['handler'] = 'references_handler_argument';
  1031. $data[$table][$id_column]['argument']['name table'] = $table . '_reference';
  1032. $data[$table][$id_column]['argument']['name field'] = 'name';
  1033. $data[$table . '_reference']['table']['join'][$table] = array(
  1034. 'left_field' => $id_column,
  1035. 'table' => 'users',
  1036. 'field' => 'uid',
  1037. );
  1038. // Relationship.
  1039. $data[$table][$id_column]['relationship'] = array(
  1040. 'handler' => 'references_handler_relationship',
  1041. 'base' => 'users',
  1042. 'base field' => 'uid',
  1043. 'field' => $id_column,
  1044. 'label' => $field['field_name'],
  1045. 'field_name' => $field['field_name'],
  1046. );
  1047. }
  1048. }
  1049. return $data;
  1050. }
  1051. /**
  1052. * Implements hook_field_views_data_views_data_alter().
  1053. */
  1054. function user_reference_field_views_data_views_data_alter(&$data, $field) {
  1055. foreach ($field['bundles'] as $entity_type => $bundles) {
  1056. $entity_info = entity_get_info($entity_type);
  1057. $pseudo_field_name = 'reverse_' . $field['field_name'] . '_' . $entity_type;
  1058. list($label, $all_labels) = field_views_field_label($field['field_name']);
  1059. $entity = $entity_info['label'];
  1060. if ($entity == t('Node')) {
  1061. $entity = t('Content');
  1062. }
  1063. // Only specify target entity type if the field is used in more than one.
  1064. if (count($field['bundles']) > 1) {
  1065. $title = t('@field (@field_name) - reverse (to @entity)', array('@entity' => $entity, '@field' => $label, '@field_name' => $field['field_name']));
  1066. }
  1067. else {
  1068. $title = t('@field (@field_name) - reverse', array('@entity' => $entity, '@field' => $label, '@field_name' => $field['field_name']));
  1069. }
  1070. $data['users'][$pseudo_field_name]['relationship'] = array(
  1071. 'title' => $title,
  1072. 'help' => t('Relate each @entity referencing the user through @field.', array('@entity' => $entity, '@field' => $label)),
  1073. 'handler' => 'views_handler_relationship_entity_reverse',
  1074. 'field_name' => $field['field_name'],
  1075. 'field table' => _field_sql_storage_tablename($field),
  1076. 'field field' => $field['field_name'] . '_uid',
  1077. 'base' => $entity_info['base table'],
  1078. 'base field' => $entity_info['entity keys']['id'],
  1079. 'label' => t('!field_name', array('!field_name' => $field['field_name'])),
  1080. );
  1081. }
  1082. }
  1083. /**
  1084. * Helper callback for the views_handler_filter_in_operator filter.
  1085. *
  1086. * @param $field_name
  1087. * The field name.
  1088. *
  1089. * @return
  1090. * The array of allowed options for the filter.
  1091. */
  1092. function user_reference_views_filter_options($field_name) {
  1093. $options = array();
  1094. if ($field = field_info_field($field_name)) {
  1095. $options = _user_reference_options($field, TRUE);
  1096. // The options will be used as is in checkboxes, and thus need to be
  1097. // sanitized first.
  1098. foreach ($options as $key => $value) {
  1099. $options[$key] = field_filter_xss($value);
  1100. }
  1101. }
  1102. return $options;
  1103. }