date_repeat.module 14 KB

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