From af1c14939e443cf37ca981756bdbbc804d118c40 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Fri, 7 Jun 2013 17:15:49 -0700 Subject: [PATCH 1/3] Add 'dynamic favicon' setting --- app/assets/images/default-favicon.ico | Bin 0 -> 1342 bytes app/assets/javascripts/discourse.js | 8 + .../external/jquery.faviconNotify.js | 224 ++++++++++++++++++ app/models/site_setting.rb | 3 +- config/locales/server.en.yml | 1 + 5 files changed, 235 insertions(+), 1 deletion(-) create mode 100644 app/assets/images/default-favicon.ico create mode 100644 app/assets/javascripts/external/jquery.faviconNotify.js diff --git a/app/assets/images/default-favicon.ico b/app/assets/images/default-favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..d20ae8eeded413f3de968472303a4dbc2a63409c GIT binary patch literal 1342 zcmd^8ZBJ5R7=98VA)LGk2*<-opa2sX2uMIXK_&%QCI}&FTf`hq#7eUjAtPGNm+C_s zt@_lL-1=5Ou*sV)9?3-&<_atZy+oH357zj(AU?u1lbE*1MWkg!~LcH z{(iED2zX(qI|2>S_U`at`h9!TrMY1CX<{>rAmiFp$g7;2s_nAHLiI) zo_W->0hwyGjg5^x4hDlB$VUiy>P#lH5sSqd0|Nt8Ukp9YTP&6*kmG0igT&{1Y?p!DwX;Q`SM}-v8+%i)M%z~o&$WGAnbw9 zU^aR5bU&R=(+p3fdcD4czHKAm24Q5xz6-u6ld)N^*V~0Zb7C?awOVZz>;Z5cciwhg zaJ}g1>7g0;(3f|mQt4T<*`(DyLY+>>5Do`Aa8SRq*v~Bb7v?zb8+5BgBFRlm1U?|Y z-fp+gVt$g}hqPL)P^D6-LB9o1k7}6R!K@}HCtGs4{3UEk^t6io>8`A-v~gcyXXkYu zd~13-^B(>JUsTP4N&YApbFd_92}go*=(mFA4|b-IBU1tx6qemnM_7+q}`@| Ng)w*j*rfm1_!9uNhV}pe literal 0 HcmV?d00001 diff --git a/app/assets/javascripts/discourse.js b/app/assets/javascripts/discourse.js index 954d8ad1b37..33e1627ce39 100644 --- a/app/assets/javascripts/discourse.js +++ b/app/assets/javascripts/discourse.js @@ -85,6 +85,14 @@ Discourse = Ember.Application.createWithMixins({ }, 200); }.observes('title', 'hasFocus', 'notifyCount'), + faviconChanged: function() { + if(Discourse.SiteSettings.dynamic_favicon) { + $.faviconNotify( + Discourse.SiteSettings.favicon_url, this.get('notifyCount') + ); + } + }.observes('notifyCount'), + // The classes of buttons to show on a post postButtons: function() { return Discourse.SiteSettings.post_menu.split("|").map(function(i) { diff --git a/app/assets/javascripts/external/jquery.faviconNotify.js b/app/assets/javascripts/external/jquery.faviconNotify.js new file mode 100644 index 00000000000..2b071e30e7c --- /dev/null +++ b/app/assets/javascripts/external/jquery.faviconNotify.js @@ -0,0 +1,224 @@ +/** + * jQuery Favicon Notify + * + * Updates the favicon to notify the user of changes. In the original tests I + * had an embedded font collection to allow any charachers - I decided that the + * ~130Kb and added complexity was overkill. As such it now uses a manual glyph + * set meaning that only numerical notifications are possible. + * + * Dual licensed under the MIT and GPL licenses: + * + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + * + * @author David King + * @copyright Copyright (c) 2011 + + * @url oodavid.com + */ +(function($){ + var canvas; + var bg = '#000000'; + var fg = '#FFFFFF'; + var pos = 'br'; + $.faviconNotify = function(icon, num, myPos, myBg, myFg){ + // Default the positions + myPos = myPos || pos; + myFg = myFg || fg; + myBg = myBg || bg; + // Create a canvas if we need one + canvas = canvas || $('')[0]; + if(canvas.getContext){ + // Load the icon + $('').load(function(e){ + // Load the icon into the canvas + canvas.height = canvas.width = 16; + var ctx = canvas.getContext('2d'); + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.drawImage(this, 0, 0); + // We gots num? + if(num !== undefined){ + num = parseFloat(num, 10); + // Convert the num into a glyphs array + var myGlyphs = []; + if(num > 99){ + myGlyphs.push(glyphs['LOTS']); + } else { + num = num.toString().split(''); + $.each(num, function(k,v){ + myGlyphs.push(glyphs[v]); + }); + } + // Merge the glyphs together + var combined = []; + var glyphHeight = myGlyphs[0].length; + $.each(myGlyphs, function(k,v){ + for(y=0; y').attr('href', canvas.toDataURL('image/png'))); + }).attr('src', icon) + } + }; + var glyphs = { + '0': [ + ' --- ', + ' -@@@- ', + '-@---@-', + '-@- -@-', + '-@- -@-', + '-@- -@-', + '-@---@-', + ' -@@@- ', + ' --- ' ], + '1': [ + ' - ', + ' -@- ', + '-@@- ', + ' -@- ', + ' -@- ', + ' -@- ', + ' -@- ', + '-@@@-', + ' --- ' ], + '2': [ + ' --- ', + ' -@@@- ', + '-@---@-', + ' - --@-', + ' -@@- ', + ' -@-- ', + '-@---- ', + '-@@@@@-', + ' ----- ' ], + '3': [ + ' --- ', + ' -@@@- ', + '-@---@-', + ' - --@-', + ' -@@- ', + ' - --@-', + '-@---@-', + ' -@@@- ', + ' --- ' ], + '4': [ + ' -- ', + ' -@@-', + ' -@-@-', + ' -@--@-', + '-@---@-', + '-@@@@@-', + ' ----@-', + ' -@-', + ' - ' ], + '5': [ + ' ----- ', + '-@@@@@-', + '-@---- ', + '-@--- ', + '-@@@@- ', + ' ----@-', + '-@---@-', + ' -@@@- ', + ' --- ' ], + '6': [ + ' --- ', + ' -@@@- ', + '-@---@-', + '-@---- ', + '-@@@@- ', + '-@---@-', + '-@---@-', + ' -@@@- ', + ' --- ' ], + '7': [ + ' ----- ', + '-@@@@@-', + ' ----@-', + ' -@- ', + ' -@- ', + ' -@- ', + ' -@- ', + ' -@- ', + ' - ' ], + '8': [ + ' --- ', + ' -@@@- ', + '-@---@-', + '-@---@-', + ' -@@@- ', + '-@---@-', + '-@---@-', + ' -@@@- ', + ' --- ' ], + '9': [ + ' --- ', + ' -@@@- ', + '-@---@-', + '-@---@-', + ' -@@@@-', + ' ----@-', + '-@---@-', + ' -@@@- ', + ' --- ' ], + '!': [ + ' - ', + '-@-', + '-@-', + '-@-', + '-@-', + '-@-', + ' - ', + '-@-', + ' - ' ], + '.': [ + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' - ', + '-@-', + ' - ' ], + 'LOTS': [ + ' - -- --- -- ', + '-@- -@@-@@@--@@-', + '-@--@--@-@--@- ', + '-@--@--@-@--@- ', + '-@--@--@-@- -@- ', + '-@--@--@-@- -@-', + '-@--@--@-@----@-', + '-@@@-@@--@-@@@- ', + ' --- -- - --- ' + ] + }; +})(jQuery); diff --git a/app/models/site_setting.rb b/app/models/site_setting.rb index e5b99a92260..93fee5b34ba 100644 --- a/app/models/site_setting.rb +++ b/app/models/site_setting.rb @@ -79,7 +79,8 @@ class SiteSetting < ActiveRecord::Base setting(:invite_expiry_days, 14) setting(:active_user_rate_limit_secs, 60) setting(:previous_visit_timeout_hours, 1) - setting(:favicon_url, '/assets/default-favicon.png') + client_setting(:favicon_url, '/assets/default-favicon.ico') + client_setting(:dynamic_favicon, false) setting(:apple_touch_icon_url, '/assets/default-apple-touch-icon.png') setting(:ninja_edit_window, 5.minutes.to_i) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 47f225f6e61..9e689872a53 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -460,6 +460,7 @@ en: logo_url: "The logo for your site eg: http://example.com/logo.png" logo_small_url: "The small logo for your site used when scrolling down on topics eg: http://example.com/logo-small.png" favicon_url: "A favicon for your site, see http://en.wikipedia.org/wiki/Favicon" + dynamic_favicon: "Show incoming message notifications on favicon" apple_touch_icon_url: "Icon used for Apple touch devices. Recommended size is 144px by 144px." notification_email: "The return email address used when sending system emails such as notifying users of lost passwords, new accounts etc" From d741798d9c2e94972da3432c40963b7cff5fa92b Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Sat, 8 Jun 2013 10:27:36 -0700 Subject: [PATCH 2/3] Add onebox for GitHub pull requests --- lib/oneboxer/github_pullrequest_onebox.rb | 26 +++++++++++++ .../templates/github_pullrequest_onebox.hbrs | 39 +++++++++++++++++++ .../github_pullrequest_onebox_spec.rb | 17 ++++++++ 3 files changed, 82 insertions(+) create mode 100644 lib/oneboxer/github_pullrequest_onebox.rb create mode 100644 lib/oneboxer/templates/github_pullrequest_onebox.hbrs create mode 100644 spec/components/oneboxer/github_pullrequest_onebox_spec.rb diff --git a/lib/oneboxer/github_pullrequest_onebox.rb b/lib/oneboxer/github_pullrequest_onebox.rb new file mode 100644 index 00000000000..e3b374be07f --- /dev/null +++ b/lib/oneboxer/github_pullrequest_onebox.rb @@ -0,0 +1,26 @@ +require_dependency 'oneboxer/handlebars_onebox' + +module Oneboxer + class GithubPullrequestOnebox < HandlebarsOnebox + + matcher /^https?:\/\/(?:www\.)?github\.com\/[^\/]+\/[^\/]+\/pull\/.+/ + favicon 'github.png' + + def translate_url + @url.match( + /github\.com\/(?[^\/]+)\/(?[^\/]+)\/pull\/(?[^\/]+)/mi + ) do |match| + "https://api.github.com/repos/#{match[:owner]}/#{match[:repo]}/pulls/#{match[:number]}" + end + end + + def parse(data) + result = JSON.parse(data) + + result['created_at'] = + Time.parse(result['created_at']).strftime("%I:%M%p - %d %b %y") + + result + end + end +end diff --git a/lib/oneboxer/templates/github_pullrequest_onebox.hbrs b/lib/oneboxer/templates/github_pullrequest_onebox.hbrs new file mode 100644 index 00000000000..cd4be7d6c8f --- /dev/null +++ b/lib/oneboxer/templates/github_pullrequest_onebox.hbrs @@ -0,0 +1,39 @@ +
+ {{#host}} + + {{/host}} + +
+ {{#user.avatar_url}} + + {{user.login}} + + {{/user.avatar_url}} + +

+ {{title}} +

+ + + +
+ {{commits}} commits + changed {{changed_files}} files + with {{additions}} additions + and {{deletions}} deletions. +
+
+
+
diff --git a/spec/components/oneboxer/github_pullrequest_onebox_spec.rb b/spec/components/oneboxer/github_pullrequest_onebox_spec.rb new file mode 100644 index 00000000000..378b8531609 --- /dev/null +++ b/spec/components/oneboxer/github_pullrequest_onebox_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' +require 'oneboxer' +require 'oneboxer/github_pullrequest_onebox' + +describe Oneboxer::GithubPullrequestOnebox do + describe '#translate_url' do + it 'returns the api url for the given pull request' do + onebox = described_class.new( + 'https://github.com/discourse/discourse/pull/988' + ) + expect(onebox.translate_url).to eq( + 'https://api.github.com/repos/discourse/discourse/pulls/988' + ) + end + end +end + From 3fdba0019bd7088356535817f86e3ae3b906a3b3 Mon Sep 17 00:00:00 2001 From: Navin Date: Sun, 9 Jun 2013 18:48:44 +0200 Subject: [PATCH 3/3] Extract callbacks and validations for Post Move Post create callbacks to PostCreate Extract Post validations Move stripped_length_validator to lib/validators --- app/models/post.rb | 82 ++----------------- lib/post_creator.rb | 29 ++++++- lib/validators/post_validator.rb | 56 +++++++++++++ .../validators}/stripped_length_validator.rb | 0 4 files changed, 89 insertions(+), 78 deletions(-) create mode 100644 lib/validators/post_validator.rb rename {app/models => lib/validators}/stripped_length_validator.rb (100%) diff --git a/app/models/post.rb b/app/models/post.rb index 992ae4d2bd5..08991e6eb80 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -30,20 +30,16 @@ class Post < ActiveRecord::Base validates_presence_of :raw, :user_id, :topic_id validates :raw, stripped_length: { in: -> { SiteSetting.post_length } } - validate :raw_quality - validate :max_mention_validator - validate :max_images_validator - validate :max_links_validator - validate :unique_post_validator + validates_with PostValidator # We can pass a hash of image sizes when saving to prevent crawling those images attr_accessor :image_sizes, :quoted_post_numbers, :no_bump, :invalidate_oneboxes SHORT_POST_CHARS = 1200 - scope :by_newest, order('created_at desc, id desc') - scope :by_post_number, order('post_number ASC') - scope :with_user, includes(:user) + scope :by_newest, -> { order('created_at desc, id desc') } + scope :by_post_number, -> { order('post_number ASC') } + scope :with_user, -> { includes(:user) } scope :public_posts, -> { joins(:topic).where('topics.archetype <> ?', Archetype.private_message) } scope :private_posts, -> { joins(:topic).where('topics.archetype = ?', Archetype.private_message) } scope :with_topic_subtype, ->(subtype) { joins(:topic).where('topics.subtype = ?', subtype) } @@ -61,24 +57,6 @@ class Post < ActiveRecord::Base update_flagged_posts_count end - def raw_quality - sentinel = TextSentinel.body_sentinel(raw) - errors.add(:raw, I18n.t(:is_invalid)) unless sentinel.valid? - end - - # Stop us from posting the same thing too quickly - def unique_post_validator - return if SiteSetting.unique_posts_mins == 0 - return if acting_user.admin? || acting_user.moderator? - - # If the post is empty, default to the validates_presence_of - return if raw.blank? - - if $redis.exists(unique_post_key) - errors.add(:raw, I18n.t(:just_posted_that)) - end - end - # The key we use in redis to ensure unique posts def unique_post_key "post-#{user_id}:#{raw_hash}" @@ -124,25 +102,6 @@ class Post < ActiveRecord::Base @acting_user = pu end - # Ensure maximum amount of mentions in a post - def max_mention_validator - if acting_user_is_trusted? - add_error_if_count_exceeded(:too_many_mentions, raw_mentions.size, SiteSetting.max_mentions_per_post) - else - add_error_if_count_exceeded(:too_many_mentions_newuser, raw_mentions.size, SiteSetting.newuser_max_mentions_per_post) - end - end - - # Ensure new users can not put too many images in a post - def max_images_validator - add_error_if_count_exceeded(:too_many_images, image_count, SiteSetting.newuser_max_images) unless acting_user_is_trusted? - end - - # Ensure new users can not put too many links in a post - def max_links_validator - add_error_if_count_exceeded(:too_many_links, link_count, SiteSetting.newuser_max_links) unless acting_user_is_trusted? - end - def total_hosts_usage hosts = linked_hosts.clone @@ -291,37 +250,14 @@ class Post < ActiveRecord::Base PostRevisor.new(self).revise!(updated_by, new_raw, opts) end - - # TODO: move into PostCreator - # Various callbacks before_create do - if reply_to_post_number.present? - self.reply_to_user_id ||= Post.select(:user_id).where(topic_id: topic_id, post_number: reply_to_post_number).first.try(:user_id) - end - - self.post_number ||= Topic.next_post_number(topic_id, reply_to_post_number.present?) - self.cooked ||= cook(raw, topic_id: topic_id) - self.sort_order = post_number - DiscourseEvent.trigger(:before_create_post, self) - self.last_version_at ||= Time.now + PostCreator.before_create_tasks(self) end # TODO: Move some of this into an asynchronous job? # TODO: Move into PostCreator after_create do - - Rails.logger.info (">" * 30) + "#{no_bump} #{created_at}" - # Update attributes on the topic - featured users and last posted. - attrs = {last_posted_at: created_at, last_post_user_id: user_id} - attrs[:bumped_at] = created_at unless no_bump - topic.update_attributes(attrs) - - # Update topic user data - TopicUser.change(user, - topic.id, - posted: true, - last_read_post_number: post_number, - seen_post_count: post_number) + PostCreator.after_create_tasks(self) end # This calculates the geometric mean of the post timings and stores it along with @@ -401,13 +337,7 @@ class Post < ActiveRecord::Base private - def acting_user_is_trusted? - acting_user.present? && acting_user.has_trust_level?(:basic) - end - def add_error_if_count_exceeded(key_for_translation, current_count, max_count) - errors.add(:base, I18n.t(key_for_translation, count: max_count)) if current_count > max_count - end def parse_quote_into_arguments(quote) return {} unless quote.present? diff --git a/lib/post_creator.rb b/lib/post_creator.rb index 0f39dda50b6..529e35ece59 100644 --- a/lib/post_creator.rb +++ b/lib/post_creator.rb @@ -73,12 +73,37 @@ class PostCreator @post end - - # Shortcut def self.create(user, opts) PostCreator.new(user, opts).create end + def self.before_create_tasks(post) + if post.reply_to_post_number.present? + post.reply_to_user_id ||= Post.select(:user_id).where(topic_id: post.topic_id, post_number: post.reply_to_post_number).first.try(:user_id) + end + + post.post_number ||= Topic.next_post_number(post.topic_id, post.reply_to_post_number.present?) + post.cooked ||= post.cook(post.raw, topic_id: post.topic_id) + post.sort_order = post.post_number + DiscourseEvent.trigger(:before_create_post, post) + post.last_version_at ||= Time.now + end + + def self.after_create_tasks(post) + Rails.logger.info (">" * 30) + "#{post.no_bump} #{post.created_at}" + # Update attributes on the topic - featured users and last posted. + attrs = {last_posted_at: post.created_at, last_post_user_id: post.user_id} + attrs[:bumped_at] = post.created_at unless post.no_bump + post.topic.update_attributes(attrs) + + # Update topic user data + TopicUser.change(post.user, + post.topic.id, + posted: true, + last_read_post_number: post.post_number, + seen_post_count: post.post_number) + end + protected def secure_group_ids(topic) diff --git a/lib/validators/post_validator.rb b/lib/validators/post_validator.rb new file mode 100644 index 00000000000..7197c7bd6bd --- /dev/null +++ b/lib/validators/post_validator.rb @@ -0,0 +1,56 @@ +class PostValidator < ActiveModel::Validator + def validate(record) + raw_quality(record) + max_mention_validator(record) + max_images_validator(record) + max_links_validator(record) + unique_post_validator(record) + end + + def raw_quality(post) + sentinel = TextSentinel.body_sentinel(post.raw) + post.errors.add(:raw, I18n.t(:is_invalid)) unless sentinel.valid? + end + + # Ensure maximum amount of mentions in a post + def max_mention_validator(post) + if acting_user_is_trusted?(post) + add_error_if_count_exceeded(post, :too_many_mentions, post.raw_mentions.size, SiteSetting.max_mentions_per_post) + else + add_error_if_count_exceeded(post, :too_many_mentions_newuser, post.raw_mentions.size, SiteSetting.newuser_max_mentions_per_post) + end + end + + # Ensure new users can not put too many images in a post + def max_images_validator(post) + add_error_if_count_exceeded(post, :too_many_images, post.image_count, SiteSetting.newuser_max_images) unless acting_user_is_trusted?(post) + end + + # Ensure new users can not put too many links in a post + def max_links_validator(post) + add_error_if_count_exceeded(post, :too_many_links, post.link_count, SiteSetting.newuser_max_links) unless acting_user_is_trusted?(post) + end + + # Stop us from posting the same thing too quickly + def unique_post_validator(post) + return if SiteSetting.unique_posts_mins == 0 + return if post.acting_user.admin? || post.acting_user.moderator? + + # If the post is empty, default to the validates_presence_of + return if post.raw.blank? + + if $redis.exists(post.unique_post_key) + post.errors.add(:raw, I18n.t(:just_posted_that)) + end + end + + private + + def acting_user_is_trusted?(post) + post.acting_user.present? && post.acting_user.has_trust_level?(:basic) + end + + def add_error_if_count_exceeded(post, key_for_translation, current_count, max_count) + post.errors.add(:base, I18n.t(key_for_translation, count: max_count)) if current_count > max_count + end +end diff --git a/app/models/stripped_length_validator.rb b/lib/validators/stripped_length_validator.rb similarity index 100% rename from app/models/stripped_length_validator.rb rename to lib/validators/stripped_length_validator.rb