├── .gitignore
├── LICENSE
├── README.md
├── RecurringDatePlugin.php
├── composer.json
├── composer.lock
├── fieldtypes
└── RecurringDate_AdvancedDateFieldType.php
├── models
└── RecurringDate_RuleModel.php
├── records
├── RecurringDate_DateRecord.php
└── RecurringDate_RuleRecord.php
├── resources
└── js
│ └── advanceddate.js
├── services
├── RecurringDateService.php
└── RecurringDate_SortService.php
├── templates
└── fields.html
└── variables
└── RecurringDateVariable.php
/.gitignore:
--------------------------------------------------------------------------------
1 | composer.phar
2 | vendor/
3 | .DS_Store
4 |
5 | # Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file
6 | # You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file
7 | # composer.lock
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 North By Northwest
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | craft-recurring-dates
2 | =====================
3 |
4 | ### I could still use help testing and bug fixing. If you find any issues put them in the Github issues. Thanks
5 |
6 | This is a plugin for the Craft CMS to add recurring dates functionality. It adds a new field type called Advanced Date which has the recurring date functions. It stores the recurring dates and an rule in the data base and returns an array of dates to the twig templates. I'm using the [Recurr library](https://github.com/simshaun/recurr) by [simshaun](https://github.com/simshaun) to build and parse the rrule.
7 |
8 | ###Installation
9 |
10 | 1. Clone this project into `your_craft_dir/craft/plugins/recurringdate`
11 | 2. Install Composer from [getcomposer.org](https://getcomposer.org/doc/00-intro.md#installation-nix)
12 | 3. Run `composer install` or `php composer.phar install` in the `recurringdate` directory to install dependencies
13 | 4. Install the plugin through the Craft admin panel
14 |
15 | ###Usage
16 |
17 | * Add a new Advanced Date Field type to one of your sections
18 | * Output is either an array of dates and the entries associated with them or an array of the values used to set the date rule. Output depends on how you query the dates
19 | * Multiple Dates Query can be used to automatically sort and filter corresponding dates. I built this with an event calendar in mind, that would need recurring dates.
20 |
21 | ####Example Output Usage - Multiple Dates Query
22 |
23 | ```
24 | {% set query = craft.entries.section('events').relatedTo(craft.categories.slug('your-slug')) %}
25 | {% set events = craft.recurringdate.dates('yourFieldHandle',
26 | {
27 | 'limit': 3,
28 | 'before': '12/22/2014',
29 | 'after': 'now +1 months',
30 | 'criteria': query
31 | })
32 | %}
33 |
34 | {% for event in events %}
35 | {{ event.date.start|date("n/j/Y") }}{{ event.date.end ? ' -- ' ~ event.date.end|date("n/j/Y") }}
36 | {% endfor %}
37 | ```
38 |
39 | ####Arguments for `craft.recurringdate.dates`
40 | * Field Handle - Handle of your field in the Craft CP
41 |
42 | Values in the options array - see above example for usage
43 | * `limit` - limit the number of entries returned
44 | * `order` - 'ASC' or 'DESC' - defaults to 'ASC'
45 | * `group` - null, 'day', 'month', or 'year' - See Grouping Info Below
46 | * `before` - null, or Date string accepted by PHP's [strtotime function](http://www.php.net/manual/en/datetime.formats.php)
47 | * `after` - null, or Date string accepted by PHP's strtotime function
48 | * `criteria` - ElementCriteriaModel returned by a craft entry query
49 | * `excludes` - if excluded dates should be respected - defaults to true
50 |
51 | ####Properties available for output for Multiple Dates Query
52 | * `date` - Array containing all the date info for the recurring date
53 | * `id` - Id of the date, used to query the correct date in a sequence
54 | * `elementId` - Id of the Craft entry associated with this date
55 | * `start` - A string containing the start date
56 | * `end` - A string containing the end date, if set
57 | * `start_time` - A string containing the start time, if set
58 | * `end_time` - A string containing the end time, if set
59 | * `allday` - If the date lasts all day
60 | * `repeats` - If the date repeats
61 | * `rrule` - The RRULE string used to build the date
62 | * `entry` - The entry associated with this date
63 |
64 | ####Info about using Group for Multiple Dates Query
65 | The Group query will return a different structure than a regular query. It's structure looks like this
66 | ```
67 | array(
68 | 'grouped_date_string' = array(
69 | array('date', 'entry'),
70 | array('date', 'entry'),
71 | ...
72 | ),
73 | 'grouped_date_string2' = array(
74 | array('date', 'entry'),
75 | array('date', 'entry')
76 | ),
77 | ...
78 | )
79 | ```
80 |
81 | ####Example Output Usage - Single Date Query using Date ID
82 | ```
83 | {% set row = craft.recurringdate.date(id) %}
84 | {% set date = row.date %}
85 | {% set entry = row.entry %}
86 | {{ entry.title }}
87 | {{ date.start_date|date("n/j/Y H:i:s") }}{{ date.end_date is defined ? ' -- ' ~ date.end_date|date("n/j/Y H:i:s") }}
88 | ```
89 |
90 | ####Properties available for output for Single Date Query
91 | The Single Date Query will only return one array that contains the `date` and the `entry` of the row with the specified ID. The idea is that you would use the multiple date query to list the date's ID and then you can select which specific date you want to display based on that ID.
92 |
93 |
94 | ####Example Output Usage - Single Entry
95 | ```
96 | {% set date = entry.advanced_date_field_name %}
97 | {{ date.start_date|date("n/j/Y H:i:s") }}{{ date.end_date is defined ? ' -- ' ~ date.end_date|date("n/j/Y H:i:s") }}
98 | ```
99 |
100 | ####Properties available for output for each Advanced Date Field - Single Entry
101 | * `start_date` - Craft DateTime Object - Start date set for the event
102 | * `start_time` - Craft DateTime Object - Same as startdate
103 | * `end_date` - Craft DateTime Object - End date set for the event, can be null if not set
104 | * `end_time` - Craft DateTime Object - Same as enddate
105 | * `allday` - Boolean - If the event is an allday event
106 | * `repeats` - Boolean - If the event repeats
107 | * `frequency` - String - Recurrence frequency, 'daily', 'weekly', 'monthly', 'yearly'
108 | * `interval` - Int - Recurrence Interval, integer from 1-31
109 | * `weekdays` - Array - days of the week that the event occurs, undefined if interval not weekly, 'SU', 'MO', 'TU', etc...
110 | * `repeat_by` - String - by day of week or day of month, undefined if interval not monthly, 'week', 'month'
111 | * `ends` - String - how the event ends, 'never', 'after', 'until'
112 | * `count` - Int - How many times this occurs, undefined if `ends` is not 'after'
113 | * `until` - Craft DateTime Object - Date event goes until, undefined if `ends` is not 'until'
114 | * `rrule` - RRULE string used to build the rule
115 |
--------------------------------------------------------------------------------
/RecurringDatePlugin.php:
--------------------------------------------------------------------------------
1 | on('content.onSaveContent', function(Event $event) {
14 | craft()->recurringDate->contentSaved($event->params['content'], $event->params['isNewContent']);
15 | });
16 | }
17 |
18 | function getName()
19 | {
20 | return Craft::t('Recurring Dates');
21 | }
22 |
23 | function getVersion()
24 | {
25 | return '0.3';
26 | }
27 |
28 | function getDeveloper()
29 | {
30 | return 'NXNW';
31 | }
32 |
33 | function getDeveloperUrl()
34 | {
35 | return 'http://nxnw.net';
36 | }
37 | }
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "require": {
3 | "simshaun/recurr": "dev-master"
4 | }
5 | }
--------------------------------------------------------------------------------
/composer.lock:
--------------------------------------------------------------------------------
1 | {
2 | "_readme": [
3 | "This file locks the dependencies of your project to a known state",
4 | "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file"
5 | ],
6 | "hash": "8810619048498dd6127f84a98ea7c44c",
7 | "packages": [
8 | {
9 | "name": "simshaun/recurr",
10 | "version": "dev-master",
11 | "source": {
12 | "type": "git",
13 | "url": "https://github.com/simshaun/recurr.git",
14 | "reference": "c90027564151953e487d9e0de3662b0c0b1e9fd1"
15 | },
16 | "dist": {
17 | "type": "zip",
18 | "url": "https://api.github.com/repos/simshaun/recurr/zipball/c90027564151953e487d9e0de3662b0c0b1e9fd1",
19 | "reference": "c90027564151953e487d9e0de3662b0c0b1e9fd1",
20 | "shasum": ""
21 | },
22 | "require": {
23 | "php": ">=5.3.0"
24 | },
25 | "require-dev": {
26 | "phpunit/phpunit": "3.7.*"
27 | },
28 | "type": "library",
29 | "extra": {
30 | "branch-alias": {
31 | "dev-master": "1.0.x-dev"
32 | }
33 | },
34 | "autoload": {
35 | "psr-0": {
36 | "Recurr": "src/"
37 | }
38 | },
39 | "notification-url": "https://packagist.org/downloads/",
40 | "license": [
41 | "MIT"
42 | ],
43 | "authors": [
44 | {
45 | "name": "Shaun Simmons",
46 | "email": "shaun@envysphere.com",
47 | "homepage": "http://envysphere.com"
48 | }
49 | ],
50 | "description": "PHP library for working with recurrence rules",
51 | "homepage": "https://github.com/simshaun/recurr",
52 | "keywords": [
53 | "dates",
54 | "events",
55 | "recurrence",
56 | "recurring",
57 | "rrule"
58 | ],
59 | "time": "2014-03-16 10:02:08"
60 | }
61 | ],
62 | "packages-dev": [
63 |
64 | ],
65 | "aliases": [
66 |
67 | ],
68 | "minimum-stability": "stable",
69 | "stability-flags": {
70 | "simshaun/recurr": 20
71 | },
72 | "platform": [
73 |
74 | ],
75 | "platform-dev": [
76 |
77 | ]
78 | }
79 |
--------------------------------------------------------------------------------
/fieldtypes/RecurringDate_AdvancedDateFieldType.php:
--------------------------------------------------------------------------------
1 | templates->formatInputId($name);
27 |
28 | $namespaceId = craft()->templates->namespaceInputId($id);
29 |
30 | craft()->templates->includeJsResource('recurringdate/js/advanceddate.js');
31 |
32 | if(!empty($value)) {
33 | $ruleModel = RecurringDate_RuleModel::populateModel($value);
34 | } else {
35 | $ruleModel = new RecurringDate_RuleModel;
36 | $ruleModel->handle = $name;
37 | }
38 |
39 | $attr = $ruleModel->getAttributes();
40 |
41 | if( strpos($attr['rrule'], 'EXDATE') !== false ){
42 | $attr['exdates'] = array();
43 |
44 | $exDatesArray = explode('EXDATE=', $attr['rrule']);
45 | $exDatesString = $exDatesArray[1];
46 | $exDatesString = rtrim($exDatesString, ";");
47 | $exDatesArray = explode(',', $exDatesString);
48 |
49 | foreach ($exDatesArray as $index => $date) {
50 | $attr['exdates'][] = DateTime::createFromFormat('Ymd', $date);
51 | }
52 | }
53 | else{
54 | $attr['exdates'] = array();
55 | }
56 |
57 | $attr['namespaceId'] = $namespaceId;
58 |
59 | return craft()->templates->render('recurringdate/fields', $attr);
60 | }
61 |
62 | //Leaving the db to be displayed
63 | public function prepValue($value){
64 | return craft()->recurringDate->getRule($this);
65 | }
66 |
67 | public function validate($value){
68 | return craft()->recurringDate->validateRule($this);
69 | }
70 |
71 | // After saving element, save field to plugin table
72 | public function onAfterElementSave()
73 | {
74 | // Returns true if entry was saved
75 | return craft()->recurringDate->saveRuleField($this);
76 | }
77 | }
--------------------------------------------------------------------------------
/models/RecurringDate_RuleModel.php:
--------------------------------------------------------------------------------
1 | AttributeType::Number,
10 | 'handle' => AttributeType::String,
11 | 'start_date'=> AttributeType::String,
12 | 'start_time'=> AttributeType::String,
13 | 'end_date' => AttributeType::String,
14 | 'end_time' => AttributeType::String,
15 | 'allday' => AttributeType::Bool,
16 | 'repeats' => AttributeType::Bool,
17 | 'frequency' => array(AttributeType::Enum, 'values' => "daily, monthly, weekly, yearly"),
18 | 'interval' => AttributeType::Number,
19 | 'weekdays' => AttributeType::String,
20 | 'repeat_by' => array(AttributeType::Enum, 'values' => "week, month"),
21 | 'ends' => array(AttributeType::Enum, 'values' => "after, until"),
22 | 'count' => AttributeType::Number,
23 | 'until' => AttributeType::String,
24 | 'rrule' => AttributeType::String,
25 | );
26 | }
27 | }
--------------------------------------------------------------------------------
/records/RecurringDate_DateRecord.php:
--------------------------------------------------------------------------------
1 | array(static::BELONGS_TO, 'RecurringDate_RuleRecord', 'onDelete' => static::CASCADE),
13 | );
14 | }
15 |
16 | protected function defineAttributes(){
17 | return array(
18 | 'start' => AttributeType::DateTime,
19 | 'end' => AttributeType::DateTime
20 | );
21 | }
22 | }
--------------------------------------------------------------------------------
/records/RecurringDate_RuleRecord.php:
--------------------------------------------------------------------------------
1 | array(static::BELONGS_TO, 'ElementRecord', 'required' => true, 'onDelete' => static::CASCADE),
13 | );
14 | }
15 |
16 | protected function defineAttributes(){
17 | return array(
18 | 'handle' => AttributeType::String,
19 | 'start_date'=> AttributeType::String,
20 | 'start_time'=> AttributeType::String,
21 | 'end_date' => AttributeType::String,
22 | 'end_time' => AttributeType::String,
23 | 'allday' => AttributeType::Bool,
24 | 'repeats' => AttributeType::Bool,
25 | 'frequency' => array(AttributeType::Enum, 'values' => "daily, monthly, weekly, yearly"),
26 | 'interval' => AttributeType::Number,
27 | 'weekdays' => AttributeType::String,
28 | 'repeat_by' => array(AttributeType::Enum, 'values' => "week, month"),
29 | 'ends' => array(AttributeType::Enum, 'values' => "after, until"),
30 | 'count' => AttributeType::Number,
31 | 'until' => AttributeType::String,
32 | 'rrule' => AttributeType::String,
33 | );
34 | }
35 | }
--------------------------------------------------------------------------------
/resources/js/advanceddate.js:
--------------------------------------------------------------------------------
1 | window.advancedDate = function(id) {
2 |
3 | var advancedDateView = function(namespace) {
4 |
5 | var _namespace = namespace;
6 | var _root = $('#' + namespace + '-field');
7 |
8 | var _init = function() {
9 | _allDayToggle();
10 | _repeatToggle();
11 | _repeatInterval();
12 | _repeatEnds();
13 | _repeatNewDate();
14 | };
15 |
16 | var _allDayToggle = function() {
17 |
18 | var startTime = _root.find('.field.starttime .starttime-time');
19 | var endTime = _root.find('.field.endtime .endtime-time');
20 | var alldaySwitch = _root.find('.allday-switch .lightswitch');
21 | var alldaySwitchData;
22 |
23 | if(!alldaySwitch.data('lightswitch')){
24 | alldaySwitch.lightswitch();
25 | alldaySwitchData = alldaySwitch.data('lightswitch');
26 | }
27 | else{
28 | alldaySwitchData = alldaySwitch.data('lightswitch');
29 | }
30 |
31 | var changeHandler = function() {
32 |
33 | if (alldaySwitchData.on) {
34 |
35 | startTime.hide();
36 | endTime.hide();
37 |
38 | } else {
39 |
40 | startTime.show();
41 | endTime.show();
42 | }
43 |
44 | };
45 |
46 | alldaySwitchData.settings.onChange = changeHandler;
47 | changeHandler();
48 |
49 |
50 | };
51 |
52 | var _repeatToggle = function() {
53 |
54 | var repeatHolder = _root.find('.repeat-holder');
55 | var repeatsSwitch = _root.find('.repeats-switch .lightswitch');
56 | var repeatsSwitchData;
57 |
58 | if(!repeatsSwitch.data('lightswitch')){
59 | repeatsSwitch.lightswitch();
60 | repeatsSwitchData = repeatsSwitch.data('lightswitch');
61 | }
62 | else{
63 | repeatsSwitchData = repeatsSwitch.data('lightswitch');
64 | }
65 |
66 | var changeHandler = function() {
67 |
68 | if (repeatsSwitchData.on) {
69 | repeatHolder.show();
70 | } else {
71 | repeatHolder.hide();
72 | }
73 |
74 | };
75 |
76 | repeatsSwitchData.settings.onChange = changeHandler;
77 | changeHandler();
78 |
79 | };
80 |
81 | var _repeatInterval = function() {
82 |
83 | var repeatSelect = _root.find('#' + _namespace + 'repeat-frequency');
84 |
85 | var repeatOn = _root.find('.field.weekdays');
86 | var repeatBy = _root.find('.field.repeat_by');
87 |
88 | var repeatEveryUnit = _root.find('.repeat-every-unit');
89 |
90 | var changeHandler = function() {
91 |
92 | repeatOn.hide();
93 | repeatBy.hide();
94 |
95 | switch (repeatSelect.val()) {
96 | case "daily":
97 | repeatEveryUnit.html('days');
98 | break;
99 | case "weekly":
100 | repeatEveryUnit.html('weeks');
101 | break;
102 | case "monthly":
103 | repeatEveryUnit.html('months');
104 | break;
105 | case "yearly":
106 | repeatEveryUnit.html('years');
107 | break;
108 | }
109 |
110 | if (repeatSelect.val() === 'weekly') {
111 | repeatOn.show();
112 | } else {
113 | repeatOn.hide();
114 | }
115 |
116 | if (repeatSelect.val() === 'monthly') {
117 | repeatBy.show();
118 | } else {
119 | repeatBy.hide();
120 | }
121 |
122 | };
123 |
124 | repeatSelect.on('change', changeHandler);
125 | repeatSelect.trigger('change');
126 |
127 | };
128 |
129 | var _repeatEnds = function() {
130 |
131 | var repeatEndsSelect = _root.find('#' + _namespace + 'repeat-ends');
132 | var repeatEndOccurrences = _root.find('.field.occurrences');
133 | var repeatEndUntil = _root.find('.field.until');
134 |
135 | var changeHandler = function() {
136 | if (repeatEndsSelect.val() == 'after') {
137 | repeatEndOccurrences.show();
138 | } else {
139 | repeatEndOccurrences.hide();
140 | }
141 |
142 | if (repeatEndsSelect.val() == 'until') {
143 | repeatEndUntil.show();
144 | } else {
145 | repeatEndUntil.hide();
146 | }
147 | };
148 |
149 | repeatEndsSelect.on('change', changeHandler);
150 | repeatEndsSelect.trigger('change');
151 |
152 | };
153 |
154 | var _repeatNewDate = function() {
155 | var repeatDateButton = _root.find('#' + _namespace + 'date-add');
156 |
157 | var clickHandler = function() {
158 | var repeatDateDiv = _root.find('.field.exdates > .padding:last');
159 |
160 | var newRepeatDate = repeatDateDiv.clone();
161 | var newRepeatDateInput = newRepeatDate.find('input');
162 | var newRepeatDateDelete = newRepeatDate.find('a');
163 |
164 | if( newRepeatDateDelete.length == 0 ){
165 | newRepeatDate.append('');
166 | newRepeatDateDelete = newRepeatDate.find('a');
167 | }
168 |
169 | indexPos1 = newRepeatDateInput.attr('id').indexOf('exdates') + 7;
170 | indexPos2 = newRepeatDateInput.attr('id').indexOf('-date');
171 | newDateIndex = newRepeatDateInput.attr('id').substring(indexPos1, indexPos2);
172 | index = parseInt(newDateIndex) + 1;
173 |
174 | handlePos1 = newRepeatDateInput.attr('id').indexOf('fields-') + 7;
175 | handlePos2 = newRepeatDateInput.attr('id').indexOf('exdates');
176 | handle = newRepeatDateInput.attr('id').substring(handlePos1, handlePos2);
177 |
178 | newRepeatDateInput.attr('class', 'text');
179 | newRepeatDateInput.attr('id', 'fields-'+handle+'exdates'+index+'-date');
180 | newRepeatDateInput.attr('name', 'fields['+handle+'][exdates][][date]');
181 | newRepeatDateInput.val("");
182 |
183 | newRepeatDate.insertAfter(repeatDateDiv);
184 | newRepeatDate.show();
185 |
186 | newRepeatDateInput.datepicker({
187 | constrainInput: false,
188 | dateFormat: 'm/d/yy',
189 | defaultDate: new Date(),
190 | prevText: 'Prev',
191 | nextText: 'Next',
192 | });
193 |
194 | var deleteClickHandler = function(){
195 | newRepeatDate.fadeOut(300, function(){ newRepeatDate.remove(); });
196 | };
197 |
198 | newRepeatDateDelete.on('click', deleteClickHandler);
199 | };
200 |
201 | repeatDateButton.on('click', clickHandler);
202 | }
203 |
204 | _init();
205 |
206 | };
207 |
208 | return {
209 | create: function(id) {
210 | $('#' + id + '-field').data('ad', new advancedDateView(id));
211 | }
212 | };
213 |
214 | }();
--------------------------------------------------------------------------------
/services/RecurringDateService.php:
--------------------------------------------------------------------------------
1 | findByAttributes(array(
17 | 'elementId' => $fieldType->element->id,
18 | 'handle' => $fieldType->model->handle,
19 | ));
20 |
21 | // Get attributes
22 | if ($ruleRecord) {
23 | $attr = $ruleRecord->getAttributes();
24 | if( !empty($attr['start_date']) ){
25 | $attr['start_date'] = DateTime::createFromString($attr['start_date'], craft()->getTimeZone());
26 | }
27 | else
28 | {
29 | $attr['start_date'] = '';
30 | }
31 |
32 | if( !empty($attr['end_date']) ){
33 | $attr['end_date'] = DateTime::createFromString($attr['end_date'], craft()->getTimeZone());
34 | }
35 | else
36 | {
37 | $attr['end_date'] = '';
38 | }
39 |
40 | if( !empty($attr['until']) ){
41 | $attr['until'] = DateTime::createFromString($attr['until'], craft()->getTimeZone());
42 | }
43 | else
44 | {
45 | $attr['until'] = '';
46 | }
47 |
48 | if( !empty($attr['start_time']) ){
49 | $attr['start_time'] = DateTime::createFromFormat('H:i:s', $attr['start_time'], craft()->getTimeZone());
50 | }
51 | else
52 | {
53 | $attr['start_time'] = '';
54 | }
55 |
56 | if( !empty($attr['end_time']) ){
57 | $attr['end_time'] = DateTime::createFromFormat('H:i:s', $attr['end_time'], craft()->getTimeZone());
58 | }
59 | else
60 | {
61 | $attr['end_time'] = '';
62 | }
63 |
64 | } else {
65 | $attr = array();
66 | }
67 |
68 | return $attr;
69 | }
70 |
71 | public function validateRule(BaseFieldType $fieldType){
72 | $postContent = $fieldType->element->getContentFromPost();
73 | $value = $postContent[$fieldType->model->handle];
74 |
75 | $startDate = $value['start_date']['date'];
76 | $startTime = $value['start_time']['time'];
77 | $endDate = $value['end_date']['date'];
78 | $endTime = $value['end_time']['time'];
79 |
80 | $allday = $value['allday'];
81 | $repeats = $value['repeats'];
82 | $ends = $value['ends'];
83 | $until = $value['until']['date'];
84 | $count = $value['count'];
85 |
86 | $errors = array();
87 |
88 | if( empty($startDate) ){
89 | $errors[] = Craft::t('There must be a valid Start Date');
90 | }
91 |
92 | if( empty($startTime) && !$allday ){
93 | $errors[] = Craft::t('If not all day event Start Time must be set');
94 | }
95 |
96 | if( empty($endDate) && !empty($endTime) ){
97 | $errors[] = Craft::t('If End Time is set, End Date must also be set');
98 | }
99 |
100 | if( !empty($endDate) && empty($endTime) && !$allday ){
101 | $errors[] = Craft::t('If End Date is set, End Time must also be set');
102 | }
103 |
104 | //Checking Dates
105 | if( !empty($endDate) && !empty($startDate) && empty($endTime) && empty($startTime) ){
106 | if( strtotime($endDate) <= strtotime($startDate) ){
107 | $errors[] = Craft::t('End Date must be after the Start Date');
108 | }
109 | }
110 |
111 | //Checking Times
112 | if( !empty($endDate) && !empty($startDate) && !empty($endTime) && !empty($startTime) ){
113 | if( strtotime($endDate . ' ' . $endTime) <= strtotime($startDate . ' ' . $startTime) ){
114 | $errors[] = Craft::t('End Date/Time must be after the Start Date/Time');
115 | }
116 | }
117 |
118 | //Check until date
119 | if( empty($until) && $ends == 'until' && $repeats ){
120 | $errors[] = Craft::t('Until date must be set if repeating until a specific date');
121 | }
122 | elseif( !empty($until) && $ends == 'until' && $repeats ){
123 | if( strtotime($until) <= strtotime($startDate) ){
124 | $errors[] = Craft::t('Until date must be after the Start Date');
125 | }
126 | }
127 |
128 | //Check after count
129 | if( empty($count) && $ends == 'after' && $repeats ){
130 | $errors[] = Craft::t('After count must be set');
131 | }
132 |
133 | if($errors){
134 | return $errors;
135 | }
136 | else{
137 | return true;
138 | }
139 | }
140 |
141 | // // Modify fieldtype query
142 | // public function modifyQuery(DbCommand $query, $params = array())
143 | // {
144 | // // Join with plugin table
145 | // $query->join('recurringdate_rules', 'elements.id='.craft()->db->tablePrefix.'recurringdate_rules'.'.elementId');
146 | // // Search by comparing coordinates
147 | // // Return modified query
148 | // return $query;
149 | // }
150 |
151 | // Once the content has been saved...
152 | public function contentSaved(ContentModel $content, $isNewContent)
153 | {
154 | $this->content = $content;
155 | $this->isNewContent = $isNewContent;
156 | }
157 |
158 | // Save field to plugin table
159 | public function saveRuleField(BaseFieldType $fieldType)
160 | {
161 | // Get elementId and handle
162 | $elementId = $fieldType->element->id;
163 | $handle = $fieldType->model->handle;
164 |
165 | // Check if attribute exists
166 | if (!$this->content->getAttribute($handle)) {
167 | return false;
168 | }
169 |
170 | // Set specified attributes
171 | $attr = $this->content[$handle];
172 |
173 | // Attempt to load existing record
174 | $ruleRecord = RecurringDate_RuleRecord::model()->findByAttributes(array(
175 | 'elementId' => $elementId,
176 | 'handle' => $handle,
177 | ));
178 |
179 | // If no record exists, create new record
180 | if (!$ruleRecord) {
181 | $ruleRecord = new RecurringDate_RuleRecord;
182 | $attr['elementId'] = $elementId;
183 | $attr['handle'] = $handle;
184 | }
185 |
186 | $attr['start_date'] = ( !empty($attr['start_date']['date']) ? strtotime($attr['start_date']['date']) : null );
187 | $attr['end_date'] = ( !empty($attr['end_date']['date']) ? strtotime($attr['end_date']['date']) : null );
188 | $attr['start_time'] = ( !empty($attr['start_time']['time']) ? date('H:i:s', strtotime($attr['start_time']['time'])) : null );
189 | $attr['end_time'] = ( !empty($attr['end_time']['time']) ? date('H:i:s', strtotime($attr['end_time']['time'])) : null );
190 |
191 |
192 | if( isset($attr['exdates']) ){
193 | $rawExDates = $attr['exdates'];
194 | $attr['exdates'] = array();
195 |
196 | foreach ($rawExDates as $index => $exdate) {
197 | $attr['exdates'][] = !empty($exdate['date']) ? strtotime($exdate['date']) : null;
198 | }
199 | }
200 | else{
201 | $attr['exdates'] = array();
202 | }
203 |
204 | if (!isset($attr['allday'])) { $attr['allday'] =null; }
205 | if (!isset($attr['repeats'])) { $attr['repeats'] =null; }
206 | if (!isset($attr['frequency'])) { $attr['frequency'] =null; }
207 | if (!isset($attr['interval'])) { $attr['interval'] =null; }
208 | if (!isset($attr['weekdays'])) { $attr['weekdays'] =null; }
209 | if (!isset($attr['repeat_by'])) { $attr['repeat_by'] =null; }
210 | if (!isset($attr['ends'])) { $attr['ends'] = null;}
211 | if (!isset($attr['count'])) { $attr['count'] = null;}
212 |
213 | $attr['until'] = ( isset($attr['until']['date']) ? strtotime($attr['until']['date']) : null );
214 |
215 | $attr['rrule'] = $this->buildRRule($attr);
216 |
217 | // Set record attributes
218 | $ruleRecord->setAttributes($attr, false);
219 |
220 | $ruleSaved = $ruleRecord->save();
221 |
222 | $id = $ruleRecord->id;
223 |
224 | RecurringDate_DateRecord::model()->deleteAll('ruleId = '. $id);
225 |
226 | $this->generateDates($attr['rrule'], $id, $attr['repeats'], $attr['start_date'], $attr['end_date']);
227 |
228 | return $ruleSaved;
229 |
230 | }
231 |
232 | private function generateDates($rrule, $id, $repeats, $start, $end){
233 | $finalDates = array();
234 | $start = DateTime::createFromString($start, craft()->getTimeZone());
235 |
236 | if(!is_null($end)){
237 | $end = DateTime::createFromString($end, craft()->getTimeZone());
238 | }
239 |
240 | if($repeats){
241 | $rule = new Recurr\RecurrenceRule($rrule);
242 | $ruleTransformer = new Recurr\RecurrenceRuleTransformer($rule, 300);
243 | $dates = $ruleTransformer->getComputedArray();
244 |
245 | if( !is_null($end) ){
246 | $durationInterval = $start->diff($end);
247 | }
248 | else{
249 | $durationInterval = $start->diff($start);
250 | }
251 |
252 | $fullDates = array();
253 | foreach ($dates as $date) {
254 | $end = clone $date;
255 | $end = $end->add($durationInterval);
256 |
257 | $startDateString = $date->format('Ymd\THis');
258 | $endDateString = $end->format('Ymd\THis');
259 |
260 |
261 | $datesValues['start'] = DateTime::createFromFormat('Ymd\THis', $startDateString, craft()->getTimeZone());
262 |
263 | if( !empty($end) ){
264 | $datesValues['end'] = DateTime::createFromFormat('Ymd\THis', $endDateString, craft()->getTimeZone());
265 | }
266 |
267 | $fullDates[] = $datesValues;
268 | }
269 |
270 | $finalDates = $fullDates;
271 | }
272 | else{
273 | $finalDates = array(array( 'start' => $start, 'end' => $end ));
274 | }
275 |
276 | foreach ($finalDates as $index => $date) {
277 | $dateRecord = new RecurringDate_DateRecord;
278 | $dateRecord->setAttributes(array(
279 | 'ruleId' => $id,
280 | 'start' => $date['start'],
281 | 'end' => $date['end'],
282 | ), false);
283 | $dateRecord->save();
284 | }
285 | }
286 |
287 | // SELECT d.start, d.end, r.end_time, r.start_time, r.allday, r.repeats, r.rrule
288 | // FROM craft_recurringdate_dates d
289 | // LEFT JOIN craft_recurringdate_rules r
290 | // ON d.ruleId = r.id
291 | // WHERE handle='eventDate'
292 | // ORDER BY start, start_time DESC
293 |
294 | // public function getCriteria($attributes = null){
295 |
296 | // }
297 |
298 | public function getDate($id){
299 | $query = craft()->db->createCommand()
300 | ->select('d.id, r.elementId, d.start start_date, d.start start, d.end end_date, d.end end, r.end_time, r.start_time, r.allday, r.repeats, r.rrule')
301 | ->from('recurringdate_dates d')
302 | ->leftJoin('recurringdate_rules r', 'd.ruleId = r.id')
303 | ->where('d.id=:id', array(':id'=>$id));
304 |
305 | return $query->queryRow();
306 | }
307 |
308 | public function getDates($handle, $limit, $order, $groupBy, $before, $after, $criteria, $excludes){
309 |
310 | $query = craft()->db->createCommand()
311 | ->select('d.id, r.elementId, d.start start_date, d.start start, d.end end_date, d.end end, r.end_time, r.start_time, r.allday, r.repeats, r.rrule')
312 | ->from('recurringdate_dates d')
313 | ->leftJoin('recurringdate_rules r', 'd.ruleId = r.id')
314 | ->where('handle=:handle', array(':handle'=>$handle));
315 |
316 |
317 | if( !is_null($before) ){
318 | $beforeValue = date('Y-m-d H:i:s', strtotime($before));
319 | $query->andWhere(':before >= d.start', array(':before'=>$beforeValue));
320 | }
321 |
322 | if( !is_null($after) ){
323 | $afterValue = date('Y-m-d H:i:s', strtotime($after));
324 | $query->andWhere(':after <= d.start', array(':after'=>$afterValue));
325 | }
326 |
327 | if( !is_null($criteria) ){
328 | $critArr = array();
329 | foreach ($criteria as $index => $entry) {
330 | $critArr[] = $entry->id;
331 | }
332 |
333 | $critIds = implode(',', $critArr);
334 |
335 | $query->andWhere('r.elementId IN (:ids)', array(':ids'=>$critIds));
336 | }
337 |
338 | if($order == 'ASC'){
339 | $query->order(array('start ASC', 'start_time ASC'));
340 | }
341 | else{
342 | $query->order(array('start DESC', 'start_time DESC'));
343 | }
344 |
345 |
346 | if( !is_null($limit) ){
347 | $query->limit($limit * 2);
348 | }
349 |
350 |
351 | $events = $query->queryAll();
352 |
353 | $eventsFinal = array();
354 |
355 | foreach ($events as $index => $value) {
356 | $id = $value['elementId'];
357 | if( $excludes ){
358 | $exdates = $this->getExdates($value['rrule']);
359 | if( !in_array(date('Ymd', strtotime($value['start'])), $exdates) ){
360 | $eventsFinal[] = array(
361 | 'date' => $value,
362 | 'entry' => craft()->entries->getEntryById($id),
363 | );
364 | }
365 | }
366 | else{
367 | $eventsFinal[] = array(
368 | 'date' => $value,
369 | 'entry' => craft()->entries->getEntryById($id),
370 | );
371 | }
372 | }
373 |
374 | if( !is_null($limit) ){
375 | $eventsFinal = array_slice($eventsFinal, 0, $limit);
376 | }
377 |
378 | if( !is_null($groupBy) ){
379 | if( $groupBy == 'day' ){
380 | $eventsFinal = $this->groupBy($eventsFinal, 'n/j/Y');
381 | }
382 | elseif( $groupBy == 'month' ){
383 | $eventsFinal = $this->groupBy($eventsFinal, 'n/1/Y');
384 | }
385 | elseif( $groupBy == 'year' ){
386 | $eventsFinal = $this->groupBy($eventsFinal, '1/1/Y');
387 | }
388 | }
389 |
390 | return $eventsFinal;
391 | }
392 |
393 | private function getExdates($rrule){
394 | if( strpos($rrule, 'EXDATE') !== false ){
395 | $exdates = array();
396 |
397 | $exDatesArray = explode('EXDATE=', $rrule);
398 | $exDatesString = $exDatesArray[1];
399 | $exDatesString = rtrim($exDatesString, ";");
400 | $exDatesArray = explode(',', $exDatesString);
401 |
402 | foreach ($exDatesArray as $index => $date) {
403 | $exdates[] = date('Ymd', strtotime($date));
404 | }
405 | }
406 | else{
407 | $exdates = array();
408 | }
409 | return $exdates;
410 | }
411 |
412 | private function groupBy($events, $groupString){
413 | $dates = array();
414 | foreach ($events as $i => $date) {
415 | $dateStart = $date['date']['start'];
416 | $formDate = date($groupString, strtotime($dateStart));
417 | if( isset($dates[$formDate]) ){
418 | $dates[$formDate][] = array(
419 | 'entry' => $date['entry'],
420 | 'date' => $date['date']
421 | );
422 | }
423 | else{
424 | $dates[$formDate] = array(array(
425 | 'entry' => $date['entry'],
426 | 'date' => $date['date']
427 | ));
428 | }
429 | }
430 |
431 | return $dates;
432 | }
433 |
434 | private function buildRRule($settings){
435 | $allday = $settings['allday'];
436 | $startDate = $settings['start_date'];
437 | $endDate = $settings['end_date'];
438 | $startTime = $settings['start_time'];
439 | $endTime = $settings['end_time'];
440 | $repeats = $settings['repeats']; //Does it repeat?
441 | $frequency = $settings['frequency']; //Weekly, Daily, Monthly, Yearly
442 | $interval = $settings['interval']; // i.e. Every 1-30 Months?
443 | $weekDays = $settings['weekdays']; //Which weekdays
444 | $repeatBy = $settings['repeat_by']; //Monthly, by day of week, or day of month
445 | $ends = $settings['ends']; //how it ends (never, after, until)
446 | $count = $settings['count']; // if ending occurs amounts
447 | $untilDate = $settings['until']; // if ending until date
448 | $exDates = $settings['exdates'];
449 |
450 | $dbString = '';
451 |
452 | //Builds RRULE based on UI Elements Input
453 | if($repeats){
454 | $rule = new Recurr\RecurrenceRule();
455 | $rule->setStartDate(DateTime::createFromString($startDate, craft()->getTimeZone()));
456 | $rule->setInterval($interval);
457 |
458 | if($ends == 'until'){
459 | $rule->setEndDate(DateTime::createFromString($untilDate, craft()->getTimeZone()));
460 | }
461 | else if($ends == 'after'){
462 | $rule->setCount($count);
463 | }
464 |
465 | switch ($frequency) {
466 | case 'daily':
467 | $rule->setFreq(Recurr\RecurrenceRule::FREQ_DAILY);
468 | break;
469 |
470 | case 'weekly':
471 | $rule->setFreq(Recurr\RecurrenceRule::FREQ_WEEKLY);
472 | if( empty($weekDays) ){
473 | //If weekdays empty set monday by default
474 | $rule->setByDay(array('MO'));
475 | }
476 | else{
477 | $rule->setByDay($weekDays);
478 | }
479 | break;
480 |
481 | case 'monthly':
482 | $rule->setFreq(Recurr\RecurrenceRule::FREQ_MONTHLY);
483 | if( $repeatBy == 'month' ){
484 | $dayOfMonth = date('j', $startDate);
485 | $rule->setByMonthDay(array($dayOfMonth));
486 | }
487 | else if( $repeatBy == 'week' ){
488 | $uStartDate = $startDate;
489 | $dayOfWeek = strtoupper( substr( date( 'D', $uStartDate ), 0, -1) );
490 | $numberOfWeek = ceil( date( 'j', $uStartDate ) / 7 );
491 | $rule->setByDay(array('+'.$numberOfWeek . $dayOfWeek));
492 | }
493 | break;
494 |
495 | case 'yearly':
496 | $rule->setFreq(Recurr\RecurrenceRule::FREQ_YEARLY);
497 | break;
498 | }
499 |
500 | if($startTime){
501 | $time = date('Ymd\THis', strtotime( date('Y-m-d', $startDate) . date(' H:i:s', strtotime($startTime))));
502 | $dbString .= 'DTSTART=' . $time . ';' . $rule->getString();
503 | }
504 | else{
505 | $time = $startDate;
506 | var_dump($startDate);
507 | $dbString .= 'DTSTART=' . date('Ymd', $time) . ';' . $rule->getString();
508 | }
509 |
510 | if( count($exDates) > 0 ){
511 | $dbString .= ';EXDATE=';
512 | foreach ($exDates as $index => $date) {
513 | $dbString .= date('Ymd', $date);
514 | if( $date !== end($exDates) ){
515 | $dbString .= ',';
516 | }
517 | }
518 | $dbString .= ';';
519 | }
520 | }
521 | else{
522 | if($startTime){
523 | $time = date('Ymd\THis', strtotime( date('Ymd', $startDate) . date('\THis', strtotime($startTime))));
524 | $dbString .= 'DTSTART=' . $time . ';';
525 | }
526 | else{
527 | $dbString .= 'DTSTART=' . date('Ymd', $startDate) . ';';
528 | }
529 | }
530 |
531 | return $dbString;
532 | }
533 | }
--------------------------------------------------------------------------------
/services/RecurringDate_SortService.php:
--------------------------------------------------------------------------------
1 | $entry) {
9 | foreach ($entry->eventDate['dates'] as $i => $date) {
10 | $dates[] = array(
11 | 'start' => $date['start'],
12 | 'entry' => $entry
13 | );
14 | }
15 | }
16 |
17 | $cmp = function($a, $b){
18 | return strcmp($a['start'], $b['start']);
19 | };
20 |
21 | usort($dates, $cmp);
22 |
23 | return $dates;
24 | }
25 |
26 | public function group($entries, $field){
27 | $dates = array();
28 |
29 | //Group the entries by date
30 | foreach ($entries as $index => $entry) {
31 | foreach ($entry->eventDate['dates'] as $i => $date) {
32 | $dateStart = $date['start'];
33 | $formDate = strtotime($dateStart->format('Ymd'));
34 | if( isset($dates[$formDate]) ){
35 | $dates[$formDate][] = array(
36 | 'entry' => $entry,
37 | 'start' => $date['start'],
38 | 'end' => $date['end']
39 | );
40 | }
41 | else{
42 | $dates[$formDate] = array(array(
43 | 'entry' => $entry,
44 | 'start' => $date['start'],
45 | 'end' => $date['end']
46 | ));
47 | }
48 | }
49 | }
50 |
51 | //Sort by dates
52 | ksort($dates);
53 |
54 | // $cmp = function($a, $b){
55 | // return strcmp($a->eventDate['startdate'], $b->eventDate['startdate']);
56 | // };
57 |
58 | // foreach ($dates as $index => $date) {
59 | // usort($date, $cmp);
60 | // }
61 |
62 | return $dates;
63 | }
64 |
65 | }
--------------------------------------------------------------------------------
/templates/fields.html:
--------------------------------------------------------------------------------
1 | {% import "_includes/forms" as forms %}
2 |
3 |
Does this event last all day?
9 |Does this event occur multiple times?
58 |