radiant-polls-extension 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +27 -0
- data/HELP.textile +71 -0
- data/LICENSE +21 -0
- data/README.textile +112 -0
- data/Rakefile +129 -0
- data/VERSION +1 -0
- data/app/controllers/admin/polls_controller.rb +25 -0
- data/app/controllers/poll_response_controller.rb +35 -0
- data/app/models/option.rb +22 -0
- data/app/models/poll.rb +67 -0
- data/app/views/admin/polls/_form.html.haml +67 -0
- data/app/views/admin/polls/_option.html.haml +9 -0
- data/app/views/admin/polls/edit.html.haml +5 -0
- data/app/views/admin/polls/index.html.haml +72 -0
- data/app/views/admin/polls/new.html.haml +5 -0
- data/app/views/admin/polls/remove.html.haml +17 -0
- data/config/locales/en.yml +25 -0
- data/config/locales/en_available_tags.yml +187 -0
- data/config/routes.rb +6 -0
- data/db/migrate/001_create_polls.rb +12 -0
- data/db/migrate/002_create_options.rb +14 -0
- data/db/migrate/003_add_start_date_to_polls.rb +9 -0
- data/lib/poll_process.rb +23 -0
- data/lib/poll_tags.rb +408 -0
- data/lib/tasks/polls_extension_tasks.rake +56 -0
- data/polls_extension.rb +51 -0
- data/public/images/admin/new-poll.png +0 -0
- data/public/images/admin/poll.png +0 -0
- data/public/images/admin/recycle.png +0 -0
- data/public/images/admin/reset.png +0 -0
- data/public/javascripts/admin/date_selector.js +177 -0
- data/public/javascripts/admin/polls.js +47 -0
- data/public/javascripts/poll_check.js +52 -0
- data/public/stylesheets/admin/polls.css +93 -0
- data/public/stylesheets/polls.css +9 -0
- data/radiant-polls-extension.gemspec +83 -0
- data/spec/controllers/admin/polls_controller_spec.rb +16 -0
- data/spec/controllers/poll_response_controller_spec.rb +149 -0
- data/spec/integration/page_caching_spec.rb +76 -0
- data/spec/lib/poll_tags_spec.rb +415 -0
- data/spec/models/poll_spec.rb +50 -0
- data/spec/spec.opts +6 -0
- data/spec/spec_helper.rb +42 -0
- metadata +141 -0
@@ -0,0 +1,56 @@
|
|
1
|
+
namespace :radiant do
|
2
|
+
namespace :extensions do
|
3
|
+
namespace :polls do
|
4
|
+
|
5
|
+
desc "Runs the migration of the Assets extension"
|
6
|
+
task :migrate => :environment do
|
7
|
+
require 'radiant/extension_migrator'
|
8
|
+
if ENV["VERSION"]
|
9
|
+
PollsExtension.migrator.migrate(ENV["VERSION"].to_i)
|
10
|
+
Rake::Task['db:schema:dump'].invoke
|
11
|
+
else
|
12
|
+
PollsExtension.migrator.migrate
|
13
|
+
Rake::Task['db:schema:dump'].invoke
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
desc "Copies public assets of the Assets to the instance public/ directory."
|
18
|
+
task :update => :environment do
|
19
|
+
is_svn_or_dir = proc {|path| path =~ /\.svn/ || File.directory?(path) }
|
20
|
+
Dir[PollsExtension.root + "/public/**/*"].reject(&is_svn_or_dir).each do |file|
|
21
|
+
path = file.sub(PollsExtension.root, '')
|
22
|
+
directory = File.dirname(path)
|
23
|
+
puts "Copying #{path}..."
|
24
|
+
mkdir_p RAILS_ROOT + directory, :verbose => false
|
25
|
+
cp file, RAILS_ROOT + path, :verbose => false
|
26
|
+
end
|
27
|
+
|
28
|
+
unless PollsExtension.root.starts_with? RAILS_ROOT # don't need to copy vendored tasks
|
29
|
+
puts "Copying rake tasks from PollsExtension"
|
30
|
+
local_tasks_path = File.join(RAILS_ROOT, %w(lib tasks))
|
31
|
+
mkdir_p local_tasks_path, :verbose => false
|
32
|
+
Dir[File.join PollsExtension.root, %w(lib tasks *.rake)].each do |file|
|
33
|
+
cp file, local_tasks_path, :verbose => false
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
desc "Syncs all available translations for this ext to the English ext master"
|
39
|
+
task :sync => :environment do
|
40
|
+
# The main translation root, basically where English is kept
|
41
|
+
language_root = PollsExtension.get_translation_keys(language_root)
|
42
|
+
words = TranslationSupport.get_translation_keys(language_root)
|
43
|
+
|
44
|
+
Dir["#{language_root}/*.yml"].each do |filename|
|
45
|
+
next if filename.match('_available_tags')
|
46
|
+
basename = File.basename(filename, '.yml')
|
47
|
+
puts "Syncing #{basename}"
|
48
|
+
(comments, other) = TranslationSupport.read_file(filename, basename)
|
49
|
+
words.each { |k,v| other[k] ||= words[k] } # Initializing hash variable as empty if it does not exist
|
50
|
+
other.delete_if { |k,v| !words[k] } # Remove if not defined in en.yml
|
51
|
+
TranslationSupport.write_file(filename, basename, comments, other)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/polls_extension.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require_dependency 'application_controller'
|
2
|
+
|
3
|
+
class PollsExtension < Radiant::Extension
|
4
|
+
|
5
|
+
version "#{File.read(File.expand_path(File.dirname(__FILE__)) + '/VERSION')}"
|
6
|
+
description 'Radiant gets polls. Forked from nuex (Chase James) to use cookies.'
|
7
|
+
url 'https://github.com/avonderluft/radiant-polls-extension'
|
8
|
+
|
9
|
+
def activate
|
10
|
+
|
11
|
+
unless defined?(CacheByPage)
|
12
|
+
puts "The Radiant cache_by_page extension is recommended, to set shorter caching times on pages with polls"
|
13
|
+
end
|
14
|
+
|
15
|
+
tab 'Content' do
|
16
|
+
add_item 'Polls', '/admin/polls'
|
17
|
+
end
|
18
|
+
|
19
|
+
if Radiant::Config.table_exists?
|
20
|
+
Radiant::Config['paginate.url_route'] = '' unless Radiant::Config['paginate.url_route']
|
21
|
+
PollsExtension.const_set('UrlCache', Radiant::Config['paginate.url_route'])
|
22
|
+
end
|
23
|
+
|
24
|
+
Page.send :include, PollTags
|
25
|
+
Page.send :include, PollProcess
|
26
|
+
|
27
|
+
Radiant::AdminUI.class_eval do
|
28
|
+
attr_accessor :poll
|
29
|
+
alias_method "polls", :poll
|
30
|
+
end
|
31
|
+
admin.poll = load_default_poll_regions
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
def load_default_poll_regions
|
36
|
+
OpenStruct.new.tap do |poll|
|
37
|
+
poll.edit = Radiant::AdminUI::RegionSet.new do |edit|
|
38
|
+
edit.main.concat %w{edit_header edit_form}
|
39
|
+
edit.form_bottom.concat %w{edit_buttons edit_timestamp}
|
40
|
+
end
|
41
|
+
poll.index = Radiant::AdminUI::RegionSet.new do |index|
|
42
|
+
index.top.concat %w{}
|
43
|
+
index.thead.concat %w{name_header date_header responses_header actions_header}
|
44
|
+
index.tbody.concat %w{name_cell date_cell responses_cell actions_cell}
|
45
|
+
index.bottom.concat %w{new_button}
|
46
|
+
end
|
47
|
+
poll.new = poll.edit
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
@@ -0,0 +1,177 @@
|
|
1
|
+
DateSelector = Behavior.create({
|
2
|
+
initialize: function(options) {
|
3
|
+
this.element.addClassName('date_selector');
|
4
|
+
this.calendar = null;
|
5
|
+
this.options = Object.extend(DateSelector.DEFAULTS, options || {});
|
6
|
+
this.date = this.getDate();
|
7
|
+
this._createCalendar();
|
8
|
+
},
|
9
|
+
setDate : function(value) {
|
10
|
+
this.date = value;
|
11
|
+
this.element.value = this.options.setter(this.date);
|
12
|
+
|
13
|
+
if (this.calendar)
|
14
|
+
setTimeout(this.calendar.element.hide.bind(this.calendar.element), 500);
|
15
|
+
},
|
16
|
+
_createCalendar : function() {
|
17
|
+
var calendar = $div({ 'class' : 'date_selector' });
|
18
|
+
document.body.appendChild(calendar);
|
19
|
+
calendar.setStyle({
|
20
|
+
position : 'absolute',
|
21
|
+
zIndex : '500',
|
22
|
+
top : Position.cumulativeOffset(this.element)[1] + this.element.getHeight() + 'px',
|
23
|
+
left : Position.cumulativeOffset(this.element)[0] + 'px'
|
24
|
+
});
|
25
|
+
this.calendar = new Calendar(calendar, this);
|
26
|
+
},
|
27
|
+
onclick : function(e) {
|
28
|
+
this.calendar.show();
|
29
|
+
Event.stop(e);
|
30
|
+
},
|
31
|
+
onfocus : function(e) {
|
32
|
+
this.onclick(e);
|
33
|
+
},
|
34
|
+
getDate : function() {
|
35
|
+
return this.options.getter(this.element.value) || new Date;
|
36
|
+
}
|
37
|
+
});
|
38
|
+
|
39
|
+
Calendar = Behavior.create({
|
40
|
+
initialize : function(selector) {
|
41
|
+
this.selector = selector;
|
42
|
+
this.element.hide();
|
43
|
+
Event.observe(document, 'click', this.element.hide.bind(this.element));
|
44
|
+
},
|
45
|
+
show : function() {
|
46
|
+
Calendar.instances.invoke('hide');
|
47
|
+
this.date = this.selector.getDate();
|
48
|
+
this.redraw();
|
49
|
+
this.element.show();
|
50
|
+
this.active = true;
|
51
|
+
},
|
52
|
+
hide : function() {
|
53
|
+
this.element.hide();
|
54
|
+
this.active = false;
|
55
|
+
},
|
56
|
+
redraw : function() {
|
57
|
+
var html = '<table class="calendar">' +
|
58
|
+
' <thead>' +
|
59
|
+
' <tr><th class="back"><a href="#">←</a></th>' +
|
60
|
+
' <th colspan="5" class="month_label">' + this._label() + '</th>' +
|
61
|
+
' <th class="forward"><a href="#">→</a></th></tr>' +
|
62
|
+
' <tr class="day_header">' + this._dayRows() + '</tr>' +
|
63
|
+
' </thead>' +
|
64
|
+
' <tbody>';
|
65
|
+
html += this._buildDateCells();
|
66
|
+
html += '</tbody></table>';
|
67
|
+
this.element.innerHTML = html;
|
68
|
+
},
|
69
|
+
onclick : function(e) {
|
70
|
+
var source = Event.element(e);
|
71
|
+
Event.stop(e);
|
72
|
+
|
73
|
+
if ($(source.parentNode).hasClassName('day')) return this._setDate(source);
|
74
|
+
if ($(source.parentNode).hasClassName('back')) return this._backMonth();
|
75
|
+
if ($(source.parentNode).hasClassName('forward')) return this._forwardMonth();
|
76
|
+
},
|
77
|
+
_setDate : function(source) {
|
78
|
+
if (source.innerHTML.strip() != '') {
|
79
|
+
this.date.setDate(parseInt(source.innerHTML));
|
80
|
+
this.selector.setDate(this.date);
|
81
|
+
this.element.select('.selected').invoke('removeClassName', 'selected');
|
82
|
+
source.parentNode.addClassName('selected');
|
83
|
+
}
|
84
|
+
},
|
85
|
+
_backMonth : function() {
|
86
|
+
this.date.setMonth(this.date.getMonth() - 1);
|
87
|
+
this.redraw();
|
88
|
+
return false;
|
89
|
+
},
|
90
|
+
_forwardMonth : function() {
|
91
|
+
this.date.setMonth(this.date.getMonth() + 1);
|
92
|
+
this.redraw();
|
93
|
+
return false;
|
94
|
+
},
|
95
|
+
_getDateFromSelector : function() {
|
96
|
+
this.date = new Date(this.selector.date.getTime());
|
97
|
+
},
|
98
|
+
_firstDay : function(month, year) {
|
99
|
+
return new Date(year, month, 1).getDay();
|
100
|
+
},
|
101
|
+
_monthLength : function(month, year) {
|
102
|
+
var length = Calendar.MONTHS[month].days;
|
103
|
+
return (month == 1 && (year % 4 == 0) && ((year % 100 != 0) || (year % 400 == 0))) ? 29 : length;
|
104
|
+
},
|
105
|
+
_label : function() {
|
106
|
+
return Calendar.MONTHS[this.date.getMonth()].label + ' ' + this.date.getFullYear();
|
107
|
+
},
|
108
|
+
_dayRows : function() {
|
109
|
+
for (var i = 0, html='', day; day = Calendar.DAYS[i]; i++)
|
110
|
+
html += '<th>' + day + '</th>';
|
111
|
+
return html;
|
112
|
+
},
|
113
|
+
_buildDateCells : function() {
|
114
|
+
var month = this.date.getMonth(), year = this.date.getFullYear();
|
115
|
+
var day = 1, monthLength = this._monthLength(month, year), firstDay = this._firstDay(month, year);
|
116
|
+
|
117
|
+
for (var i = 0, html = '<tr>'; i < 9; i++) {
|
118
|
+
for (var j = 0; j <= 6; j++) {
|
119
|
+
|
120
|
+
if (day <= monthLength && (i > 0 || j >= firstDay)) {
|
121
|
+
var classes = ['day'];
|
122
|
+
|
123
|
+
if (this._compareDate(new Date, year, month, day)) classes.push('today');
|
124
|
+
if (this._compareDate(this.selector.date, year, month, day)) classes.push('selected');
|
125
|
+
|
126
|
+
html += '<td class="' + classes.join(' ') + '">' +
|
127
|
+
'<a href="#">' + day++ + '</a>' +
|
128
|
+
'</td>';
|
129
|
+
} else html += '<td></td>';
|
130
|
+
}
|
131
|
+
|
132
|
+
if (day > monthLength) break;
|
133
|
+
else html += '</tr><tr>';
|
134
|
+
}
|
135
|
+
|
136
|
+
return html + '</tr>';
|
137
|
+
},
|
138
|
+
_compareDate : function(date, year, month, day) {
|
139
|
+
return date.getFullYear() == year &&
|
140
|
+
date.getMonth() == month &&
|
141
|
+
date.getDate() == day;
|
142
|
+
}
|
143
|
+
});
|
144
|
+
|
145
|
+
DateSelector.DEFAULTS = {
|
146
|
+
setter: function(date) {
|
147
|
+
return [
|
148
|
+
date.getFullYear(),
|
149
|
+
date.getMonth() + 1,
|
150
|
+
date.getDate()
|
151
|
+
].join('/');
|
152
|
+
},
|
153
|
+
getter: function(value) {
|
154
|
+
var parsed = Date.parse(value);
|
155
|
+
|
156
|
+
if (!isNaN(parsed)) return new Date(parsed);
|
157
|
+
else return null;
|
158
|
+
}
|
159
|
+
}
|
160
|
+
|
161
|
+
Object.extend(Calendar, {
|
162
|
+
DAYS : $w('S M T W T F S'),
|
163
|
+
MONTHS : [
|
164
|
+
{ label : 'January', days : 31 },
|
165
|
+
{ label : 'February', days : 28 },
|
166
|
+
{ label : 'March', days : 31 },
|
167
|
+
{ label : 'April', days : 30 },
|
168
|
+
{ label : 'May', days : 31 },
|
169
|
+
{ label : 'June', days : 30 },
|
170
|
+
{ label : 'July', days : 31 },
|
171
|
+
{ label : 'August', days : 31 },
|
172
|
+
{ label : 'September', days : 30 },
|
173
|
+
{ label : 'October', days : 31 },
|
174
|
+
{ label : 'November', days : 30 },
|
175
|
+
{ label : 'December', days : 31 }
|
176
|
+
]
|
177
|
+
});
|
@@ -0,0 +1,47 @@
|
|
1
|
+
// poll javascript
|
2
|
+
|
3
|
+
// add an option field
|
4
|
+
|
5
|
+
function add_option(container_id){
|
6
|
+
var container = $(container_id);
|
7
|
+
var template = new Template('<p class="option" id="option_#{id}"><input type="text" name="poll[option_attributes][][title]" size="30" /> <a href="#" onclick="Element.remove(\'option_#{id}\')">Cancel</a></p>');
|
8
|
+
new Insertion.Bottom(container, template.evaluate({id: new Date().getTime()}));
|
9
|
+
$(container).childElements().last().childElements()[0].activate();
|
10
|
+
}
|
11
|
+
|
12
|
+
|
13
|
+
// delete an option
|
14
|
+
|
15
|
+
function delete_option(id, container_id){
|
16
|
+
if(confirm("Really delete this option?")){
|
17
|
+
var container = $(container_id);
|
18
|
+
|
19
|
+
// mark option for deletion
|
20
|
+
($("option_"+id).down('.should_destroy') || $("option_"+id).next('.error-with-field').next('.should_destroy')).value = 1
|
21
|
+
|
22
|
+
// hide the field
|
23
|
+
var elem = Element.hide("option_"+id);
|
24
|
+
var next_elem = elem.nextSiblings()[0];
|
25
|
+
if (next_elem.hasClassName('error-with-field')) {
|
26
|
+
next_elem.hide();
|
27
|
+
}
|
28
|
+
|
29
|
+
// don't redisplay the deletion notice if its already there
|
30
|
+
if(typeof $('options').previous('.title').down('#options-deleted') == 'undefined')
|
31
|
+
{
|
32
|
+
new Insertion.After('options-title', '<p class="option" id="options-deleted">Removed options will be deleted when you Save this poll.</p>');
|
33
|
+
}
|
34
|
+
new Effect.Highlight('options-deleted');
|
35
|
+
}
|
36
|
+
}
|
37
|
+
|
38
|
+
|
39
|
+
// rearrange the page contents (works around problems with error messages)
|
40
|
+
|
41
|
+
function initialize_page_view() {
|
42
|
+
$('options').descendants().findAll(function(obj) {
|
43
|
+
return obj.hasClassName('delete');
|
44
|
+
}).each(function(obj) {
|
45
|
+
obj.nextSiblings()[0].insert({ after:obj.remove() });
|
46
|
+
});
|
47
|
+
}
|
@@ -0,0 +1,52 @@
|
|
1
|
+
// In order to cache pages with poll functionality we put the logic in the client.
|
2
|
+
// In other words, rather than having the server check the cookie status
|
3
|
+
// to see if the user has taken the poll and then decide to send either the questions or results,
|
4
|
+
// we should send both options to the client. Then, using the following javascript on the browser,
|
5
|
+
// check the cookie and display either the poll options or the poll results.
|
6
|
+
|
7
|
+
// Read a cookie
|
8
|
+
function readCookie(name) {
|
9
|
+
var nameEQ = name + "=";
|
10
|
+
var ca = document.cookie.split(';');
|
11
|
+
for(var i=0;i < ca.length;i++) {
|
12
|
+
var c = ca[i];
|
13
|
+
while (c.charAt(0)==' ') c = c.substring(1,c.length);
|
14
|
+
if (c.indexOf(nameEQ) == 0) {
|
15
|
+
var a=c.substring(nameEQ.length,c.length);
|
16
|
+
/* alert(a);*/
|
17
|
+
return a;
|
18
|
+
}
|
19
|
+
}
|
20
|
+
return null;
|
21
|
+
}
|
22
|
+
|
23
|
+
// Client-side Poll processing - check for presence of poll
|
24
|
+
// If present, read cookie to see if it has been submitted by this user
|
25
|
+
function checkForPoll () {
|
26
|
+
var poll = document.getElementById('poll');
|
27
|
+
if (poll!=null) {
|
28
|
+
var pollDivs = poll.getElementsByTagName("div");
|
29
|
+
var pollName = pollDivs[1].id;
|
30
|
+
var submittedDiv = document.getElementById(pollName + "_submitted");
|
31
|
+
var unsubmittedDiv = document.getElementById(pollName + "_unsubmitted");
|
32
|
+
theCookie=readCookie(pollName);
|
33
|
+
if (theCookie!=null) {
|
34
|
+
unsubmittedDiv.parentNode.removeChild(unsubmittedDiv);
|
35
|
+
submittedDiv.style.display = "inline";
|
36
|
+
} else {
|
37
|
+
submittedDiv.parentNode.removeChild(submittedDiv);
|
38
|
+
unsubmittedDiv.style.display = "inline";
|
39
|
+
}
|
40
|
+
}
|
41
|
+
}
|
42
|
+
|
43
|
+
// ensure radio button selected
|
44
|
+
function checkRadioSelection (frmId, rbGroupName) {
|
45
|
+
var radios = document.getElementById(frmId).elements[rbGroupName];
|
46
|
+
for (var i=0; i <radios.length; i++) {
|
47
|
+
if (radios[i].checked) return true;
|
48
|
+
}
|
49
|
+
alert("Please make a selection first...");
|
50
|
+
return false;
|
51
|
+
}
|
52
|
+
|
@@ -0,0 +1,93 @@
|
|
1
|
+
#content #polls.index {
|
2
|
+
border: none;
|
3
|
+
border-collapse: collapse;
|
4
|
+
}
|
5
|
+
#content #polls.index .responses {
|
6
|
+
padding-right: 2em;
|
7
|
+
text-align: right;
|
8
|
+
}
|
9
|
+
#content #polls.index .reset {
|
10
|
+
padding-left: 0;
|
11
|
+
width: 140px;
|
12
|
+
}
|
13
|
+
#content #polls.index .reset a {
|
14
|
+
background: url("../../images/admin/polls/recycle.png") no-repeat scroll 4px 0 transparent;
|
15
|
+
color: black;
|
16
|
+
font-size: 80%;
|
17
|
+
padding: 0 0 0 20px;
|
18
|
+
text-decoration: none;
|
19
|
+
}
|
20
|
+
#content #polls.index th.modify {
|
21
|
+
text-align: center;
|
22
|
+
}
|
23
|
+
#content #polls.index .node .poll,
|
24
|
+
#content #polls.index .node .responses {
|
25
|
+
font-size: 115%;
|
26
|
+
font-weight: bold;
|
27
|
+
}
|
28
|
+
#content #polls.index .node .responses .none {
|
29
|
+
color: #666;
|
30
|
+
}
|
31
|
+
#content #polls.index .node .poll a,
|
32
|
+
#content #polls.index .node .poll a:visited,
|
33
|
+
#content #polls.index .node .responses a,
|
34
|
+
#content #polls.index .node .responses a:visited {
|
35
|
+
color: black;
|
36
|
+
text-decoration: none;
|
37
|
+
}
|
38
|
+
|
39
|
+
#content #poll_form_area .title {
|
40
|
+
margin-top: 1em;
|
41
|
+
}
|
42
|
+
#content #poll_form_area .title label {
|
43
|
+
font-size: 120%;
|
44
|
+
}
|
45
|
+
|
46
|
+
#options p.option {
|
47
|
+
margin-bottom: 1em;
|
48
|
+
}
|
49
|
+
#options .error-with-field {
|
50
|
+
margin-top: -1em;
|
51
|
+
}
|
52
|
+
#options .delete {
|
53
|
+
margin: 0 0.5em;
|
54
|
+
}
|
55
|
+
#content .form-area p#add-option {
|
56
|
+
margin-top: 1em;
|
57
|
+
}
|
58
|
+
|
59
|
+
#poll_start_date {
|
60
|
+
position: relative;
|
61
|
+
}
|
62
|
+
table.calendar {
|
63
|
+
background: white;
|
64
|
+
border: 1px solid black;
|
65
|
+
}
|
66
|
+
table.calendar td {
|
67
|
+
background: white;
|
68
|
+
border: 1px solid #eee;
|
69
|
+
padding: 2px;
|
70
|
+
text-align: right;
|
71
|
+
}
|
72
|
+
table.calendar td.today a {
|
73
|
+
color: red !important;
|
74
|
+
}
|
75
|
+
table.calendar td.back, table.calendar td.forward {
|
76
|
+
background: #ddd;
|
77
|
+
}
|
78
|
+
table.calendar th {
|
79
|
+
background: #eee;
|
80
|
+
}
|
81
|
+
table.calendar a {
|
82
|
+
color: black;
|
83
|
+
text-decoration: none;
|
84
|
+
}
|
85
|
+
table.calendar tbody tr:last-child td {
|
86
|
+
border-bottom: 1px solid black;
|
87
|
+
}
|
88
|
+
table.calendar tbody tr td:first-child {
|
89
|
+
border-left: 1px solid black;
|
90
|
+
}
|
91
|
+
table.calendar tbody tr td:last-child {
|
92
|
+
border-right: 1px solid black;
|
93
|
+
}
|