date_repeat.module 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  1. <?php
  2. /**
  3. * @file
  4. * This module creates a form element that allows users to select
  5. * repeat rules for a date, and reworks the result into an iCal
  6. * RRULE string that can be stored in the database.
  7. *
  8. * The module also parses iCal RRULEs to create an array of dates
  9. * that meet their criteria.
  10. *
  11. * Other modules can use this API to add self-validating form elements
  12. * to their dates, and identify dates that meet the RRULE criteria.
  13. */
  14. /**
  15. * Implements hook_element_info().
  16. */
  17. function date_repeat_element_info() {
  18. $type['date_repeat_rrule'] = array(
  19. '#input' => TRUE,
  20. '#process' => array('date_repeat_rrule_process'),
  21. '#element_validate' => array('date_repeat_rrule_validate'),
  22. '#theme_wrappers' => array('date_repeat_rrule'),
  23. );
  24. $type['date_repeat_form_element_radios'] = array(
  25. '#input' => TRUE,
  26. '#process' => array('date_repeat_form_element_radios_process'),
  27. '#theme_wrappers' => array('radios'),
  28. '#pre_render' => array('form_pre_render_conditional_form_element'),
  29. );
  30. if (module_exists('ctools')) {
  31. $type['date_repeat_rrule']['#pre_render'] = array('ctools_dependent_pre_render');
  32. }
  33. return $type;
  34. }
  35. /**
  36. * Implements hook_theme().
  37. */
  38. function date_repeat_theme() {
  39. return array(
  40. 'date_repeat_current_exceptions' => array('render element' => 'element'),
  41. 'date_repeat_current_additions' => array('render element' => 'element'),
  42. 'date_repeat_rrule' => array('render element' => 'element'),
  43. );
  44. }
  45. /**
  46. * Helper function for FREQ options.
  47. */
  48. function date_repeat_freq_options() {
  49. return array(
  50. 'DAILY' => t('Daily', array(), array('context' => 'datetime_singular')),
  51. 'WEEKLY' => t('Weekly', array(), array('context' => 'datetime_singular')),
  52. 'MONTHLY' => t('Monthly', array(), array('context' => 'datetime_singular')),
  53. 'YEARLY' => t('Yearly', array(), array('context' => 'datetime_singular')),
  54. );
  55. }
  56. /**
  57. * Helper function for interval options.
  58. */
  59. function date_repeat_interval_options() {
  60. $options = range(0, 366);
  61. unset($options[0]);
  62. return $options;
  63. }
  64. /**
  65. * Helper function for FREQ options.
  66. *
  67. * Translated and untranslated arrays of the iCal day of week names.
  68. * We need the untranslated values for date_modify(), translated
  69. * values when displayed to user.
  70. */
  71. function date_repeat_dow_day_options($translated = TRUE) {
  72. return array(
  73. 'SU' => $translated ? t('Sunday', array(), array('context' => 'day_name')) : 'Sunday',
  74. 'MO' => $translated ? t('Monday', array(), array('context' => 'day_name')) : 'Monday',
  75. 'TU' => $translated ? t('Tuesday', array(), array('context' => 'day_name')) : 'Tuesday',
  76. 'WE' => $translated ? t('Wednesday', array(), array('context' => 'day_name')) : 'Wednesday',
  77. 'TH' => $translated ? t('Thursday', array(), array('context' => 'day_name')) : 'Thursday',
  78. 'FR' => $translated ? t('Friday', array(), array('context' => 'day_name')) : 'Friday',
  79. 'SA' => $translated ? t('Saturday', array(), array('context' => 'day_name')) : 'Saturday',
  80. );
  81. }
  82. /**
  83. * Helper function for FREQ options.
  84. *
  85. * Translated and untranslated arrays of the iCal abbreviated day of week names.
  86. */
  87. function date_repeat_dow_day_options_abbr($translated = TRUE, $length = 3) {
  88. $return = array();
  89. switch ($length) {
  90. case 1:
  91. $context = 'day_abbr1';
  92. break;
  93. case 2:
  94. $context = 'day_abbr2';
  95. break;
  96. default:
  97. $context = '';
  98. break;
  99. }
  100. foreach (date_repeat_dow_day_untranslated() as $key => $day) {
  101. $return[$key] = $translated ? t(substr($day, 0, $length), array(), array('context' => $context)) : substr($day, 0, $length);
  102. }
  103. return $return;
  104. }
  105. /**
  106. * Helper function for weekdays translated.
  107. */
  108. function date_repeat_dow_day_untranslated() {
  109. static $date_repeat_weekdays;
  110. if (empty($date_repeat_weekdays)) {
  111. $date_repeat_weekdays = array(
  112. 'SU' => 'Sunday',
  113. 'MO' => 'Monday',
  114. 'TU' => 'Tuesday',
  115. 'WE' => 'Wednesday',
  116. 'TH' => 'Thursday',
  117. 'FR' => 'Friday',
  118. 'SA' => 'Saturday'
  119. );
  120. }
  121. return $date_repeat_weekdays;
  122. }
  123. /**
  124. * Helper function for weekdays order.
  125. */
  126. function date_repeat_dow_day_options_ordered($weekdays) {
  127. $day_keys = array_keys($weekdays);
  128. $day_values = array_values($weekdays);
  129. for ($i = 1; $i <= variable_get('date_first_day', 0); $i++) {
  130. $last_key = array_shift($day_keys);
  131. array_push($day_keys, $last_key);
  132. $last_value = array_shift($day_values);
  133. array_push($day_values, $last_value);
  134. }
  135. $weekdays = array_combine($day_keys, $day_values);
  136. return $weekdays;
  137. }
  138. /**
  139. * Helper function for BYDAY options.
  140. */
  141. function date_repeat_dow_count_options() {
  142. return array('' => t('Every', array(), array('context' => 'date_order'))) + date_order_translated();
  143. }
  144. /**
  145. * Helper function for BYDAY options.
  146. *
  147. * Creates options like -1SU and 2TU
  148. */
  149. function date_repeat_dow_options() {
  150. $options = array();
  151. foreach (date_repeat_dow_count_options() as $count_key => $count_value) {
  152. foreach (date_repeat_dow_day_options() as $dow_key => $dow_value) {
  153. $options[$count_key . $dow_key] = $count_value . ' ' . $dow_value;
  154. }
  155. }
  156. return $options;
  157. }
  158. /**
  159. * Translate a day of week position to the iCal day name.
  160. *
  161. * Used with date_format($date, 'w') or get_variable('date_first_day'),
  162. * which return 0 for Sunday, 1 for Monday, etc.
  163. *
  164. * dow 2 becomes 'TU', dow 3 becomes 'WE', and so on.
  165. */
  166. function date_repeat_dow2day($dow) {
  167. $days_of_week = array_keys(date_repeat_dow_day_options(FALSE));
  168. return $days_of_week[$dow];
  169. }
  170. /**
  171. * Shift the array of iCal day names into the right order for a specific week start day.
  172. */
  173. function date_repeat_days_ordered($week_start_day) {
  174. $days = array_flip(array_keys(date_repeat_dow_day_options(FALSE)));
  175. $start_position = $days[$week_start_day];
  176. $keys = array_flip($days);
  177. if ($start_position > 0) {
  178. for ($i = 1; $i <= $start_position; $i++) {
  179. $last = array_shift($keys);
  180. array_push($keys, $last);
  181. }
  182. }
  183. return $keys;
  184. }
  185. /**
  186. * Build a description of an iCal rule.
  187. *
  188. * Constructs a human-readable description of the rule.
  189. */
  190. function date_repeat_rrule_description($rrule, $format = 'D M d Y') {
  191. // Empty or invalid value.
  192. if (empty($rrule) || !strstr($rrule, 'RRULE')) {
  193. return;
  194. }
  195. module_load_include('inc', 'date_api', 'date_api_ical');
  196. module_load_include('inc', 'date_repeat', 'date_repeat_calc');
  197. $parts = date_repeat_split_rrule($rrule);
  198. $additions = $parts[2];
  199. $exceptions = $parts[1];
  200. $rrule = $parts[0];
  201. if ($rrule['FREQ'] == 'NONE') {
  202. return;
  203. }
  204. // Make sure there will be an empty description for any unused parts.
  205. $description = array(
  206. '!interval' => '',
  207. '!byday' => '',
  208. '!bymonth' => '',
  209. '!count' => '',
  210. '!until' => '',
  211. '!except' => '',
  212. '!additional' => '',
  213. '!week_starts_on' => '',
  214. );
  215. $interval = date_repeat_interval_options();
  216. switch ($rrule['FREQ']) {
  217. case 'WEEKLY':
  218. $description['!interval'] = format_plural($rrule['INTERVAL'], 'every week', 'every @count weeks') . ' ';
  219. break;
  220. case 'MONTHLY':
  221. $description['!interval'] = format_plural($rrule['INTERVAL'], 'every month', 'every @count months') . ' ';
  222. break;
  223. case 'YEARLY':
  224. $description['!interval'] = format_plural($rrule['INTERVAL'], 'every year', 'every @count years') . ' ';
  225. break;
  226. default:
  227. $description['!interval'] = format_plural($rrule['INTERVAL'], 'every day', 'every @count days') . ' ';
  228. break;
  229. }
  230. if (!empty($rrule['BYDAY'])) {
  231. $days = date_repeat_dow_day_options();
  232. $counts = date_repeat_dow_count_options();
  233. $results = array();
  234. foreach ($rrule['BYDAY'] as $byday) {
  235. // Get the numeric part of the BYDAY option, i.e. +3 from +3MO.
  236. $day = substr($byday, -2);
  237. $count = str_replace($day, '', $byday);
  238. if (!empty($count)) {
  239. // See if there is a 'pretty' option for this count, i.e. +1 => First.
  240. $order = array_key_exists($count, $counts) ? strtolower($counts[$count]) : $count;
  241. $results[] = trim(t('!repeats_every_interval on the !date_order !day_of_week',
  242. array(
  243. '!repeats_every_interval ' => '',
  244. '!date_order' => $order,
  245. '!day_of_week' => $days[$day]
  246. )));
  247. }
  248. else {
  249. $results[] = trim(t('!repeats_every_interval every !day_of_week',
  250. array('!repeats_every_interval ' => '', '!day_of_week' => $days[$day])));
  251. }
  252. }
  253. $description['!byday'] = implode(' ' . t('and') . ' ', $results);
  254. }
  255. if (!empty($rrule['BYMONTH'])) {
  256. if (count($rrule['BYMONTH']) < 12) {
  257. $results = array();
  258. $months = date_month_names();
  259. foreach ($rrule['BYMONTH'] as $month) {
  260. $results[] = $months[$month];
  261. }
  262. if (!empty($rrule['BYMONTHDAY'])) {
  263. $description['!bymonth'] = trim(t('!repeats_every_interval on the !month_days of !month_names',
  264. array(
  265. '!repeats_every_interval ' => '',
  266. '!month_days' => implode(', ', $rrule['BYMONTHDAY']),
  267. '!month_names' => implode(', ', $results)
  268. )));
  269. }
  270. else {
  271. $description['!bymonth'] = trim(t('!repeats_every_interval on !month_names',
  272. array(
  273. '!repeats_every_interval ' => '',
  274. '!month_names' => implode(', ', $results)
  275. )));
  276. }
  277. }
  278. }
  279. if ($rrule['INTERVAL'] < 1) {
  280. $rrule['INTERVAL'] = 1;
  281. }
  282. if (!empty($rrule['COUNT'])) {
  283. $description['!count'] = trim(t('!repeats_every_interval !count times',
  284. array('!repeats_every_interval ' => '', '!count' => $rrule['COUNT'])));
  285. }
  286. if (!empty($rrule['UNTIL'])) {
  287. $until = date_ical_date($rrule['UNTIL'], 'UTC');
  288. date_timezone_set($until, date_default_timezone_object());
  289. $description['!until'] = trim(t('!repeats_every_interval until !until_date',
  290. array(
  291. '!repeats_every_interval ' => '',
  292. '!until_date' => date_format_date($until, 'custom', $format)
  293. )));
  294. }
  295. if ($exceptions) {
  296. $values = array();
  297. foreach ($exceptions as $exception) {
  298. $except = date_ical_date($exception, 'UTC');
  299. date_timezone_set($except, date_default_timezone_object());
  300. $values[] = date_format_date($except, 'custom', $format);
  301. }
  302. $description['!except'] = trim(t('!repeats_every_interval except !except_dates',
  303. array(
  304. '!repeats_every_interval ' => '',
  305. '!except_dates' => implode(', ', $values)
  306. )));
  307. }
  308. if (!empty($rrule['WKST'])) {
  309. $day_names = date_repeat_dow_day_options();
  310. $description['!week_starts_on'] = trim(t('!repeats_every_interval where the week start on !day_of_week',
  311. array('!repeats_every_interval ' => '', '!day_of_week' => $day_names[trim($rrule['WKST'])])));
  312. }
  313. if ($additions) {
  314. $values = array();
  315. foreach ($additions as $addition) {
  316. $add = date_ical_date($addition, 'UTC');
  317. date_timezone_set($add, date_default_timezone_object());
  318. $values[] = date_format_date($add, 'custom', $format);
  319. }
  320. $description['!additional'] = trim(t('Also includes !additional_dates.',
  321. array('!additional_dates' => implode(', ', $values))));
  322. }
  323. $output = t('Repeats !interval !bymonth !byday !count !until !except. !additional', $description);
  324. // Removes double whitespaces from Repeat tile.
  325. $output = preg_replace('/\s+/', ' ', $output);
  326. // Removes whitespace before full stop ".", at the end of the title.
  327. $output = str_replace(' .', '.', $output);
  328. return $output;
  329. }
  330. /**
  331. * Parse an iCal rule into a parsed RRULE array and an EXDATE array.
  332. */
  333. function date_repeat_split_rrule($rrule) {
  334. $parts = explode("\n", str_replace("\r\n", "\n", $rrule));
  335. $rrule = array();
  336. $exceptions = array();
  337. $additions = array();
  338. $additions = array();
  339. foreach ($parts as $part) {
  340. if (strstr($part, 'RRULE')) {
  341. $cleanded_part = str_replace('RRULE:', '', $part);
  342. $rrule = (array) date_ical_parse_rrule('RRULE:', $cleanded_part);
  343. }
  344. elseif (strstr($part, 'EXDATE')) {
  345. $exdate = str_replace('EXDATE:', '', $part);
  346. $exceptions = (array) date_ical_parse_exceptions('EXDATE:', $exdate);
  347. unset($exceptions['DATA']);
  348. }
  349. elseif (strstr($part, 'RDATE')) {
  350. $rdate = str_replace('RDATE:', '', $part);
  351. $additions = (array) date_ical_parse_exceptions('RDATE:', $rdate);
  352. unset($additions['DATA']);
  353. }
  354. }
  355. return array($rrule, $exceptions, $additions);
  356. }
  357. /**
  358. * Analyze a RRULE and return dates that match it.
  359. */
  360. function date_repeat_calc($rrule, $start, $end, $exceptions = array(), $timezone = NULL, $additions = array()) {
  361. module_load_include('inc', 'date_repeat', 'date_repeat_calc');
  362. return _date_repeat_calc($rrule, $start, $end, $exceptions, $timezone, $additions);
  363. }
  364. /**
  365. * Generate the repeat rule setting form.
  366. */
  367. function date_repeat_rrule_process($element, &$form_state, $form) {
  368. module_load_include('inc', 'date_repeat', 'date_repeat_form');
  369. return _date_repeat_rrule_process($element, $form_state, $form);
  370. }
  371. /**
  372. * Process function for 'date_repeat_form_element_radios'.
  373. */
  374. function date_repeat_form_element_radios_process($element) {
  375. $childrenkeys = element_children($element);
  376. if (count($element['#options']) &&
  377. count($element['#options']) == count($childrenkeys)) {
  378. $weight = 0;
  379. $children = array();
  380. $classes = isset($element['#div_classes']) ?
  381. $element['#div_classes'] : array();
  382. foreach ($childrenkeys as $childkey) {
  383. $children[$childkey] = $element[$childkey];
  384. unset($element[$childkey]);
  385. }
  386. foreach ($element['#options'] as $key => $choice) {
  387. $currentchildkey = array_shift($childrenkeys);
  388. $weight += 0.001;
  389. $class = array_shift($classes);
  390. $element += array($key => array());
  391. $parents_for_id = array_merge($element['#parents'], array($key));
  392. $element[$key] += array(
  393. '#prefix' => '<div' . ($class ? " class=\"{$class}\"" : '') . '>',
  394. '#type' => 'radio',
  395. '#title' => $choice,
  396. '#title_display' => 'invisible',
  397. '#return_value' => $key,
  398. '#default_value' => isset($element['#default_value']) ?
  399. $element['#default_value'] : NULL,
  400. '#attributes' => $element['#attributes'],
  401. '#parents' => $element['#parents'],
  402. '#id' => drupal_html_id('edit-' . implode('-', $parents_for_id)),
  403. '#ajax' => isset($element['#ajax']) ? $element['ajax'] : NULL,
  404. '#weight' => $weight,
  405. '#theme_wrappers' => array(),
  406. '#suffix' => ' ',
  407. );
  408. $child = $children[$currentchildkey];
  409. $weight += 0.001;
  410. $child['#weight'] = $weight;
  411. $child['#title_display'] = 'invisible';
  412. $child['#suffix'] = (!empty($child['#suffix']) ? $child['#suffix'] : '') .
  413. '</div>';
  414. $child['#parents'] = $element['#parents'];
  415. array_pop($child['#parents']);
  416. array_push($child['#parents'], $currentchildkey);
  417. $element_prototype = element_info($child['#type']);
  418. $old_wrappers = array();
  419. if (isset($child['#theme_wrappers'])) {
  420. $old_wrappers += $child['#theme_wrappers'];
  421. }
  422. if (isset($element_prototype['#theme_wrappers'])) {
  423. $old_wrappers += $element_prototype['#theme_wrappers'];
  424. }
  425. $child['#theme_wrappers'] = array();
  426. foreach ($old_wrappers as $wrapper) {
  427. if ($wrapper != 'form_element') {
  428. $child['#theme_wrappers'][] = $wrapper;
  429. }
  430. }
  431. $element[$currentchildkey] = $child;
  432. }
  433. }
  434. return $element;
  435. }