radiant-forum-extension 2.0.1 → 2.0.2

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/VERSION CHANGED
@@ -1 +1 @@
1
- 2.0.1
1
+ 2.0.2
@@ -4,6 +4,7 @@ class ForumBaseController < ReaderActionController
4
4
  radiant_layout { |c| Radiant::Config['forum.layout'] || Radiant::Config['reader.layout'] }
5
5
  before_filter :require_login_unless_public
6
6
  before_filter :establish_context
7
+ after_filter :clear_site_cache, :only => [:create, :update, :destroy]
7
8
  helper :forum, :reader
8
9
 
9
10
  protected
@@ -54,7 +55,10 @@ protected
54
55
 
55
56
  def render_page_or_feed(template_name = action_name)
56
57
  respond_to do |format|
57
- format.html { render :action => template_name }
58
+ format.html {
59
+ expires_in SiteController.cache_timeout, :public => true, :private => false
60
+ render :action => template_name
61
+ }
58
62
  format.rss { render :action => template_name, :layout => 'feed' }
59
63
  format.js { render :action => template_name, :layout => false }
60
64
  end
@@ -75,4 +79,8 @@ protected
75
79
  false
76
80
  end
77
81
 
82
+ def clear_site_cache
83
+ Radiant::Cache.clear if defined?(Radiant::Cache)
84
+ end
85
+
78
86
  end
@@ -0,0 +1,13 @@
1
+ class PostAttachmentsController < ForumBaseController
2
+
3
+ # this simple controller exists for two reasons: to use sendfile for attachments (but it doesn't, yet)
4
+ # and to allow other extensions (ie group_forum) to restrict access to post attachments
5
+
6
+ def show
7
+ @attachment = PostAttachment.find(params[:id])
8
+ size = params[:size] || 'original'
9
+ expires_in SiteController.cache_timeout, :public => true, :private => false
10
+ send_file @attachment.file.path(size.to_sym), :type => @attachment.file_content_type, :disposition => 'inline'
11
+ end
12
+
13
+ end
@@ -37,7 +37,7 @@ class PostAttachment < ActiveRecord::Base
37
37
  },
38
38
  :whiny_thumbnails => false,
39
39
  :url => "/:class/:id/:basename:no_original_style.:extension",
40
- :path => ":rails_root/public/:class/:id/:basename:no_original_style.:extension"
40
+ :path => ":rails_root/:class/:id/:basename:no_original_style.:extension" # attachments can only be accessed through the PostAttachments controller, in case file security is required
41
41
 
42
42
  attr_protected :file_file_name, :file_content_type, :file_file_size
43
43
  validates_attachment_presence :file, :message => t('error.no_file')
@@ -1,4 +1,4 @@
1
- - add_reader_css '/stylesheets/reader.css'
1
+ - add_reader_css '/stylesheets/forum.css'
2
2
  - if current_reader
3
3
  - if Radiant::Config['forum.toolbar?']
4
4
  - add_reader_js '/punymce/puny_mce_src.js'
@@ -8,7 +8,7 @@
8
8
  - add_reader_js '/punymce/plugins/editsource/editsource.js'
9
9
  - add_reader_js '/punymce/plugins/paste.js'
10
10
  - add_reader_js '/javascripts/forum.js'
11
-
11
+ - add_reader_js '/javascripts/gallery.js'
12
12
 
13
13
  - content_for :section_navigation do
14
14
  = link_to t('navigation.forum'), topics_url, :class => 'section'
@@ -18,6 +18,7 @@
18
18
  = link_to t('navigation.new_topic'), new_post_url
19
19
  - if current_reader
20
20
  = link_to t('navigation.your_account'), reader_account_url
21
+ = link_to t('log_out'), reader_logout_url
21
22
  - else
22
23
  = link_to t('log_in'), reader_login_url
23
24
  - if Radiant::Config['forum.help_url']
@@ -47,3 +48,4 @@
47
48
  - content_for :newtopic do
48
49
  .newmessage
49
50
  = link_to t('new_topic'), new_post_url
51
+
@@ -0,0 +1,26 @@
1
+ %h2
2
+ = t('busy_topics')
3
+ %ul
4
+ - Topic.most_commented(10).each do |topic|
5
+ %li
6
+ = link_to topic.name, forum_topic_url(topic.forum, topic), :class => 'title'
7
+ = t('started_by')
8
+ = link_to(topic.reader.name, reader_url(topic.reader))
9
+ %br
10
+ = topic.posts.count
11
+ = t('posts') + ','
12
+ = t('most_recently')
13
+ = friendly_date(topic.replied_at) + '.'
14
+
15
+ %h2
16
+ = t('busy_readers')
17
+ %ul
18
+ - Reader.most_commenting(10).each do |reader|
19
+ %li
20
+ = link_to reader.name, reader_url(reader), :class => 'title'
21
+ = reader.posts.count
22
+ = t('posts') + ','
23
+ %br
24
+ = t('last_seen')
25
+ = time_ago_in_words(reader.posts.first.created_at)
26
+ = t('ago')
@@ -1,9 +1,9 @@
1
1
  - af ||= false
2
2
  %li
3
3
  - if attachment.image?
4
- = link_to image_tag(attachment.thumbnail), attachment.file.url
4
+ = link_to image_tag(post_attachment_url(attachment, :size => :thumbnail)), post_attachment_url(attachment), :class => 'thumbnail'
5
5
  - else
6
- = link_to attachment.filename, attachment.file.url, :class => "attachment #{attachment.extension}"
6
+ = link_to attachment.filename, post_attachment_url(attachment), :class => "attachment #{attachment.extension}"
7
7
  - if af
8
8
  = af.check_box :_destroy, :class => 'checkbox'
9
9
 
@@ -4,10 +4,10 @@
4
4
  = t('attached') + ':'
5
5
  - post.attachments.images.each do |att|
6
6
  - display_size = Radiant::Config['forum.image_zoom_size'] || :original
7
- = link_to image_tag(att.thumbnail), att.file.url(display_size), :class => :thumbnail
7
+ = link_to image_tag(post_attachment_url(att, :size => :thumbnail)), post_attachment_url(att, :size => display_size), :class => :thumbnail, :rel => post_attachment_url(att, :size => :original)
8
8
  - if post.attachments.non_images.any?
9
9
  %ul.attachments
10
10
  - post.attachments.non_images.each do |att|
11
11
  %li
12
- = link_to att.filename, att.file.url, :class => "attachment #{att.extension}"
12
+ = link_to att.filename, post_attachment_url(att), :class => "attachment #{att.extension}"
13
13
 
@@ -9,6 +9,7 @@
9
9
 
10
10
  - content_for :sidebar do
11
11
  = render :partial => 'forums/latest'
12
+ = render :partial => 'forums/statistics'
12
13
 
13
14
  - content_for :pagination do
14
15
  = pagination_and_summary_for(@topics, t('topics'))
@@ -10,6 +10,8 @@ en:
10
10
  attach_file: "attach a file"
11
11
  author: "author"
12
12
  begun_on: "begun on %{date}"
13
+ busy_topics: "Biggest topics"
14
+ busy_readers: "Biggest talkers"
13
15
  by: "by"
14
16
  by_content_and_author: "by content, author or category."
15
17
  comment: "comment"
@@ -68,6 +70,7 @@ en:
68
70
  name: "Discussion title"
69
71
  sticky: "Sticky"
70
72
  locked: "Locked"
73
+ last_seen: "last here"
71
74
  latest_activity: "latest activity"
72
75
  latest_discussion: "Latest topics"
73
76
  latest_posts: "Latest comments"
data/config/routes.rb CHANGED
@@ -3,6 +3,7 @@ ActionController::Routing::Routes.draw do |map|
3
3
  forum.resources :forums, :only => [:index, :show], :has_many => [:topics]
4
4
  forum.resources :topics, :only => [:index, :show], :has_many => [:posts]
5
5
  forum.resources :posts, :member => { :remove => :get }
6
+ forum.resources :post_attachments, :only => [:show]
6
7
  end
7
8
 
8
9
  map.namespace :admin, :member => { :remove => :get }, :path_prefix => 'admin/forum' do |admin|
data/forum_extension.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  require_dependency 'application_controller'
2
2
 
3
3
  class ForumExtension < Radiant::Extension
4
- version "2.0.1"
4
+ version "2.0.2"
5
5
  description "Nice clean forums and page comments for inclusion in your radiant site. Requires the reader extension and share_layouts."
6
6
  url "http://spanner.org/radiant/forum"
7
7
 
@@ -1,6 +1,17 @@
1
1
  module CommentableModel # for inclusion into ActiveRecord::Base
2
2
  def self.included(base)
3
3
  base.extend ClassMethods
4
+ base.class_eval do
5
+ named_scope :most_commented, lambda { |count|
6
+ {
7
+ :select => "topics.*, count(posts.id) AS post_count",
8
+ :joins => "INNER JOIN posts ON posts.topic_id = topics.id",
9
+ :group => "topics.id",
10
+ :order => "post_count DESC",
11
+ :limit => count
12
+ }
13
+ }
14
+ end
4
15
  end
5
16
 
6
17
  module ClassMethods
data/lib/forum_reader.rb CHANGED
@@ -4,6 +4,16 @@ module ForumReader
4
4
  base.class_eval {
5
5
  has_many :topics, :dependent => :nullify
6
6
  has_many :posts, :order => 'posts.created_at desc', :dependent => :nullify
7
+
8
+ named_scope :most_commenting, lambda { |count|
9
+ {
10
+ :select => "readers.*, count(posts.id) AS post_count",
11
+ :joins => "INNER JOIN posts ON posts.reader_id = readers.id",
12
+ :group => "readers.id",
13
+ :order => "post_count DESC",
14
+ :limit => count
15
+ }
16
+ }
7
17
  }
8
18
  end
9
19
  end
@@ -1,9 +1,11 @@
1
1
  /*
2
2
  Sample jquery-based forum scripts.
3
3
 
4
- Two functions are handled here:
5
4
  * inline editing of posts by authors and administrators.
6
5
  * attachment and upload of files.
6
+ * simple show and hide for first post
7
+ * toolbar hookup
8
+ * submit-button protection
7
9
  */
8
10
 
9
11
  (function($) {
@@ -153,21 +155,14 @@
153
155
  });
154
156
  }
155
157
 
156
- $.tools.post = {
157
- conf: {
158
-
159
- }
160
- };
161
-
162
158
  $.fn.editable_post = function(conf) {
163
- conf = $.extend({}, $.tools.post.conf, conf);
164
159
  this.each(function() {
165
160
  new EditablePost($(this), conf);
166
161
  });
167
162
  return this;
168
163
  };
169
164
 
170
- function FirstPost(container, conf) {
165
+ function HideablePost(container, conf) {
171
166
  var self = this;
172
167
  $.extend(self, {
173
168
  head: container.find('.post_header'),
@@ -175,11 +170,11 @@
175
170
  shower: null,
176
171
  show: function () {
177
172
  self.body.slideDown();
178
- self.shower.text('Hide first post');
173
+ self.shower.text('Hide');
179
174
  },
180
175
  hide: function () {
181
176
  self.body.slideUp();
182
- self.shower.text('Show first post');
177
+ self.shower.text('Show');
183
178
  },
184
179
  toggle: function (event) {
185
180
  squash(event);
@@ -188,14 +183,14 @@
188
183
  }
189
184
  });
190
185
 
191
- self.shower = $('<a href="#" class="shower">Hide first post</a>').appendTo(self.head.find('p.context'));
186
+ self.shower = $('<a href="#" class="shower">Hide</a>').appendTo(self.head.find('p.context'));
192
187
  self.shower.click(self.toggle);
193
188
  if ($('a.prev_page').length > 0) self.hide();
194
189
  }
195
190
 
196
- $.fn.first_post = function() {
191
+ $.fn.hideable_post = function() {
197
192
  this.each(function() {
198
- new FirstPost($(this));
193
+ new HideablePost($(this));
199
194
  });
200
195
  return this;
201
196
  };
@@ -215,7 +210,7 @@
215
210
  addUpload: function(event) {
216
211
  squash(event);
217
212
  var upload_field = self.file_field.clone();
218
- var nest_id = self.uploadCount() + 1;
213
+ var nest_id = self.attachmentCount() + self.uploadCount(); // nb. starts at zero so this total is +1
219
214
  var container = $('<li class="attachment">' + upload_field.val() + '</li>');
220
215
  upload_field.attr("id", upload_field.attr('id').replace(/\d+/, nest_id));
221
216
  upload_field.attr("name", upload_field.attr('name').replace(/\d+/, nest_id));
@@ -245,15 +240,8 @@
245
240
  self.attachments_list.find('li').add_remover();
246
241
  self.file_field.change(self.addUpload);
247
242
  }
248
-
249
- $.tools.stack = {
250
- conf: {
251
-
252
- }
253
- };
254
243
 
255
244
  $.fn.upload_stack = function(conf) {
256
- conf = $.extend({}, $.tools.stack.conf, conf);
257
245
  this.each(function() {
258
246
  el = new UploadStack($(this), conf);
259
247
  });
@@ -442,7 +430,7 @@
442
430
 
443
431
  $(function() {
444
432
  $(".post").editable_post({});
445
- $(".post.first").first_post({});
433
+ $(".post.first").hideable_post({});
446
434
  $(".upload_stack").upload_stack({});
447
435
  $(".toolbarred").add_editor({});
448
436
  $("input:submit").live('click', function (event) {
@@ -0,0 +1,249 @@
1
+ (function($) {
2
+
3
+ /*
4
+ * Some easing functions borrowed from the effects library to save loading the whole lot.
5
+ * Glide is really quartic out. Boing is back out. Bounce is bounce out.
6
+ */
7
+
8
+ $.easing.glide = function (x, t, b, c, d) {
9
+ return -c * ((t=t/d-1)*t*t*t - 1) + b;
10
+ }
11
+
12
+ $.easing.boing = function (x, t, b, c, d, s) {
13
+ if (s == undefined) s = 1.70158;
14
+ return c*((t=t/d-1)*t*((s+1)*t + s) + 1) + b;
15
+ };
16
+
17
+ function Gallery() {
18
+ var self = this;
19
+ var container = $('<div class="gallery"><img class="preview" /><p class="caption" /><div class="controls"><a href="#" class="previous" /><a href="#" class="download" /><a href="#" class="next" /></a></div><div class="closer"><a href="#" /></div></div>').hide().appendTo($('body'));
20
+ $.extend(self, {
21
+ container: container,
22
+ zoomer: new Zoomer(),
23
+ image: container.find('img.preview'),
24
+ caption: container.find('p.caption'),
25
+ controls: container.find('div.controls'),
26
+ closer: container.find('div.closer'),
27
+ stack: [],
28
+ item: null,
29
+ add: function (a) {
30
+ self.stack.push(new GalleryItem(a));
31
+ },
32
+ display: function (item) {
33
+ self.item = item;
34
+ if (self.visible()) self.crossfade();
35
+ else self.zoomUp();
36
+ },
37
+
38
+ mimic: function () {
39
+ self.zoomer.setImage(self.item.src);
40
+ self.image.attr('src', self.item.src);
41
+ if (self.item.caption.html()) self.caption.html(self.item.caption.html()).show();
42
+ else self.caption.hide();
43
+ },
44
+ zoomUp: function () {
45
+ self.hide();
46
+ self.mimic();
47
+ self.zoomer.zoomUp(self.item, self.show);
48
+ },
49
+ zoomDown: function () {
50
+ self.zoomer.zoomDown(self.item);
51
+ self.hide();
52
+ },
53
+ crossfade: function () {
54
+ self.fadeDown(self.fadeUp);
55
+ },
56
+ fadeDown: function (onFade) {
57
+ self.caption.fadeTo('fast', 0.2);
58
+ self.image.fadeTo('fast', 0.2, onFade);
59
+ },
60
+ fadeUp: function (onFade) {
61
+ self.mimic();
62
+ self.resize();
63
+ self.caption.fadeTo('fast', 1);
64
+ self.image.fadeTo('fast', 1, onFade);
65
+ },
66
+
67
+ current: function () {
68
+ return self.stack.indexOf(self.item);
69
+ },
70
+ next: function (e) {
71
+ e.preventDefault();
72
+ var at = self.current();
73
+ var next = (at == self.stack.length-1) ? 0 : at + 1;
74
+ self.display(self.stack[next]);
75
+ },
76
+ previous: function (e) {
77
+ e.preventDefault();
78
+ var at = self.current();
79
+ var previous = (at == 0) ? self.stack.length-1 : at - 1;
80
+ self.display(self.stack[previous]);
81
+ },
82
+ close: function (e) {
83
+ e.preventDefault();
84
+ self.zoomDown();
85
+ },
86
+ show: function () {
87
+ self.zoomer.hide();
88
+ self.container.show();
89
+ },
90
+ hide: function () {
91
+ self.container.hide();
92
+ },
93
+ visible: function () {
94
+ return self.container.is(':visible');
95
+ },
96
+ showControls: function (e) {
97
+ self.controls.fadeIn("fast");
98
+ self.closer.fadeIn("fast");
99
+ self.controls.find('a.download').attr('href', self.item.download_url());
100
+ },
101
+ hideControls: function (e) {
102
+ self.controls.fadeOut("fast");
103
+ self.closer.fadeOut("fast");
104
+ },
105
+
106
+ resize: function (item) {
107
+ if (!item) item = self.item;
108
+ var w = $(window);
109
+ var d = item.imageSize();
110
+ var p = self.container.offset();
111
+ self.image.animate(d, 'fast');
112
+ self.container.animate({
113
+ left: p.left + (self.image.innerWidth() - d.width)/2,
114
+ top: p.top + (self.image.innerHeight() - d.height)/2
115
+ }, 'fast');
116
+ self.controls.css({left: (d.width - 96)/2});
117
+ },
118
+ reposition: function (item) {
119
+ if (!item) item = self.item;
120
+ var w = $(window);
121
+ var d = item.imageSize();
122
+ var p = {
123
+ top: w.scrollTop() + (w.height() - d.height)/2,
124
+ left: w.scrollLeft() + (w.width() - d.width)/2
125
+ };
126
+ if (self.visible) {
127
+ self.image.animate(d, 'fast');
128
+ self.container.animate(p, 'fast');
129
+ } else {
130
+ self.image.css(d);
131
+ self.container.css(p);
132
+ }
133
+ self.controls.css({left: (d.width - 96)/2});
134
+ return $.extend(d,p);
135
+ },
136
+ currentPosition: function () {
137
+ var p = self.container.offset();
138
+ return {
139
+ left: p.left,
140
+ top: p.top,
141
+ width: self.image.innerWidth(),
142
+ height: self.image.innerHeight()
143
+ };
144
+ }
145
+ });
146
+
147
+ self.closer.find('a').click(self.close);
148
+ self.controls.find('a.previous').click(self.previous);
149
+ self.controls.find('a.next').click(self.next);
150
+ self.container.hover(self.showControls, self.hideControls);
151
+ };
152
+
153
+ function Zoomer() {
154
+ var self = this;
155
+ var sprite = $('<img class="grower" />').hide().appendTo($('body'));
156
+ $.extend(self, {
157
+ sprite: sprite,
158
+ defaultUpState: {position: 'absolute', opacity: 1, borderLeftWidth: 20, borderRightWidth: 20, borderTopWidth: 20, borderBottomWidth: 20},
159
+ defaultDownState: {position: 'absolute', opacity: 0, borderLeftWidth: 4, borderRightWidth: 4, borderTopWidth: 4, borderBottomWidth: 4},
160
+ zoomDuration: 'slow',
161
+ setImage: function (src) {
162
+ self.sprite.attr('src', src);
163
+ },
164
+ zoomUp: function (item, onZoom) {
165
+ if (!onZoom) onZoom = self.hide;
166
+ self.sprite.css($.extend(self.defaultDownState, item.position()));
167
+ self.show();
168
+ self.sprite.animate($.extend({}, self.defaultUpState, $.gallery.reposition()), self.zoomDuration, onZoom);
169
+ },
170
+ zoomDown: function (item, onZoom) {
171
+ if (!onZoom) onZoom = self.hide;
172
+ self.show();
173
+ self.sprite.css($.extend(self.defaultUpState, $.gallery.currentPosition()));
174
+ self.sprite.animate($.extend({}, self.defaultDownState, item.position()), self.zoomDuration, onZoom);
175
+ },
176
+ show: function () {
177
+ self.sprite.show();
178
+ },
179
+ hide: function () {
180
+ self.sprite.hide();
181
+ }
182
+ });
183
+ }
184
+
185
+
186
+ function GalleryItem(a) {
187
+ var self = this;
188
+ var link = $(a);
189
+ $.extend(self, {
190
+ link: link,
191
+ thumb: link.find('img'),
192
+ image: $('<img class="preloader" />').css({visibility: 'hidden'}).appendTo($('body')),
193
+ caption: link.next('.caption'),
194
+ src: link.attr('href'),
195
+ deactivate: function (event) {
196
+ self.thumb.fadeTo('slow', 0.3);
197
+ self.thumb.css('cursor', 'text');
198
+ self.image.bind("load", self.activate);
199
+ self.image.attr('src', self.src);
200
+ },
201
+ activate: function () {
202
+ self.thumb.fadeTo('slow', 1);
203
+ self.thumb.css('cursor', 'pointer');
204
+ self.link.click(function (e) {
205
+ if (e) {
206
+ e.preventDefault();
207
+ e.stopPropagation();
208
+ }
209
+ $.gallery.display(self);
210
+ });
211
+ },
212
+ position: function () {
213
+ var p = self.thumb.offset();
214
+ return {
215
+ left: p.left,
216
+ top: p.top,
217
+ width: self.thumb.outerWidth(),
218
+ height: self.thumb.outerHeight()
219
+ };
220
+ },
221
+ imageSize: function () {
222
+ return {
223
+ width: self.image.innerWidth(),
224
+ height: self.image.innerHeight()
225
+ };
226
+ },
227
+ download_url: function () {
228
+ return self.link.attr('rel');
229
+ }
230
+ });
231
+ if (!self.image.complete) {
232
+ self.deactivate();
233
+ } else {
234
+ self.activate();
235
+ }
236
+ }
237
+
238
+ $.fn.galleried = function() {
239
+ this.each(function() {
240
+ if (!$.gallery) $.gallery = new Gallery();
241
+ $.gallery.add(this);
242
+ });
243
+ };
244
+
245
+ })(jQuery);
246
+
247
+ $(function() {
248
+ $("a.thumbnail").galleried();
249
+ });