muck-services 0.1.34 → 0.1.35
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 +1 -1
- data/app/controllers/muck/entries_controller.rb +3 -4
- data/app/controllers/muck/recommendations_controller.rb +19 -25
- data/app/controllers/muck/visits_controller.rb +1 -1
- data/app/models/entry.rb +27 -205
- data/app/models/feed.rb +6 -1
- data/app/models/recommendation.rb +2 -0
- data/app/views/entries/_related_entry.html.erb +1 -1
- data/app/views/entries/_tag_cloud.html.erb +3 -1
- data/app/views/entries/browse_by_tags.html.erb +1 -1
- data/app/views/entries/index.html.erb +2 -0
- data/app/views/recommendations/index.js.erb +1 -0
- data/app/views/recommendations/index.pjs.erb +8 -6
- data/app/views/recommendations/index.rss.builder +30 -30
- data/app/views/recommendations/index.xml.builder +21 -30
- data/app/views/recommendations/real_time.pjs.erb +1 -1
- data/lib/muck_services/tasks.rb +10 -16
- data/locales/en.yml +1 -0
- data/muck-services.gemspec +6 -3
- data/test/rails_root/config/environment.rb +1 -1
- data/test/rails_root/db/migrate/20100123035450_create_access_codes.rb +19 -0
- data/test/rails_root/db/migrate/20100123233654_create_access_code_requests.rb +14 -0
- data/test/rails_root/db/schema.rb +23 -1
- data/test/rails_root/test/factories.rb +6 -1
- data/test/rails_root/test/test_helper.rb +1 -0
- data/test/rails_root/test/unit/entry_test.rb +145 -0
- data/test/rails_root/test/unit/feed_test.rb +3 -0
- data/test/rails_root/test/unit/recommendation_test.rb +29 -0
- metadata +6 -3
- data/app/views/recommendations/index.html.erb +0 -5
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.1.
|
1
|
+
0.1.35
|
@@ -42,15 +42,14 @@ class Muck::EntriesController < ApplicationController
|
|
42
42
|
render_text "Unable to find the specified document"
|
43
43
|
return
|
44
44
|
end
|
45
|
-
@entry_title = @entry.title
|
46
|
-
@page_title = @entry_title
|
47
|
-
# I18n.locale = @entry.language[0..1]
|
45
|
+
@entry_title = "#{@entry.title} (#{@entry.feed.short_title})"
|
46
|
+
@page_title = "#{@entry_title} - #{I18n.t('muck.services.related_resources_title')}"
|
48
47
|
@limit = params[:limit] ? params[:limit].to_i : 20
|
49
48
|
@limit = 40 if @limit > 40
|
50
49
|
|
51
50
|
respond_to do |format|
|
52
51
|
format.html do
|
53
|
-
@recommendations = @entry.
|
52
|
+
@recommendations = @entry.related_entries.top(true, @limit)
|
54
53
|
if params[:details] == 'true'
|
55
54
|
render :template => "entries/details"
|
56
55
|
else
|
@@ -1,51 +1,45 @@
|
|
1
1
|
class Muck::RecommendationsController < ApplicationController
|
2
|
-
|
2
|
+
|
3
3
|
unloadable
|
4
4
|
|
5
|
-
# GET /recommendations
|
6
|
-
# GET /recommendations.xml
|
7
5
|
def index
|
8
|
-
|
9
|
-
|
6
|
+
# if a uri isn't specified in params, we assume that they are requesting from the page doing the requesting
|
10
7
|
@uri = params[:u] || request.env['HTTP_REFERER']
|
11
8
|
if @uri.blank? || !allowed_uri(@uri)
|
12
9
|
render :text => '<!-- permission denied -->'
|
13
10
|
return
|
14
11
|
end
|
15
|
-
|
12
|
+
|
13
|
+
# we trim eduCommons urls back to the course so all pages in the course get the same recommendations
|
16
14
|
if params[:educommons]
|
17
15
|
@uri = @uri[%r=http://.*?/.*?/[^/]+=] || @uri
|
18
16
|
params[:title] = true
|
19
17
|
params[:more_link] = true
|
20
18
|
end
|
21
19
|
|
22
|
-
|
23
|
-
@entry = Entry.recommender_entry(@uri)
|
24
|
-
# I18n.locale = @entry.language[0..1] if !@entry.nil?
|
25
|
-
|
20
|
+
@details = (params[:details] == 'true')
|
26
21
|
@limit = params[:limit] ? params[:limit].to_i : 5
|
27
22
|
@limit = 25 if @limit > 25
|
28
|
-
|
23
|
+
@omit_feeds = params[:omit_feeds]
|
24
|
+
@order = params[:order] || "rank"
|
25
|
+
|
26
|
+
Entry.track_time_on_page(session, @uri)
|
27
|
+
@entry = Entry.recommender_entry(@uri)
|
28
|
+
@recommendations = @entry.related_entries.top(@details, @limit, @omit_feeds, @order)
|
29
|
+
@app = request.protocol + request.host_with_port
|
30
|
+
|
29
31
|
respond_to do |format|
|
30
32
|
format.html do
|
31
|
-
order = params[:order] || "mixed"
|
32
33
|
if !@entry.id.nil?
|
33
|
-
redirect_to resource_path(@entry) + "?limit=#{@limit}&order=#{order}&details=#{@details}"
|
34
|
+
redirect_to resource_path(@entry) + "?limit=#{@limit}&order=#{@order}&details=#{@details}"
|
34
35
|
else
|
35
|
-
|
36
|
-
render :template => 'recommendations/index'
|
36
|
+
render(:text => t('muck.services.url_not_in_index', :uri => params[:uri]), :layout => true)
|
37
37
|
end
|
38
38
|
end
|
39
|
-
format.xml {
|
40
|
-
|
41
|
-
}
|
42
|
-
format.
|
43
|
-
@host = "http://#{URI.parse(@uri).host}"
|
44
|
-
render('recommendations/index.pjs.erb', :layout => false)
|
45
|
-
}
|
46
|
-
format.rss {
|
47
|
-
render(:template => 'recommendations/index.rss.builder', :layout => false)
|
48
|
-
}
|
39
|
+
format.xml { render('recommendations/index.xml.builder', :layout => false) }
|
40
|
+
format.pjs { render('recommendations/index.pjs.erb', :layout => false) }
|
41
|
+
format.rss { render('recommendations/index.rss.builder', :layout => false) }
|
42
|
+
format.js { render('recommendations/index.js.erb', :layout => false) }
|
49
43
|
end
|
50
44
|
end
|
51
45
|
|
@@ -9,7 +9,7 @@ class Muck::VisitsController < ApplicationController
|
|
9
9
|
@page_title = @entry.title
|
10
10
|
@resource_uri = @entry.resource_uri
|
11
11
|
@share = Share.new(:title => @entry.title, :uri => @resource_uri, :entry_id => @entry.id) if GlobalConfig.enable_services_shares
|
12
|
-
@recommendations = @entry.
|
12
|
+
@recommendations = @entry.related_entries.top
|
13
13
|
|
14
14
|
if GlobalConfig.enable_services_comments
|
15
15
|
# Show the activities related to this entry
|
data/app/models/entry.rb
CHANGED
@@ -34,18 +34,31 @@ class Entry < ActiveRecord::Base
|
|
34
34
|
has_many :activities, :as => :attachable, :dependent => :destroy
|
35
35
|
has_many :attentions, :dependent => :destroy
|
36
36
|
has_many :personal_recommendations, :as => :destination
|
37
|
+
|
38
|
+
has_many :related_to, :foreign_key => 'entry_id', :class_name => 'Recommendation'
|
39
|
+
has_many :related_entries, :through => :related_to, :source => :dest_entry do
|
40
|
+
def top(details=false, limit=5, omit_feeds=nil, order='rank desc, relevance desc')
|
41
|
+
select = 'entries.feed_id, recommendations.id recommendation_id, recommendations.relevance, ' +
|
42
|
+
'entries.title, feeds.short_title collection, recommendations.dest_entry_id '
|
43
|
+
select << ', entries.author, entries.published_at, recommendations.clicks, entries.permalink, ' +
|
44
|
+
'entries.direct_link direct_uri, ' +
|
45
|
+
'recommendations.avg_time_at_dest average_time_at_dest, entries.description' if details
|
46
|
+
conditions = omit_feeds == nil ? '' : "entries.feed_id NOT IN (#{omit_feeds.gsub(/[^0-9,]/,'')})"
|
47
|
+
joins = 'INNER JOIN feeds ON entries.feed_id = feeds.id'
|
48
|
+
find(:all, :select => select, :joins => joins, :conditions => conditions, :limit => limit, :order => order)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
37
52
|
acts_as_commentable
|
38
53
|
acts_as_taggable
|
39
54
|
acts_as_muck_recommendation
|
40
55
|
|
56
|
+
@@min_secs_tracked = 5
|
57
|
+
@@max_secs_tracked = 120
|
41
58
|
@@default_time_on_page = 60.0
|
42
59
|
|
43
60
|
acts_as_solr({:if => false, :fields => [{:aggregation => :integer}, {:feed_id => :integer}, {:grain_size => :string}, {:tags => :string}]}, {:type_field => :type_s})
|
44
61
|
|
45
|
-
def resource_uri
|
46
|
-
self.direct_link.nil? ? self.permalink : self.direct_link
|
47
|
-
end
|
48
|
-
|
49
62
|
def self.search(search_terms, grain_size = nil, language = "en", limit = 10, offset = 0, operator = :or)
|
50
63
|
raise MuckServices::Exceptions::LanguageNotSupported, I18n.t('muck.services.language_not_supported') unless Recommender::Languages.supported_languages.include?(language)
|
51
64
|
query = ((!grain_size.nil? && grain_size != 'all') ? (search_terms + ") AND (grain_size:#{grain_size}") : search_terms) + ") AND (aggregation:#{Aggregation.global_feeds_id}"
|
@@ -54,198 +67,36 @@ class Entry < ActiveRecord::Base
|
|
54
67
|
:joins => "INNER JOIN feeds ON feeds.id = entries.feed_id", :core => language, :operator => operator)
|
55
68
|
end
|
56
69
|
|
57
|
-
def
|
58
|
-
|
59
|
-
Entry.find(:first, :conditions => ['permalink = ? OR direct_link = ?', uri, uri], :order => 'direct_link IS NULL DESC') || Entry.new(:permalink => uri)
|
60
|
-
end
|
61
|
-
|
62
|
-
def recommendation_entries(limit = 20, order = "relevance", details = false, omit_feeds = nil)
|
63
|
-
sql = "SELECT recommendations.dest_entry_id AS id, entries.permalink, entries.title, entries.description, entries.direct_link, feeds.short_title AS collection "
|
64
|
-
sql << ", relevance_calculated_at, relevance, clicks, avg_time_at_dest AS avg_time_on_target, author, published_at " if details == true
|
65
|
-
sql << "FROM recommendations "
|
66
|
-
sql << "INNER JOIN entries ON recommendations.dest_entry_id = entries.id "
|
67
|
-
sql << "INNER JOIN feeds ON entries.feed_id = feeds.id "
|
68
|
-
sql << "WHERE recommendations.entry_id = ? "
|
69
|
-
sql << ("AND entries.feed_id NOT IN (" + omit_feeds.gsub(/[^0-9,]/,'') + ") ") if omit_feeds != nil
|
70
|
-
sql << "ORDER BY " + order + " DESC "
|
71
|
-
sql << "LIMIT " + limit.to_s
|
72
|
-
Entry.find_by_sql([sql,self.id])
|
73
|
-
end
|
74
|
-
|
75
|
-
def recommendations(limit = 20, order = "relevance", details = false, omit_feeds = nil)
|
76
|
-
sql = "SELECT recommendations.id, dest_entry_id, entries.permalink, entries.title, entries.description, entries.direct_link, feeds.short_title AS collection "
|
77
|
-
sql << ", relevance_calculated_at, relevance, clicks, avg_time_at_dest AS avg_time_on_target, author, published_at " if details == true
|
78
|
-
sql << "FROM recommendations "
|
79
|
-
sql << "INNER JOIN entries ON recommendations.dest_entry_id = entries.id "
|
80
|
-
sql << "INNER JOIN feeds ON entries.feed_id = feeds.id "
|
81
|
-
sql << "WHERE recommendations.entry_id = ? "
|
82
|
-
sql << ("AND entries.feed_id NOT IN (" + omit_feeds.gsub(/[^0-9,]/,'') + ") ") if omit_feeds != nil
|
83
|
-
sql << "ORDER BY " + order + " DESC "
|
84
|
-
sql << "LIMIT " + limit.to_s
|
85
|
-
Entry.find_by_sql([sql,self.id])
|
86
|
-
end
|
87
|
-
|
88
|
-
def relevant_recommendations(limit = 5, order = "relevance", details = false, omit_feeds = nil)
|
89
|
-
return self.recommendations(limit, order, details, omit_feeds)
|
90
|
-
end
|
91
|
-
|
92
|
-
def ranked_recommendations(limit = 5, order = "mixed", details = false, omit_feeds = nil)
|
93
|
-
return self.recommendations(limit, "clicks DESC, relevance", details, omit_feeds) if order == "clicks"
|
94
|
-
return self.recommendations(limit, "relevance", details, omit_feeds) if (order == "relevance" || details == true)
|
95
|
-
return relevant_recommendations_filtered(limit, details, omit_feeds) if omit_feeds != nil
|
96
|
-
|
97
|
-
recs = []
|
98
|
-
if self.popular != nil && !self.popular.empty?
|
99
|
-
recs.concat(ActiveSupport::JSON.decode(self.popular).first(limit))
|
100
|
-
end
|
101
|
-
if recs.length < limit && self.relevant != nil && !self.relevant.empty?
|
102
|
-
relevant_recs = randomize(ActiveSupport::JSON.decode(self.relevant))
|
103
|
-
recs.concat(relevant_recs.first(limit - recs.length))
|
104
|
-
end
|
105
|
-
if recs.length < limit && self.relevant != nil && !self.other.empty?
|
106
|
-
other_recs = randomize(ActiveSupport::JSON.decode(self.other))
|
107
|
-
recs.concat(other_recs.first(limit - recs.length))
|
108
|
-
end
|
109
|
-
return recs
|
110
|
-
end
|
111
|
-
|
112
|
-
def json_recommendations(limit = 5, order = "mixed", details = false, omit_feeds = nil)
|
113
|
-
recs = ranked_recommendations(limit, order, details, omit_feeds)
|
114
|
-
(recs.nil? || recs.empty?) ? "" : ActiveSupport::JSON.encode(recs)
|
115
|
-
end
|
116
|
-
|
117
|
-
def relevant_recommendations_filtered(limit, details, omit_feeds)
|
118
|
-
# get recommendations for the entry from the recommendations table
|
119
|
-
recs = self.recommendations(limit, "mixed", details, omit_feeds)
|
120
|
-
|
121
|
-
# for storing the various lists
|
122
|
-
popular_recs = []
|
123
|
-
relevant_recs = []
|
124
|
-
other_recs = []
|
125
|
-
|
126
|
-
# see where to cut draw the lines for popular and relevant
|
127
|
-
click_threshold = calc_click_threshold(recs)
|
128
|
-
relevance_threshold = calc_relevance_threshold(recs)
|
129
|
-
|
130
|
-
# store the recommendations
|
131
|
-
recs.each do |r|
|
132
|
-
if (r["clicks"].to_i > click_threshold)
|
133
|
-
popular_recs << r
|
134
|
-
elsif (r["relevance"].to_f > relevance_threshold)
|
135
|
-
relevant_recs << r
|
136
|
-
else
|
137
|
-
other_recs << r
|
138
|
-
end
|
139
|
-
end
|
140
|
-
|
141
|
-
# order popular items strictly by clicks
|
142
|
-
popular_recs.sort{|r1,r2| r2["avg_time_on_target"].to_i <=> r1["avg_time_on_target"].to_i}
|
143
|
-
|
144
|
-
return popular_recs[0..limit] if popular_recs.size > limit
|
145
|
-
return (popular_recs + relevant_recs)[0..limit] if (popular_recs.size + relevant_recs.size) > limit
|
146
|
-
return (popular_recs + relevant_recs + other_recs)[0..limit]
|
70
|
+
def resource_uri
|
71
|
+
self.direct_link.nil? ? self.permalink : self.direct_link
|
147
72
|
end
|
148
73
|
|
149
|
-
def
|
150
|
-
|
151
|
-
# get recommendations for the entry from the recommendations table
|
152
|
-
recs = self.recommendations
|
153
|
-
|
154
|
-
# for storing the various lists
|
155
|
-
popular_recs = []
|
156
|
-
relevant_recs = []
|
157
|
-
other_recs = []
|
158
|
-
|
159
|
-
# see where to cut draw the lines for popular and relevant
|
160
|
-
click_threshold = calc_click_threshold(recs)
|
161
|
-
relevance_threshold = calc_relevance_threshold(recs)
|
162
|
-
|
163
|
-
# store the recommendations
|
164
|
-
recs.each do |r|
|
165
|
-
if (r["clicks"].to_i > click_threshold)
|
166
|
-
popular_recs << r
|
167
|
-
elsif (r["relevance"].to_f > relevance_threshold)
|
168
|
-
relevant_recs << r
|
169
|
-
else
|
170
|
-
other_recs << r
|
171
|
-
end
|
172
|
-
end
|
173
|
-
|
174
|
-
# order popular items strictly by clicks
|
175
|
-
popular_recs.sort{|r1,r2| r2["avg_time_on_target"].to_i <=> r1["avg_time_on_target"].to_i}
|
176
|
-
|
177
|
-
# cache the JSON for the lists in the entry record
|
178
|
-
self.popular = ActiveSupport::JSON.encode(popular_recs)
|
179
|
-
self.relevant = ActiveSupport::JSON.encode(relevant_recs)
|
180
|
-
self.other = ActiveSupport::JSON.encode(other_recs)
|
181
|
-
self.save!
|
182
|
-
end
|
183
|
-
|
184
|
-
def randomize(recs)
|
185
|
-
i = recs.length
|
186
|
-
return recs if (i == 0)
|
187
|
-
while (i > 0)
|
188
|
-
i = i - 1
|
189
|
-
j = (rand*(i+1)).floor
|
190
|
-
ti = recs[i]
|
191
|
-
tj =recs[j]
|
192
|
-
recs[i] = tj
|
193
|
-
recs[j] = ti
|
194
|
-
end
|
195
|
-
return recs
|
74
|
+
def self.normalized_uri(uri)
|
75
|
+
uri.sub(/index.?\.(html|aspx|shtm|htm|asp|php|cfm|jsp|shtml|jhtml)$/, '')
|
196
76
|
end
|
197
77
|
|
198
|
-
def
|
199
|
-
|
200
|
-
|
201
|
-
sum += r["relevance"].to_f
|
202
|
-
end
|
203
|
-
average = sum/recs.length
|
204
|
-
sum = 0
|
205
|
-
recs.each do |r|
|
206
|
-
sum += (r["relevance"].to_f-average)**2
|
207
|
-
end
|
208
|
-
standard_deviation = Math.sqrt(sum/recs.length);
|
209
|
-
return average + standard_deviation
|
210
|
-
end
|
211
|
-
|
212
|
-
def calc_click_threshold(recs)
|
213
|
-
sum = 0
|
214
|
-
recs.each do |r|
|
215
|
-
sum += r["clicks"].to_f
|
216
|
-
end
|
217
|
-
average = sum/recs.length
|
218
|
-
sum = 0
|
219
|
-
recs.each do |r|
|
220
|
-
sum += (r["clicks"].to_f-average)**2
|
221
|
-
end
|
222
|
-
standard_deviation = Math.sqrt(sum/recs.length);
|
223
|
-
threshold = average + standard_deviation
|
224
|
-
return threshold > 5 ? threshold : 5
|
78
|
+
def self.recommender_entry(uri)
|
79
|
+
uri = normalized_uri(uri)
|
80
|
+
Entry.find(:first, :conditions => ['permalink = ? OR direct_link = ?', uri, uri], :order => 'direct_link IS NULL DESC') || Entry.new(:permalink => uri)
|
225
81
|
end
|
226
82
|
|
227
|
-
def self.avg_time(clicks, old_avg, time_on_page)
|
228
|
-
return (old_avg*(clicks-1) + time_on_page)/clicks
|
229
|
-
end
|
230
|
-
|
231
83
|
def self.track_time_on_page(session, uri)
|
232
84
|
recommendation_id = session[:last_clicked_recommendation]
|
233
85
|
if !recommendation_id.nil?
|
234
86
|
time_on_page = (Time.now - session[:last_clicked_recommendation_time].to_f).to_i
|
235
87
|
|
236
|
-
# if they spend
|
237
|
-
if time_on_page >
|
88
|
+
# if they spend less or more than time is reasonable, we don't infer anything
|
89
|
+
if time_on_page > @@min_secs_tracked and time_on_page < @@max_secs_tracked
|
238
90
|
if normalized_uri(uri) != session[:last_clicked_recommendation_uri]
|
239
91
|
recommendation = Recommendation.find(recommendation_id)
|
240
92
|
entry = Entry.find(recommendation.entry_id)
|
241
93
|
new_avg = (recommendation.avg_time_at_dest*recommendation.clicks - @@default_time_on_page + time_on_page)/recommendation.clicks
|
242
94
|
recommendation.avg_time_at_dest = new_avg
|
243
95
|
recommendation.save!
|
244
|
-
entry.rank_recommendations
|
245
96
|
session[:last_clicked_recommendation] = nil
|
246
97
|
end
|
247
98
|
else
|
248
|
-
session[:last_clicked_recommendation] = nil if time_on_page >
|
99
|
+
session[:last_clicked_recommendation] = nil if time_on_page > @@min_secs_tracked
|
249
100
|
end
|
250
101
|
end
|
251
102
|
end
|
@@ -255,10 +106,6 @@ class Entry < ActiveRecord::Base
|
|
255
106
|
recommendation = Recommendation.find(recommendation_id)
|
256
107
|
return "" if !recommendation
|
257
108
|
|
258
|
-
# get the entries being linked from and to
|
259
|
-
entry = Entry.find(recommendation.entry_id)
|
260
|
-
target = Entry.find(recommendation.dest_entry_id)
|
261
|
-
|
262
109
|
# get the list of recommendations that have been clicked during this session
|
263
110
|
clicks = session[:rids] || Array.new
|
264
111
|
|
@@ -286,9 +133,6 @@ class Entry < ActiveRecord::Base
|
|
286
133
|
session[:last_clicked_recommendation_time] = now
|
287
134
|
session[:last_clicked_recommendation_uri] = redirect
|
288
135
|
|
289
|
-
# update the recommendation cache for the entry
|
290
|
-
entry.rank_recommendations if entry
|
291
|
-
|
292
136
|
# track the click in the db
|
293
137
|
Click.create(:recommendation_id => recommendation_id, :when => now, :referrer => referrer, :requester => requester, :user_agent => user_agent)
|
294
138
|
end
|
@@ -296,28 +140,6 @@ class Entry < ActiveRecord::Base
|
|
296
140
|
return redirect
|
297
141
|
end
|
298
142
|
|
299
|
-
def self.truncate_words(text, length = 30, end_string = ' ...')
|
300
|
-
words = text.split()
|
301
|
-
words[0..(length-1)].join(' ') + (words.length > length ? end_string : '')
|
302
|
-
end
|
303
|
-
|
304
|
-
protected
|
305
|
-
|
306
|
-
# def self.redirect_uri(target, referrer, redirect_type)
|
307
|
-
# if !target.direct_link.nil? and redirect_type != "metadata"
|
308
|
-
# return target.direct_link if redirect_type == "direct_link"
|
309
|
-
# if !referrer.nil?
|
310
|
-
# domain = "http://" + URI.parse(referrer).host
|
311
|
-
# return target.direct_link if target.permalink[0..domain.length-1] != domain
|
312
|
-
# end
|
313
|
-
# end
|
314
|
-
# return target.permalink
|
315
|
-
# end
|
316
|
-
|
317
|
-
def self.normalized_uri(uri)
|
318
|
-
uri.sub(/index.?\.(html|aspx|shtm|htm|asp|php|cfm|jsp|shtml|jhtml)$/, '')
|
319
|
-
end
|
320
|
-
|
321
143
|
def self.real_time_recommendations(uri, language='en', limit=5, details=false, options = {})
|
322
144
|
raise MuckServices::Exceptions::LanguageNotSupported, I18n.t('muck.services.language_not_supported') unless Recommender::Languages.supported_languages.include?(language)
|
323
145
|
fields = "entries.id, entries.title, entries.permalink, entries.direct_link, feeds.short_title AS collection"
|
data/app/models/feed.rb
CHANGED
@@ -181,7 +181,12 @@ class Feed < ActiveRecord::Base
|
|
181
181
|
|
182
182
|
# Looks for feeds from a given url
|
183
183
|
def self.discover_feeds(uri)
|
184
|
-
|
184
|
+
begin
|
185
|
+
Feedbag.find(uri)
|
186
|
+
rescue URI::InvalidURIError
|
187
|
+
# if we aren't able to discover a feed then just return the original uri
|
188
|
+
[Struct.new(:url => uri, :title => '')]
|
189
|
+
end
|
185
190
|
end
|
186
191
|
|
187
192
|
# Finds or creates a feed based on the url. Any give feed uri should only exist once in the system
|
@@ -1,4 +1,4 @@
|
|
1
1
|
<%= link_to related_entry.title + " (" + related_entry.collection + ")", "/r?id=#{related_entry.id}", :class => "title_link", :rel => "nofollow" %>
|
2
2
|
- <%= link_to t('muck.services.related_resources'), resource_path(related_entry.dest_entry_id), :class => "related_resources_link", :rel => "nofollow" %>
|
3
|
-
<% if !related_entry.
|
3
|
+
<% if !related_entry.direct_uri.nil? %> - <%= link_to t('muck.services.metadata'), "/r?id=#{related_entry.id}&target=metadata", :class => "catalog_link" %><% end %>
|
4
4
|
<% if related_entry.description.length > 0 %><br/><span class="description"><%= truncate_words(related_entry.description) %></span><% end %>
|
@@ -1,3 +1,4 @@
|
|
1
|
+
<% cache({:locale => Language.locale_id, :format => 'html', :grain_size => @grain_size, :tag_filter => @tag_filter}) do %>
|
1
2
|
<div id="tag-cloud">
|
2
3
|
<fieldset>
|
3
4
|
<legend><%= @tag_filter.nil? ? (@grain_size == 'course' ? t('muck.services.courses_tag_cloud_label') : t('muck.services.resources_tag_cloud_label')) : t('muck.services.narrow_further') %></legend>
|
@@ -15,4 +16,5 @@
|
|
15
16
|
<% end %>
|
16
17
|
<% end -%>
|
17
18
|
</fieldset>
|
18
|
-
</div>
|
19
|
+
</div>
|
20
|
+
<% end %>
|
@@ -0,0 +1 @@
|
|
1
|
+
<% cache({:locale => Language.locale_id, :format => 'js', :details => @details, :limit => @limit, :order => @order, :id => + @entry.id}, :omit_feeds => @omit_feeds) do %>{"document":{"document_id":<%= @entry.id.to_s %>,"uri":"<%= @entry.permalink %>","recommendations":<%= @recommendations.to_json %>}}<% end %>
|
@@ -1,12 +1,13 @@
|
|
1
|
+
<% cache({:locale => Language.locale_id, :format => 'pjs', :details => @details, :limit => @limit, :order => @order, :id => + @entry.id, :omit_feeds => @omit_feeds}) do %>
|
1
2
|
<%
|
2
|
-
@
|
3
|
-
if !@
|
3
|
+
@host = "http://#{URI.parse(@uri).host}"
|
4
|
+
if !@recommendations.empty?
|
4
5
|
@direct_link_text = params[:direct_link_text] || t('muck.services.direct_link')
|
5
6
|
%>
|
6
7
|
var catalog_page = <%= !@entry.direct_link.nil? and @uri == @entry.permalink %>;
|
7
8
|
var document_host = '<%= @host %>';
|
8
|
-
var recs = <%= @
|
9
|
-
var app = "<%=
|
9
|
+
var recs = <%= @recommendations.to_json %>;
|
10
|
+
var app = "<%= request.protocol + request.host_with_port + '/' %>";
|
10
11
|
<% if @details == true -%>
|
11
12
|
function truncate(text, length) {
|
12
13
|
nEnd = text.indexOf(" ", 200);
|
@@ -30,8 +31,8 @@ for(nRec = 0; nRec < recs.length; nRec++) {
|
|
30
31
|
metadata_link = catalog_page && document_host == r.uri.substring(0, document_host.length);
|
31
32
|
direct_link = metadata_link && r.direct_link;
|
32
33
|
document.write('<p class="oer_recommender_item">');
|
33
|
-
document.write('<a class="oer_recommender_recommendation_link" href="' + app + 'r?id=' + r.
|
34
|
-
if (direct_link) document.write(' <a class="oer_recommender_direct_link" href="' + app + 'r?id=' + r.
|
34
|
+
document.write('<a class="oer_recommender_recommendation_link" href="' + app + 'r?id=' + r.recommendation_id + (metadata_link ? "&target=metadata" : "") + '">' + r.title + ' (' + r.collection + ')</a>');
|
35
|
+
if (direct_link) document.write(' <a class="oer_recommender_direct_link" href="' + app + 'r?id=' + r.recommendation_id + '&target=direct_link"><%= @direct_link_text %></a>');
|
35
36
|
<% if @details == true -%>
|
36
37
|
document.write(' <span class="oer_recommender_published_at">(' + format_date(r.published_at) + ')</span>');
|
37
38
|
document.write(' <span class="oer_recommender_relevance_score"><%= t('muck.services.relevance')%>: ' + Math.round(r.relevance*100)/100 + '</span>');
|
@@ -44,3 +45,4 @@ document.write('</div>');
|
|
44
45
|
<% if params[:more_link] %>document.write('<div class="oer_recommender_more_link"><a href="' + app + 'resources/<%= @entry.id %>"><%= t('muck.services.gm_more_prompt') %></a></div>');<% end %>
|
45
46
|
document.write('</div>');
|
46
47
|
<% end %>
|
48
|
+
<% end %>
|
@@ -1,37 +1,37 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
@recommendations = @entry.ranked_recommendations(@limit, params[:order] || "mixed")
|
5
|
-
end
|
1
|
+
cache({:locale => Language.locale_id, :format => 'rss', :details => @details, :limit => @limit, :order => @order, :id => + @entry.id, :omit_feeds => @omit_feeds}) do
|
2
|
+
headers["Content-Type"] = "application/rss+xml"
|
3
|
+
xml.instruct!
|
6
4
|
|
7
|
-
|
8
|
-
xml.instruct!
|
9
|
-
|
10
|
-
xml.rss "version" => "2.0", "xmlns:dc" => "http://purl.org/dc/elements/1.1/", "xmlns:oerr" => "http://www.oerrecommender.org/oerr/elements/1.0/" do
|
5
|
+
xml.rss "version" => "2.0", "xmlns:dc" => "http://purl.org/dc/elements/1.1/", "xmlns:oerr" => "http://www.folksemantic.com/oerr/elements/1.0/" do
|
11
6
|
xml.channel do
|
12
7
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
# xml.description "Blog posts for " + @event.title
|
17
|
-
xml.generator 'OER Recommender'
|
8
|
+
xml.title "Folksemantic recommendations for " + @entry.permalink
|
9
|
+
xml.link url_for(request.env["REQUEST_URI"])
|
10
|
+
xml.generator 'Folksemantic'
|
18
11
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
12
|
+
@recommendations.each do |recommendation|
|
13
|
+
xml.item do
|
14
|
+
xml.title recommendation["title"]
|
15
|
+
xml.link (@app +'/r?id=' + recommendation["recommendation_id"])
|
16
|
+
xml.guid (@app +'/r?id=' + recommendation["recommendation_id"])
|
17
|
+
xml.oerr :relevance, recommendation["relevance"]
|
18
|
+
|
19
|
+
if @details == true
|
20
|
+
xml.oerr :recommendation_id, recommendation["recommendation_id"]
|
21
|
+
xml.oerr :uri, recommendation["permalink"]
|
22
|
+
xml.oerr :direct_uri, recommendation["direct_link"]
|
23
|
+
if recommendation["description"] != nil
|
24
|
+
xml.description "type" => "html" do
|
25
|
+
xml.text! recommendation["description"]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
xml.oerr :clicks, recommendation["clicks"]
|
29
|
+
xml.oerr :average_time_at_dest, recommendation["avg_time_on_target"]
|
30
|
+
xml.pubDate CGI.rfc1123_date recommendation["published_at"]
|
31
|
+
xml.author recommendation["author"]
|
32
|
+
end
|
33
|
+
end
|
33
34
|
end
|
34
35
|
end
|
35
|
-
|
36
36
|
end
|
37
|
-
end
|
37
|
+
end
|
@@ -1,34 +1,25 @@
|
|
1
|
-
xml.
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
else
|
6
|
-
|
7
|
-
app = request.protocol + request.host_with_port + '/'
|
8
|
-
@recommendations = @entry.ranked_recommendations(@limit, params[:order] || "mixed", @details)
|
9
|
-
xml.recommendations(:document_id => @entry.nil? ? "" : @entry.id, :uri => @uri, :title => t("muck.services.gm_title"), :more_prompt => t("muck.services.gm_more_prompt"), :direct_link_text => t("muck.services.direct_link")) do
|
1
|
+
cache({:locale => Language.locale_id, :format => 'xml', :details => @details, :limit => @limit, :order => @order, :id => + @entry.id, :omit_feeds => @omit_feeds}) do
|
2
|
+
xml.instruct!
|
3
|
+
xml.recommendations(:document_id => @entry.id, :uri => @entry.permalink, :title => t("muck.services.gm_title"),
|
4
|
+
:more_prompt => t("muck.services.gm_more_prompt"), :direct_link_text => t("muck.services.direct_link")) do
|
10
5
|
@recommendations.each do |recommendation|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
xml.author recommendation["author"]
|
27
|
-
xml.tag_list recommendation["tag_list"]
|
28
|
-
else
|
29
|
-
xml.description ""
|
30
|
-
end
|
6
|
+
xml.recommendation do
|
7
|
+
xml.title recommendation["title"]
|
8
|
+
xml.collection recommendation["collection"]
|
9
|
+
xml.link @app + '/r?id=' + recommendation["recommendation_id"].to_s
|
10
|
+
xml.relevance recommendation["relevance"]
|
11
|
+
|
12
|
+
if @details == true
|
13
|
+
xml.recommendation_id recommendation["recommendation_id"]
|
14
|
+
xml.uri recommendation["permalink"]
|
15
|
+
xml.direct_uri recommendation["direct_link"]
|
16
|
+
xml.description recommendation["description"]
|
17
|
+
xml.clicks recommendation["clicks"]
|
18
|
+
xml.average_time_at_dest recommendation["avg_time_on_target"]
|
19
|
+
xml.published_at recommendation["published_at"]
|
20
|
+
xml.author recommendation["author"]
|
31
21
|
end
|
22
|
+
end
|
32
23
|
end
|
33
|
-
end
|
24
|
+
end
|
34
25
|
end
|
@@ -36,7 +36,7 @@ for(nRec = 0; nRec < recs.length; nRec++) {
|
|
36
36
|
document.write('<br/><span class="oer_recommender_description">' + truncate(r.description) + '</span>');
|
37
37
|
document.write('<br/><span class="oer_recommender_uri">' + r.uri + '</span>');
|
38
38
|
<% end -%>
|
39
|
-
document.write('</
|
39
|
+
document.write('</p>');
|
40
40
|
}
|
41
41
|
document.write('</div>');
|
42
42
|
<% if params[:more_link] %>document.write('<div class="oer_recommender_more_link"><a href="' + app + 'documents/<%= @entry.id %>"><%= t('muck.services.gm_more_prompt') %></a></div>');<% end %>
|
data/lib/muck_services/tasks.rb
CHANGED
@@ -39,7 +39,7 @@ module MuckServices
|
|
39
39
|
}
|
40
40
|
end
|
41
41
|
|
42
|
-
desc "Loads some feeds oai endpoints to get things started"
|
42
|
+
desc "Loads some feeds and oai endpoints to get things started (this should be run before muck:services:db:create_global_aggregation_feeds)"
|
43
43
|
task :bootstrap => :environment do
|
44
44
|
require 'active_record/fixtures'
|
45
45
|
ActiveRecord::Base.establish_connection(RAILS_ENV.to_sym)
|
@@ -78,6 +78,15 @@ module MuckServices
|
|
78
78
|
|
79
79
|
end
|
80
80
|
|
81
|
+
desc "Creates a global feeds aggregation and adds all existing feeds to it"
|
82
|
+
task :create_global_feeds_aggregation => :environment do
|
83
|
+
if Aggregation.find_by_title('global_feeds') == nil
|
84
|
+
global_feeds_id = Aggregation.create(:title => 'global_feeds', :terms => 'global_feeds',
|
85
|
+
:description => 'Feeds included in the site indexes.').id
|
86
|
+
Feed.find(:all).each { |feed| AggregationFeed.create(:feed_id => feed.id, :aggregation_id => global_feeds_id) }
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
81
90
|
end
|
82
91
|
|
83
92
|
desc "Sync files from muck services."
|
@@ -87,21 +96,6 @@ module MuckServices
|
|
87
96
|
system "rsync -ruv #{path}/public ."
|
88
97
|
end
|
89
98
|
|
90
|
-
desc "Create global_feeds aggregation and add feeds to it"
|
91
|
-
task :create_global_feeds => :environment do
|
92
|
-
admin_id = User.find_by_login('admin').id
|
93
|
-
global_aggregation = Aggregation.create(:title => 'global_feeds', :terms => 'global_feeds',
|
94
|
-
:description => 'Feeds included in the site indexes.',
|
95
|
-
:ownable_id => admin_id, :ownable_type => 'User')
|
96
|
-
global_aggregation = Aggregation.find_by_title('global_feeds')
|
97
|
-
global_aggregation.feeds << Feed.find(:all, :conditions => "uri LIKE 'http://www.oercommons.org%' OR id < 1047364815")
|
98
|
-
global_aggregation.save!
|
99
|
-
AggregationFeed.update_all("feed_type = 'Feed'")
|
100
|
-
OaiEndpoint.find(:all).each do |ep|
|
101
|
-
AggregationFeed.create(:aggregation_id => global_aggregation.id, :feed_type => 'OaiEndpoint', :feed_id => ep.id)
|
102
|
-
end
|
103
|
-
end
|
104
|
-
|
105
99
|
desc "Add attention types"
|
106
100
|
task :add_attention_types => :environment do
|
107
101
|
AttentionType.create(:id => AttentionType::WRITE, :name => 'write', :default_weight => 10)
|
data/locales/en.yml
CHANGED
@@ -214,5 +214,6 @@ en:
|
|
214
214
|
previous: Previous
|
215
215
|
facebook_notes: Facebook Notes
|
216
216
|
opml_generated: Feeds for {{terms}}
|
217
|
+
url_not_in_index: The <a href='{{uri}}'>resource you requested recommendations for</a> has not been indexed. Please add a repository containing metadata for the resource to add it to the index.
|
217
218
|
activity_templates:
|
218
219
|
entry_comment: Comment
|
data/muck-services.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{muck-services}
|
8
|
-
s.version = "0.1.
|
8
|
+
s.version = "0.1.35"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Joel Duffin", "Justin Ball"]
|
12
|
-
s.date = %q{2010-
|
12
|
+
s.date = %q{2010-02-08}
|
13
13
|
s.description = %q{This gem contains the rails specific code for dealing with feeds, aggregations and recommendations. It is meant to work with the muck-raker gem.}
|
14
14
|
s.email = %q{justin@tatemae.com}
|
15
15
|
s.extra_rdoc_files = [
|
@@ -114,7 +114,7 @@ Gem::Specification.new do |s|
|
|
114
114
|
"app/views/parts/_add_feed.html.erb",
|
115
115
|
"app/views/parts/_select_feed.html.erb",
|
116
116
|
"app/views/recommendations/get_button.html.erb",
|
117
|
-
"app/views/recommendations/index.
|
117
|
+
"app/views/recommendations/index.js.erb",
|
118
118
|
"app/views/recommendations/index.pjs.erb",
|
119
119
|
"app/views/recommendations/index.rss.builder",
|
120
120
|
"app/views/recommendations/index.xml.builder",
|
@@ -505,6 +505,8 @@ Gem::Specification.new do |s|
|
|
505
505
|
"test/rails_root/db/migrate/20091115011828_add_aggregations_for_personal_recs.rb",
|
506
506
|
"test/rails_root/db/migrate/20091116094447_rename_action_table.rb",
|
507
507
|
"test/rails_root/db/migrate/20091118203605_add_default_feed_type_to_aggregation_feed.rb",
|
508
|
+
"test/rails_root/db/migrate/20100123035450_create_access_codes.rb",
|
509
|
+
"test/rails_root/db/migrate/20100123233654_create_access_code_requests.rb",
|
508
510
|
"test/rails_root/db/schema.rb",
|
509
511
|
"test/rails_root/features/step_definitions/common_steps.rb",
|
510
512
|
"test/rails_root/features/step_definitions/visit_steps.rb",
|
@@ -534,6 +536,7 @@ Gem::Specification.new do |s|
|
|
534
536
|
"test/rails_root/test/unit/identity_feed_test.rb",
|
535
537
|
"test/rails_root/test/unit/oai_endpoint_test.rb",
|
536
538
|
"test/rails_root/test/unit/personal_recommendation_test.rb",
|
539
|
+
"test/rails_root/test/unit/recommendation_test.rb",
|
537
540
|
"test/rails_root/test/unit/service_category_test.rb",
|
538
541
|
"test/rails_root/test/unit/service_test.rb",
|
539
542
|
"test/rails_root/test/unit/services_mailer_test.rb",
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class CreateAccessCodes < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :access_codes do |t|
|
4
|
+
t.string :code
|
5
|
+
t.integer :uses, :default => 0, :null => false
|
6
|
+
t.boolean :unlimited, :default => false, :null => false
|
7
|
+
t.datetime :expires_at
|
8
|
+
t.integer :use_limit, :default => 1, :null => false
|
9
|
+
t.timestamps
|
10
|
+
end
|
11
|
+
add_index :access_codes, :code
|
12
|
+
add_column :users, :access_code_id, :integer
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.down
|
16
|
+
drop_table :access_codes
|
17
|
+
remove_column :users, :access_code_id
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class CreateAccessCodeRequests < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :access_code_requests do |t|
|
4
|
+
t.string :email
|
5
|
+
t.datetime :code_sent_at
|
6
|
+
t.timestamps
|
7
|
+
end
|
8
|
+
add_index :access_code_requests, :email
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.down
|
12
|
+
drop_table :access_code_requests
|
13
|
+
end
|
14
|
+
end
|
@@ -9,7 +9,28 @@
|
|
9
9
|
#
|
10
10
|
# It's strongly recommended to check this file into your version control system.
|
11
11
|
|
12
|
-
ActiveRecord::Schema.define(:version =>
|
12
|
+
ActiveRecord::Schema.define(:version => 20100123233654) do
|
13
|
+
|
14
|
+
create_table "access_code_requests", :force => true do |t|
|
15
|
+
t.string "email"
|
16
|
+
t.datetime "code_sent_at"
|
17
|
+
t.datetime "created_at"
|
18
|
+
t.datetime "updated_at"
|
19
|
+
end
|
20
|
+
|
21
|
+
add_index "access_code_requests", ["email"], :name => "index_access_code_requests_on_email"
|
22
|
+
|
23
|
+
create_table "access_codes", :force => true do |t|
|
24
|
+
t.string "code"
|
25
|
+
t.integer "uses", :default => 0, :null => false
|
26
|
+
t.boolean "unlimited", :default => false, :null => false
|
27
|
+
t.datetime "expires_at"
|
28
|
+
t.integer "use_limit", :default => 1, :null => false
|
29
|
+
t.datetime "created_at"
|
30
|
+
t.datetime "updated_at"
|
31
|
+
end
|
32
|
+
|
33
|
+
add_index "access_codes", ["code"], :name => "index_access_codes_on_code"
|
13
34
|
|
14
35
|
create_table "activities", :force => true do |t|
|
15
36
|
t.integer "item_id"
|
@@ -462,6 +483,7 @@ ActiveRecord::Schema.define(:version => 20091118203605) do
|
|
462
483
|
t.datetime "updated_at"
|
463
484
|
t.string "identity_url"
|
464
485
|
t.string "url_key"
|
486
|
+
t.integer "access_code_id"
|
465
487
|
end
|
466
488
|
|
467
489
|
add_index "users", ["email"], :name => "index_users_on_email"
|
@@ -3,4 +3,9 @@ Factory.define :share do |f|
|
|
3
3
|
f.title { Factory.next(:title) }
|
4
4
|
f.shared_by {|a| a.association(:user)}
|
5
5
|
f.entry {|a| a.association(:entry)}
|
6
|
-
end
|
6
|
+
end
|
7
|
+
|
8
|
+
Factory.define :recommendation do |r|
|
9
|
+
r.entry {|a| a.association(:entry)}
|
10
|
+
r.dest_entry {|a| a.association(:entry)}
|
11
|
+
end
|
@@ -16,6 +16,7 @@ class ActiveSupport::TestCase
|
|
16
16
|
TEST_URI = 'http://www.engadget.com'
|
17
17
|
TEST_RSS_URI = 'http://www.engadget.com/rss.xml'
|
18
18
|
TEST_USERNAME_TEMPLATE = 'http://feeds.delicious.com/v2/rss/{username}?count=100'
|
19
|
+
TEST_XML_URI = 'http://ocw.mit.edu/OcwWeb/rss/all/mit-allcourses.xml'
|
19
20
|
|
20
21
|
end
|
21
22
|
|
@@ -37,6 +37,40 @@ class EntryTest < ActiveSupport::TestCase
|
|
37
37
|
subject { @entry }
|
38
38
|
|
39
39
|
should_belong_to :feed
|
40
|
+
|
41
|
+
|
42
|
+
context "recommender_entry" do
|
43
|
+
should "return an entry if the direct_link matches the specified uri" do
|
44
|
+
uri = Factory.next(:uri)
|
45
|
+
e = Factory.create(:entry, :direct_link => uri)
|
46
|
+
assert_equal e.id, Entry.recommender_entry(uri).id
|
47
|
+
end
|
48
|
+
|
49
|
+
should "return an entry if the permalink matches the specified uri" do
|
50
|
+
uri = Factory.next(:uri)
|
51
|
+
e = Factory.create(:entry, :permalink => uri)
|
52
|
+
assert_equal e.id, Entry.recommender_entry(uri).id
|
53
|
+
end
|
54
|
+
|
55
|
+
should "return an empty entry with the specified uri if the specified uri doesn't match" do
|
56
|
+
uri = Factory.next(:uri)
|
57
|
+
e = Factory.create(:entry, :permalink => uri)
|
58
|
+
assert_not_nil Entry.recommender_entry(uri)
|
59
|
+
assert_equal uri, Entry.recommender_entry(uri).permalink
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
context "resource_uri" do
|
64
|
+
should "return the permalink when no direct_link is specified" do
|
65
|
+
@entry.direct_link = nil
|
66
|
+
assert_equal @entry.permalink, @entry.resource_uri
|
67
|
+
end
|
68
|
+
|
69
|
+
should "return the direct_link when a direct_link is specified" do
|
70
|
+
@entry.direct_link = Factory.next(:uri)
|
71
|
+
assert_equal @entry.direct_link, @entry.resource_uri
|
72
|
+
end
|
73
|
+
end
|
40
74
|
|
41
75
|
context "search" do
|
42
76
|
# should "search indexes for ruby" do
|
@@ -48,7 +82,118 @@ class EntryTest < ActiveSupport::TestCase
|
|
48
82
|
end
|
49
83
|
end
|
50
84
|
end
|
85
|
+
|
86
|
+
context "normalized_uri" do
|
87
|
+
should "remove the trailing file name from any url that ends in index.*" do
|
88
|
+
index_uri = 'http://example.com/some_dir/'
|
89
|
+
['html','aspx','shtm','htm','asp','php','cfm','jsp','shtml','jhtml'].each { |ext|
|
90
|
+
assert_equal index_uri, Entry.normalized_uri(index_uri + 'index.' + ext)
|
91
|
+
}
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
context "recommendation requested omitting certain feeds" do
|
96
|
+
should "not contain entries from those feeds" do
|
97
|
+
feed1_id = Factory(:feed).id
|
98
|
+
feed2_id = Factory(:feed).id
|
99
|
+
assert_not_equal feed1_id, feed2_id
|
100
|
+
Recommendation.create(:entry_id => @entry.id, :dest_entry_id => Factory.create(:entry, :feed_id => feed1_id).id)
|
101
|
+
Recommendation.create(:entry_id => @entry.id, :dest_entry_id => Factory.create(:entry, :feed_id => feed2_id).id)
|
102
|
+
r = @entry.related_entries.top(false, 5, feed2_id.to_s)
|
103
|
+
assert_equal 1, r.length
|
104
|
+
r.each { |e| assert_not_equal feed2_id, e.feed_id}
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
context "with 3 recommendations" do
|
109
|
+
|
110
|
+
setup do
|
111
|
+
3.times { |n| Recommendation.create(:entry_id => @entry.id,
|
112
|
+
:dest_entry_id => Factory(:entry).id, :relevance => n.to_f/10.0) }
|
113
|
+
end
|
114
|
+
|
115
|
+
should "have 3 related_entries" do
|
116
|
+
assert_equal 3, @entry.related_entries.top.length
|
117
|
+
end
|
118
|
+
|
119
|
+
should "return only 2 if specified" do
|
120
|
+
assert_equal 2, @entry.related_entries.top(true,2).length
|
121
|
+
end
|
122
|
+
|
123
|
+
should "return results in order of relevance by default" do
|
124
|
+
related_entries = @entry.related_entries.top
|
125
|
+
assert related_entries[1].relevance < related_entries[0].relevance
|
126
|
+
assert related_entries[2].relevance < related_entries[1].relevance
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
51
130
|
|
131
|
+
context "recommendation requested without details" do
|
132
|
+
should "not have an author" do
|
133
|
+
Recommendation.create(:entry_id => @entry.id,
|
134
|
+
:dest_entry_id => Factory.create(:entry, :author => Factory.next(:name)).id,
|
135
|
+
:relevance => '.3')
|
136
|
+
assert_raise ActiveRecord::MissingAttributeError do
|
137
|
+
@entry.related_entries.top.first.author
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
should "have basic fields" do
|
142
|
+
title = Factory.next(:name)
|
143
|
+
collection = Factory.next(:name)
|
144
|
+
feed_id = Factory(:feed, :short_title => collection).id
|
145
|
+
dest_entry_id = Factory.create(:entry, :title => title, :feed_id => feed_id).id
|
146
|
+
relevance = '.3'
|
147
|
+
recommendation_id = Recommendation.create(:entry_id => @entry.id, :relevance => relevance,
|
148
|
+
:dest_entry_id => dest_entry_id).id.to_s
|
149
|
+
r = @entry.related_entries.top(true).first
|
150
|
+
assert_nothing_raised do
|
151
|
+
assert_equal recommendation_id, r.recommendation_id
|
152
|
+
assert_equal relevance.to_f, r.relevance.to_f
|
153
|
+
assert_equal title, r.title
|
154
|
+
assert_equal collection, r.collection
|
155
|
+
assert_not_nil r.dest_entry_id
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
context "recommendation requested with details" do
|
161
|
+
should "have basic and detailed fields" do
|
162
|
+
title = Factory.next(:name)
|
163
|
+
collection = Factory.next(:name)
|
164
|
+
author = Factory.next(:name)
|
165
|
+
published_at = DateTime.now
|
166
|
+
clicks = 23
|
167
|
+
average_time_at_dest = 44
|
168
|
+
description = Factory.next(:name)
|
169
|
+
permalink = Factory.next(:uri)
|
170
|
+
direct_uri = Factory.next(:uri)
|
171
|
+
feed_id = Factory(:feed, :short_title => collection).id
|
172
|
+
relevance = '.3'
|
173
|
+
dest_entry_id = Factory.create(:entry, :feed_id => feed_id, :title => title,
|
174
|
+
:author => author, :published_at => published_at, :permalink => permalink,
|
175
|
+
:direct_link => direct_uri, :description => description).id
|
176
|
+
recommendation_id = Recommendation.create(:entry_id => @entry.id, :relevance => relevance,
|
177
|
+
:avg_time_at_dest => average_time_at_dest, :clicks => clicks, :dest_entry_id => dest_entry_id).id.to_s
|
178
|
+
r = @entry.related_entries.top(true).first
|
179
|
+
assert_nothing_raised do
|
180
|
+
assert_equal recommendation_id, r.recommendation_id
|
181
|
+
assert_equal relevance.to_f, r.relevance.to_f
|
182
|
+
assert_equal title, r.title
|
183
|
+
assert_equal collection, r.collection
|
184
|
+
assert_not_nil r.dest_entry_id
|
185
|
+
|
186
|
+
assert_equal author, r.author
|
187
|
+
# assert_equal published_at, r.published_at
|
188
|
+
assert_equal clicks, r.clicks.to_i
|
189
|
+
assert_equal permalink, r.permalink
|
190
|
+
assert_equal direct_uri, r.direct_uri
|
191
|
+
assert_equal average_time_at_dest, r.average_time_at_dest.to_i
|
192
|
+
assert_equal description, r.description
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
52
197
|
end
|
53
198
|
|
54
199
|
end
|
@@ -122,6 +122,9 @@ class FeedTest < ActiveSupport::TestCase
|
|
122
122
|
should "Discover feeds from url" do
|
123
123
|
Feed.discover_feeds(TEST_URI)
|
124
124
|
end
|
125
|
+
should "Discover feeds from xml url" do
|
126
|
+
assert_equal TEST_XML_URI, Feed.discover_feeds(TEST_XML_URI)[0].url
|
127
|
+
end
|
125
128
|
end
|
126
129
|
|
127
130
|
context "Harvest feed" do
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# == Schema Information
|
2
|
+
#
|
3
|
+
# Table name: recommendations
|
4
|
+
#
|
5
|
+
# id :integer(4) not null, primary key
|
6
|
+
# entry_id :integer(4)
|
7
|
+
# dest_entry_id :integer(4)
|
8
|
+
# rank :integer(4)
|
9
|
+
# relevance :decimal(8, 6) default(0.0)
|
10
|
+
# clicks :integer(4) default(0)
|
11
|
+
# avg_time_at_dest :integer(4) default(60)
|
12
|
+
#
|
13
|
+
|
14
|
+
require File.dirname(__FILE__) + '/../test_helper'
|
15
|
+
|
16
|
+
class RecommendationTest < ActiveSupport::TestCase
|
17
|
+
|
18
|
+
context "recommendation" do
|
19
|
+
setup do
|
20
|
+
@recommendation = Factory(:recommendation)
|
21
|
+
end
|
22
|
+
|
23
|
+
subject { @recommendation }
|
24
|
+
|
25
|
+
should_belong_to :entry
|
26
|
+
should_belong_to :dest_entry
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: muck-services
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.35
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Joel Duffin
|
@@ -10,7 +10,7 @@ autorequire:
|
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
|
13
|
-
date: 2010-
|
13
|
+
date: 2010-02-08 00:00:00 -07:00
|
14
14
|
default_executable:
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
@@ -240,7 +240,7 @@ files:
|
|
240
240
|
- app/views/parts/_add_feed.html.erb
|
241
241
|
- app/views/parts/_select_feed.html.erb
|
242
242
|
- app/views/recommendations/get_button.html.erb
|
243
|
-
- app/views/recommendations/index.
|
243
|
+
- app/views/recommendations/index.js.erb
|
244
244
|
- app/views/recommendations/index.pjs.erb
|
245
245
|
- app/views/recommendations/index.rss.builder
|
246
246
|
- app/views/recommendations/index.xml.builder
|
@@ -652,6 +652,8 @@ test_files:
|
|
652
652
|
- test/rails_root/db/migrate/20091115011828_add_aggregations_for_personal_recs.rb
|
653
653
|
- test/rails_root/db/migrate/20091116094447_rename_action_table.rb
|
654
654
|
- test/rails_root/db/migrate/20091118203605_add_default_feed_type_to_aggregation_feed.rb
|
655
|
+
- test/rails_root/db/migrate/20100123035450_create_access_codes.rb
|
656
|
+
- test/rails_root/db/migrate/20100123233654_create_access_code_requests.rb
|
655
657
|
- test/rails_root/db/schema.rb
|
656
658
|
- test/rails_root/features/step_definitions/common_steps.rb
|
657
659
|
- test/rails_root/features/step_definitions/visit_steps.rb
|
@@ -681,6 +683,7 @@ test_files:
|
|
681
683
|
- test/rails_root/test/unit/identity_feed_test.rb
|
682
684
|
- test/rails_root/test/unit/oai_endpoint_test.rb
|
683
685
|
- test/rails_root/test/unit/personal_recommendation_test.rb
|
686
|
+
- test/rails_root/test/unit/recommendation_test.rb
|
684
687
|
- test/rails_root/test/unit/service_category_test.rb
|
685
688
|
- test/rails_root/test/unit/service_test.rb
|
686
689
|
- test/rails_root/test/unit/services_mailer_test.rb
|