FEATURE: First Quote badge

This commit is contained in:
Sam 2014-07-11 14:17:01 +10:00
parent 02158e59b2
commit 89fc989adb
9 changed files with 199 additions and 128 deletions

View File

@ -1,6 +1,10 @@
module Jobs module Jobs
class BadgeGrant < Jobs::Scheduled class BadgeGrant < Jobs::Scheduled
def self.run
self.new.execute(nil)
end
every 1.day every 1.day
def execute(args) def execute(args)

View File

@ -11,12 +11,26 @@ class Badge < ActiveRecord::Base
FirstShare = 12 FirstShare = 12
FirstFlag = 13 FirstFlag = 13
FirstLink = 14 FirstLink = 14
FirstQuote = 15
# other consts # other consts
AutobiographerMinBioLength = 10 AutobiographerMinBioLength = 10
module Queries module Queries
FirstQuote = <<SQL
SELECT l.user_id, l.post_id, l.created_at granted_at
FROM
(
SELECT MIN(l1.id) id
FROM topic_links l1
JOIN badge_posts p1 ON p1.id = l1.post_id
JOIN badge_posts p2 ON p2.id = l1.link_post_id
WHERE NOT reflection AND quote
GROUP BY l1.user_id
) ids
JOIN topic_links l ON l.id = ids.id
SQL
FirstLink = <<SQL FirstLink = <<SQL
SELECT l.user_id, l.post_id, l.created_at granted_at SELECT l.user_id, l.post_id, l.created_at granted_at
@ -26,7 +40,7 @@ class Badge < ActiveRecord::Base
FROM topic_links l1 FROM topic_links l1
JOIN badge_posts p1 ON p1.id = l1.post_id JOIN badge_posts p1 ON p1.id = l1.post_id
JOIN badge_posts p2 ON p2.id = l1.link_post_id JOIN badge_posts p2 ON p2.id = l1.link_post_id
WHERE NOT reflection AND p1.topic_id <> p2.topic_id WHERE NOT reflection AND p1.topic_id <> p2.topic_id AND not quote
GROUP BY l1.user_id GROUP BY l1.user_id
) ids ) ids
JOIN topic_links l ON l.id = ids.id JOIN topic_links l ON l.id = ids.id

View File

@ -104,12 +104,13 @@ class TopicLink < ActiveRecord::Base
PrettyText PrettyText
.extract_links(post.cooked) .extract_links(post.cooked)
.map{|u| [u, URI.parse(u)] rescue nil} .map{|u| [u, URI.parse(u.url)] rescue nil}
.reject{|u,p| p.nil?} .reject{|u,p| p.nil?}
.uniq{|u,p| u} .uniq{|u,p| p}
.each do |url, parsed| .each do |link, parsed|
begin begin
url = link.url
internal = false internal = false
topic_id = nil topic_id = nil
post_number = nil post_number = nil
@ -157,7 +158,9 @@ class TopicLink < ActiveRecord::Base
domain: parsed.host || Discourse.current_hostname, domain: parsed.host || Discourse.current_hostname,
internal: internal, internal: internal,
link_topic_id: topic_id, link_topic_id: topic_id,
link_post_id: reflected_post.try(:id)) link_post_id: reflected_post.try(:id),
quote: link.is_quote
)
# Create the reflection if we can # Create the reflection if we can
if topic_id.present? if topic_id.present?

View File

@ -1993,3 +1993,6 @@ en:
first_link: first_link:
name: First Link name: First Link
description: Added an internal link to another topic description: Added an internal link to another topic
first_quote:
name: First Quote
description: Quoted a user

View File

@ -27,6 +27,15 @@ Badge.seed do |b|
b.query = Badge::Queries::FirstLink b.query = Badge::Queries::FirstLink
end end
Badge.seed do |b|
b.id = Badge::FirstQuote
b.name = "First Quote"
b.badge_type_id = BadgeType::Bronze
b.multiple_grant = false
b.target_posts = true
b.query = Badge::Queries::FirstQuote
end
Badge.seed do |b| Badge.seed do |b|
b.id = Badge::FirstLike b.id = Badge::FirstLike
b.name = "First Like" b.name = "First Like"

View File

@ -0,0 +1,23 @@
class AddIsQuoteToTopicLinks < ActiveRecord::Migration
def up
add_column :topic_links, :quote, :boolean, default: false, null: false
# a primitive backfill, eventual rebake will catch missing
execute "
UPDATE topic_links
SET quote = true
WHERE id IN (
SELECT l.id
FROM topic_links l
JOIN posts p ON p.id = l.post_id
JOIN posts lp ON l.link_post_id = lp.id
WHERE p.raw LIKE '%\[quote=%post:' ||
lp.post_number::varchar || ',%topic:' ||
lp.topic_id::varchar || '%\]%\[/quote]%'
)"
end
def down
remove_column :topic_links, :quote
end
end

View File

@ -203,13 +203,29 @@ module PrettyText
doc.to_html doc.to_html
end end
class DetectedLink
attr_accessor :is_quote, :url
def initialize(url, is_quote=false)
@url = url
@is_quote = is_quote
end
end
def self.extract_links(html) def self.extract_links(html)
links = [] links = []
doc = Nokogiri::HTML.fragment(html) doc = Nokogiri::HTML.fragment(html)
# remove href inside quotes # remove href inside quotes
doc.css("aside.quote a").each { |l| l["href"] = "" } doc.css("aside.quote a").each { |l| l["href"] = "" }
# extract all links from the post # extract all links from the post
doc.css("a").each { |l| links << l["href"] unless l["href"].blank? } doc.css("a").each { |l|
unless l["href"].blank?
links << DetectedLink.new(l["href"])
end
}
# extract links to quotes # extract links to quotes
doc.css("aside.quote[data-topic]").each do |a| doc.css("aside.quote[data-topic]").each do |a|
topic_id = a['data-topic'] topic_id = a['data-topic']
@ -219,7 +235,7 @@ module PrettyText
url << "/#{post_number}" url << "/#{post_number}"
end end
links << url links << DetectedLink.new(url, true)
end end
links links

View File

@ -139,27 +139,37 @@ describe PrettyText do
PrettyText.extract_links("<aside class='quote'>not a linked quote</aside>\n").to_a.should be_empty PrettyText.extract_links("<aside class='quote'>not a linked quote</aside>\n").to_a.should be_empty
end end
def extract_urls(text)
PrettyText.extract_links(text).map(&:url).to_a
end
it "should be able to extract links" do it "should be able to extract links" do
PrettyText.extract_links("<a href='http://cnn.com'>http://bla.com</a>").to_a.should == ["http://cnn.com"] extract_urls("<a href='http://cnn.com'>http://bla.com</a>").should == ["http://cnn.com"]
end end
it "should extract links to topics" do it "should extract links to topics" do
PrettyText.extract_links("<aside class=\"quote\" data-topic=\"321\">aside</aside>").to_a.should == ["/t/topic/321"] extract_urls("<aside class=\"quote\" data-topic=\"321\">aside</aside>").should == ["/t/topic/321"]
end end
it "should extract links to posts" do it "should extract links to posts" do
PrettyText.extract_links("<aside class=\"quote\" data-topic=\"1234\" data-post=\"4567\">aside</aside>").to_a.should == ["/t/topic/1234/4567"] extract_urls("<aside class=\"quote\" data-topic=\"1234\" data-post=\"4567\">aside</aside>").should == ["/t/topic/1234/4567"]
end end
it "should not extract links inside quotes" do it "should not extract links inside quotes" do
PrettyText.extract_links(" links = PrettyText.extract_links("
<a href='http://body_only.com'>http://useless1.com</a> <a href='http://body_only.com'>http://useless1.com</a>
<aside class=\"quote\" data-topic=\"1234\"> <aside class=\"quote\" data-topic=\"1234\">
<a href='http://body_and_quote.com'>http://useless3.com</a> <a href='http://body_and_quote.com'>http://useless3.com</a>
<a href='http://quote_only.com'>http://useless4.com</a> <a href='http://quote_only.com'>http://useless4.com</a>
</aside> </aside>
<a href='http://body_and_quote.com'>http://useless2.com</a> <a href='http://body_and_quote.com'>http://useless2.com</a>
").to_a.should == ["http://body_only.com", "http://body_and_quote.com", "/t/topic/1234"] ")
links.map{|l| [l.url, l.is_quote]}.to_a.sort.should ==
[["http://body_only.com",false],
["http://body_and_quote.com", false],
["/t/topic/1234",true]
].sort
end end
it "should not preserve tags in code blocks" do it "should not preserve tags in code blocks" do

View File

@ -2,194 +2,187 @@ require 'spec_helper'
describe TopicLink do describe TopicLink do
it { should belong_to :topic }
it { should belong_to :post }
it { should belong_to :user }
it { should have_many :topic_link_clicks }
it { should validate_presence_of :url } it { should validate_presence_of :url }
def test_uri def test_uri
URI.parse(Discourse.base_url) URI.parse(Discourse.base_url)
end end
before do let(:topic) do
@topic = Fabricate(:topic, title: 'unique topic name') Fabricate(:topic, title: 'unique topic name')
@user = @topic.user end
let(:user) do
topic.user
end end
it "can't link to the same topic" do it "can't link to the same topic" do
ftl = TopicLink.new(url: "/t/#{@topic.id}", ftl = TopicLink.new(url: "/t/#{topic.id}",
topic_id: @topic.id, topic_id: topic.id,
link_topic_id: @topic.id) link_topic_id: topic.id)
ftl.valid?.should be_false ftl.valid?.should be_false
end end
describe 'external links' do describe 'external links' do
before do before do
@post = Fabricate(:post, raw: " post = Fabricate(:post, raw: "
http://a.com/ http://a.com/
http://b.com/b http://b.com/b
http://#{'a'*200}.com/invalid http://#{'a'*200}.com/invalid
http://b.com/#{'a'*500} http://b.com/#{'a'*500}
", user: @user, topic: @topic) ", user: user, topic: topic)
TopicLink.extract_from(@post) TopicLink.extract_from(post)
end end
it 'works' do it 'works' do
# has the forum topic links # has the forum topic links
@topic.topic_links.count.should == 2 topic.topic_links.count.should == 2
# works with markdown links # works with markdown links
@topic.topic_links.exists?(url: "http://a.com/").should be_true topic.topic_links.exists?(url: "http://a.com/").should be_true
#works with markdown links followed by a period #works with markdown links followed by a period
@topic.topic_links.exists?(url: "http://b.com/b").should be_true topic.topic_links.exists?(url: "http://b.com/b").should be_true
end end
end end
describe 'internal links' do describe 'internal links' do
context "rendered onebox" do it "extracts onebox" do
other_topic = Fabricate(:topic, user: user)
other_topic.posts.create(user: user, raw: "some content for the first post")
other_post = other_topic.posts.create(user: user, raw: "some content for the second post")
before do url = "http://#{test_uri.host}/t/#{other_topic.slug}/#{other_topic.id}/#{other_post.post_number}"
@other_topic = Fabricate(:topic, user: @user) invalid_url = "http://#{test_uri.host}/t/#{other_topic.slug}/9999999999999999999999999999999"
@other_topic.posts.create(user: @user, raw: "some content for the first post")
@other_post = @other_topic.posts.create(user: @user, raw: "some content for the second post")
@url = "http://#{test_uri.host}/t/#{@other_topic.slug}/#{@other_topic.id}/#{@other_post.post_number}" topic.posts.create(user: user, raw: 'initial post')
@invalid_url = "http://#{test_uri.host}/t/#{@other_topic.slug}/9999999999999999999999999999999" post = topic.posts.create(user: user, raw: "Link to another topic:\n\n#{url}\n\n#{invalid_url}")
post.reload
@topic.posts.create(user: @user, raw: 'initial post') TopicLink.extract_from(post)
@post = @topic.posts.create(user: @user, raw: "Link to another topic:\n\n#{@url}\n\n#{@invalid_url}")
@post.reload
TopicLink.extract_from(@post)
@link = @topic.topic_links.first
end
it 'works' do
# should have a link
@link.should be_present
# should be the canonical URL
@link.url.should == @url
end
link = topic.topic_links.first
# should have a link
link.should be_present
# should be the canonical URL
link.url.should == url
end end
context 'topic link' do context 'topic link' do
before do
@other_topic = Fabricate(:topic, user: @user)
@other_post = @other_topic.posts.create(user: @user, raw: "some content")
@url = "http://#{test_uri.host}/t/#{@other_topic.slug}/#{@other_topic.id}" let(:other_topic) do
Fabricate(:topic, user: user)
end
@topic.posts.create(user: @user, raw: 'initial post') let(:post) do
@post = @topic.posts.create(user: @user, raw: "Link to another topic: #{@url}") other_topic.posts.create(user: user, raw: "some content")
TopicLink.extract_from(@post)
@link = @topic.topic_links.first
end end
it 'works' do it 'works' do
# extracted the link
@link.should be_present
# is set to internal # ensure other_topic has a post
@link.should be_internal post
# has the correct url url = "http://#{test_uri.host}/t/#{other_topic.slug}/#{other_topic.id}"
@link.url.should == @url
# has the extracted domain topic.posts.create(user: user, raw: 'initial post')
@link.domain.should == test_uri.host linked_post = topic.posts.create(user: user, raw: "Link to another topic: #{url}")
# should have the id of the linked forum TopicLink.extract_from(linked_post)
@link.link_topic_id == @other_topic.id
# should not be the reflection link = topic.topic_links.first
@link.should_not be_reflection link.should be_present
end link.should be_internal
link.url.should == url
link.domain.should == test_uri.host
link.link_topic_id == other_topic.id
link.should_not be_reflection
describe 'reflection in the other topic' do reflection = other_topic.topic_links.first
before do reflection.should be_present
@reflection = @other_topic.topic_links.first reflection.should be_reflection
end reflection.post_id.should be_present
reflection.domain.should == test_uri.host
reflection.url.should == "http://#{test_uri.host}/t/unique-topic-name/#{topic.id}/#{linked_post.post_number}"
reflection.link_topic_id.should == topic.id
reflection.link_post_id.should == linked_post.id
it 'works' do reflection.user_id.should == link.user_id
# exists
@reflection.should be_present
@reflection.should be_reflection
@reflection.post_id.should be_present
@reflection.domain.should == test_uri.host
@reflection.url.should == "http://#{test_uri.host}/t/unique-topic-name/#{@topic.id}/#{@post.post_number}"
@reflection.link_topic_id.should == @topic.id
@reflection.link_post_id.should == @post.id
#has the user id of the original link
@reflection.user_id.should == @link.user_id
end
end end
context 'removing a link' do context 'removing a link' do
before do before do
@post.revise(@post.user, "no more linkies") post.revise(post.user, "no more linkies")
TopicLink.extract_from(@post) TopicLink.extract_from(post)
end end
it 'should remove the link' do it 'should remove the link' do
@topic.topic_links.where(post_id: @post.id).should be_blank topic.topic_links.where(post_id: post.id).should be_blank
# should remove the reflected link # should remove the reflected link
@reflection = @other_topic.topic_links.should be_blank other_topic.topic_links.should be_blank
end end
end end
end end
context "link to a user on discourse" do context "link to a user on discourse" do
let(:post) { @topic.posts.create(user: @user, raw: "<a href='/users/#{@user.username_lower}'>user</a>") } let(:post) { topic.posts.create(user: user, raw: "<a href='/users/#{user.username_lower}'>user</a>") }
before do before do
TopicLink.extract_from(post) TopicLink.extract_from(post)
end end
it 'does not extract a link' do it 'does not extract a link' do
@topic.topic_links.should be_blank topic.topic_links.should be_blank
end end
end end
context "link to a discourse resource like a FAQ" do context "link to a discourse resource like a FAQ" do
let(:post) { @topic.posts.create(user: @user, raw: "<a href='/faq'>faq link here</a>") } let(:post) { topic.posts.create(user: user, raw: "<a href='/faq'>faq link here</a>") }
before do before do
TopicLink.extract_from(post) TopicLink.extract_from(post)
end end
it 'does not extract a link' do it 'does not extract a link' do
@topic.topic_links.should be_present topic.topic_links.should be_present
end end
end end
context "@mention links" do context "mention links" do
let(:post) { @topic.posts.create(user: @user, raw: "Hey @#{@user.username_lower}") } let(:post) { topic.posts.create(user: user, raw: "Hey #{user.username_lower}") }
before do before do
TopicLink.extract_from(post) TopicLink.extract_from(post)
end end
it 'does not extract a link' do it 'does not extract a link' do
@topic.topic_links.should be_blank topic.topic_links.should be_blank
end
end
context "quote links" do
it "sets quote correctly" do
linked_post = topic.posts.create(user: user, raw: "my test post")
quoting_post = Fabricate(:post, raw: "[quote=\"#{user.username}, post: #{linked_post.post_number}, topic: #{topic.id}\"]\nquote\n[/quote]")
TopicLink.extract_from(quoting_post)
link = quoting_post.topic.topic_links.first
link.link_post_id.should == linked_post.id
link.quote.should == true
end end
end end
context "link to a local attachments" do context "link to a local attachments" do
let(:post) { @topic.posts.create(user: @user, raw: '<a class="attachment" href="/uploads/default/208/87bb3d8428eb4783.rb">ruby.rb</a>') } let(:post) { topic.posts.create(user: user, raw: '<a class="attachment" href="/uploads/default/208/87bb3d8428eb4783.rb">ruby.rb</a>') }
it "extracts the link" do it "extracts the link" do
TopicLink.extract_from(post) TopicLink.extract_from(post)
link = @topic.topic_links.first link = topic.topic_links.first
# extracted the link # extracted the link
link.should be_present link.should be_present
# is set to internal # is set to internal
@ -203,11 +196,11 @@ http://b.com/#{'a'*500}
end end
context "link to an attachments uploaded on S3" do context "link to an attachments uploaded on S3" do
let(:post) { @topic.posts.create(user: @user, raw: '<a class="attachment" href="//s3.amazonaws.com/bucket/2104a0211c9ce41ed67989a1ed62e9a394c1fbd1446.rb">ruby.rb</a>') } let(:post) { topic.posts.create(user: user, raw: '<a class="attachment" href="//s3.amazonaws.com/bucket/2104a0211c9ce41ed67989a1ed62e9a394c1fbd1446.rb">ruby.rb</a>') }
it "extracts the link" do it "extracts the link" do
TopicLink.extract_from(post) TopicLink.extract_from(post)
link = @topic.topic_links.first link = topic.topic_links.first
# extracted the link # extracted the link
link.should be_present link.should be_present
# is not internal # is not internal
@ -223,40 +216,36 @@ http://b.com/#{'a'*500}
end end
describe 'internal link from pm' do describe 'internal link from pm' do
before do it 'works' do
@pm = Fabricate(:topic, user: @user, archetype: 'private_message') pm = Fabricate(:topic, user: user, archetype: 'private_message')
@other_post = @pm.posts.create(user: @user, raw: "some content") pm.posts.create(user: user, raw: "some content")
@url = "http://#{test_uri.host}/t/topic-slug/#{@topic.id}" url = "http://#{test_uri.host}/t/topic-slug/#{topic.id}"
@pm.posts.create(user: @user, raw: 'initial post') pm.posts.create(user: user, raw: 'initial post')
@linked_post = @pm.posts.create(user: @user, raw: "Link to another topic: #{@url}") linked_post = pm.posts.create(user: user, raw: "Link to another topic: #{url}")
TopicLink.extract_from(@linked_post) TopicLink.extract_from(linked_post)
@link = @topic.topic_links.first topic.topic_links.first.should be_nil
pm.topic_links.first.should_not be_nil
end end
it 'should not create a reflection' do
@topic.topic_links.first.should be_nil
end
it 'should not create a normal link' do
@pm.topic_links.first.should_not be_nil
end
end end
describe 'internal link with non-standard port' do describe 'internal link with non-standard port' do
it 'includes the non standard port if present' do it 'includes the non standard port if present' do
@other_topic = Fabricate(:topic, user: @user) other_topic = Fabricate(:topic, user: user)
SiteSetting.stubs(:port).returns(5678) SiteSetting.port = 5678
alternate_uri = URI.parse(Discourse.base_url) alternate_uri = URI.parse(Discourse.base_url)
@url = "http://#{alternate_uri.host}:5678/t/topic-slug/#{@other_topic.id}"
@post = @topic.posts.create(user: @user, url = "http://#{alternate_uri.host}:5678/t/topic-slug/#{other_topic.id}"
raw: "Link to another topic: #{@url}") post = topic.posts.create(user: user,
TopicLink.extract_from(@post) raw: "Link to another topic: #{url}")
@reflection = @other_topic.topic_links.first TopicLink.extract_from(post)
@reflection.url.should == "http://#{alternate_uri.host}:5678/t/unique-topic-name/#{@topic.id}" reflection = other_topic.topic_links.first
reflection.url.should == "http://#{alternate_uri.host}:5678/t/unique-topic-name/#{topic.id}"
end end
end end