Skip to content

Commit

Permalink
Add support for Atom (RSS) feed about latest project changes (pull #351)
Browse files Browse the repository at this point in the history
This adds support for an Atom (RSS) feed about the latest project changes.

Note: This adds some spurious warnings during test from feed_test.rb.

Signed-off-by: Dan Kohn <dan@dankohn.com>

* Working

* Add width and height

* Law of Demeter

* Fix autodiscovery link

* feed tests

* Added index

Signed-off-by: Dan Kohn <dan@dankohn.com>

* Better fix for intermittent Capybara error

Signed-off-by: Dan Kohn <dan@dankohn.com>
  • Loading branch information
dankohn authored and david-a-wheeler committed May 12, 2016
1 parent 5c67ed2 commit 9638581
Show file tree
Hide file tree
Showing 13 changed files with 158 additions and 28 deletions.
Binary file added app/assets/images/feed-icon-28x28.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions app/controllers/projects_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,11 @@ def destroy
end
end

def feed
@projects = Project.recently_updated
respond_to { |format| format.atom }
end

def repo_data
github = Github.new oauth_token: session[:user_token], auto_pagination: true
github.repos.list.map do |repo|
Expand Down
7 changes: 7 additions & 0 deletions app/models/project.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# frozen_string_literal: true
# rubocop:disable Metrics/ClassLength
class Project < ActiveRecord::Base
using StringRefinements
using SymbolRefinements
Expand All @@ -25,6 +26,12 @@ class Project < ActiveRecord::Base
scope :in_progress, -> { where(badge_status: 'in_progress') }
scope :passing, -> { where(badge_status: 'passing') }

scope :recently_updated, (

This comment has been minimized.

Copy link
@ciibot

ciibot May 30, 2017

Collaborator

Remove unused methods (stringrefinements::symbolrefinements::project#recently_updated)

lambda do
unscoped.limit(50).order(updated_at: :desc).includes(:user)
end
)

scope :text_search, (
lambda do |text|
start_text = "#{text}%"
Expand Down
1 change: 1 addition & 0 deletions app/views/layouts/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<%= favicon_link_tag %>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
<%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
<%= auto_discovery_link_tag :atom, { controller: 'projects', action: 'feed' }, { title: 'CII Best Practices BadgeApp Updated Projects' } %>
<%= csrf_meta_tags %>
</head>
<body>
Expand Down
19 changes: 19 additions & 0 deletions app/views/projects/feed.atom.builder
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
atom_feed do |feed|
feed.title('CII Best Practices BadgeApp Updated Projects')
feed.updated(@projects[0].updated_at) unless @projects.empty?

@projects.each do |project|
feed.entry(project) do |entry|
entry.title project.name.presence || '(Name Unknown)'
status = "<p><b>#{project.badge_status.titleize}: " \
"#{project.badge_percentage}%</b></p>"
url = project.homepage_url.presence || project.repo_url
link = "<p><a href='#{url}'>#{url}</a></p>"
description = markdown((project.description || '')
.truncate(160, separator: ' '))
content = status + link + description
entry.content(type: 'html') { entry.cdata! content }
entry.author { |author| author.name(project.user_name) }
end
end
end
22 changes: 12 additions & 10 deletions app/views/projects/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,18 @@
<h2>Projects</h2>
<%= form_for projects_path, method: 'get', enforce_utf8: false,
html: { class: 'project_search' } do %>
<%= label_tag :badge_status %>
<%= select_tag(:status,
options_for_select(([nil] + Project::BADGE_STATUSES)
.map { |status| [status.to_s.titleize, status] },
params[:status]), onchange: 'this.form.submit();') %>
&nbsp; &nbsp; &nbsp;
<%= label_tag :text_search %>
<%= text_field_tag :q, params[:q], size: 25,
placeholder: 'Name or URL begins with...' %>
<%= submit_tag 'Search', name: nil %>
<%= label_tag :badge_status %>
<%= select_tag(:status,
options_for_select(([nil] + Project::BADGE_STATUSES)
.map { |status| [status.to_s.titleize, status] },
params[:status]), onchange: 'this.form.submit();') %>
&nbsp; &nbsp; &nbsp;
<%= label_tag :text_search %>
<%= text_field_tag :q, params[:q], size: 25,
placeholder: 'Name or URL begins with...' %>
<%= submit_tag 'Search', name: nil %>
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
<%= link_to image_tag('feed-icon-28x28', width: '28', height: '28'), feed_path %>
<% end %>
<br>
<%= link_to 'Add New Project', new_project_path, class: 'btn btn-success' %>
Expand Down
4 changes: 3 additions & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
get 'background' => 'static_pages#background'
get 'criteria' => 'static_pages#criteria'

get 'feed' => 'projects#feed', defaults: { format: 'atom' }

resources :projects do
member do
get 'badge', defaults: { format: 'svg' }
Expand All @@ -21,7 +23,7 @@
delete 'logout' => 'sessions#destroy'

get 'auth/:provider/callback' => 'sessions#create'
get '/signout' => 'sessions#destroy', :as => :signout
get '/signout' => 'sessions#destroy', as: :signout

# The priority is based upon order of creation: first created ->
# highest priority.
Expand Down
5 changes: 5 additions & 0 deletions db/migrate/20160511222530_add_index_updated_at.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddIndexUpdatedAt < ActiveRecord::Migration
def change
add_index :projects, :updated_at
end
end
3 changes: 2 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 20160503195329) do
ActiveRecord::Schema.define(version: 20160511222530) do

# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
Expand Down Expand Up @@ -187,6 +187,7 @@
add_index "projects", ["homepage_url"], name: "index_projects_on_homepage_url", using: :btree
add_index "projects", ["name"], name: "index_projects_on_name", using: :btree
add_index "projects", ["repo_url"], name: "index_projects_on_repo_url", using: :btree
add_index "projects", ["updated_at"], name: "index_projects_on_updated_at", using: :btree
add_index "projects", ["user_id", "created_at"], name: "index_projects_on_user_id_and_created_at", using: :btree
add_index "projects", ["user_id"], name: "index_projects_on_user_id", using: :btree

Expand Down
30 changes: 14 additions & 16 deletions test/features/login_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,43 +25,41 @@ class LoginTest < Capybara::Rails::TestCase
assert has_content? 'Signed in!'

visit edit_project_path(@project)
choose 'project_discussion_status_unmet'
kill_sticky_headers # This is necessary for Chrome and Firefox

ensure_choice 'project_discussion_status_unmet'
assert_match X, find('#discussion_enough')['src']

choose 'project_english_status_met'
ensure_choice 'project_english_status_met'
assert_match CHECK, find('#english_enough')['src']

kill_sticky_headers # This is necessary for Chrome and Firefox
choose 'project_contribution_status_met' # No URL given, so fails
ensure_choice 'project_contribution_status_met' # No URL given, so fails
assert_match QUESTION, find('#contribution_enough')['src']

choose 'project_contribution_requirements_status_unmet' # No URL given
ensure_choice 'project_contribution_requirements_status_unmet' # No URL
assert_match X, find('#contribution_requirements_enough')['src']

click_on 'Change Control'
assert has_content? 'repo_public'
choose 'project_repo_public_status_unmet'
ensure_choice 'project_repo_public_status_unmet'
assert_match X, find('#repo_public_enough')['src']

# Extra assertions to deal with flapping test
assert find('#project_repo_distributed_status_')['checked']
# loop needed because selection doesn't always take the first time
loops = 0
while find('#project_repo_distributed_status_')['checked']
puts "#{pluralize loops, 'extra loop'} required" if loops > 0
loops += 1
choose 'project_repo_distributed_status_unmet' # SUGGESTED, so enough
wait_for_jquery
end
ensure_choice 'project_repo_distributed_status_unmet' # SUGGESTED, so enough
assert find('#project_repo_distributed_status_unmet')['checked']
assert_match DASH, find('#repo_distributed_enough')['src']

click_on 'Reporting'
assert has_content? 'report_process'
choose 'project_report_process_status_unmet'
ensure_choice 'project_report_process_status_unmet'
assert_match X, find('#report_process_enough')['src']

click_on 'Submit'
assert_match X, find('#discussion_enough')['src']
end

def ensure_choice(radio_button_id)
# Necessary because Capybara click doesn't always take the first time
choose radio_button_id until find("##{radio_button_id}")['checked']
end
end
64 changes: 64 additions & 0 deletions test/fixtures/files/feed.atom
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom">
<id>tag:www.example.com,2005:/feed</id>
<link rel="alternate" type="text/html" href="http://www.example.com"/>
<link rel="self" type="application/atom+xml" href="http://www.example.com/feed"/>
<title>CII Best Practices BadgeApp Updated Projects</title>
<updated>2015-03-01T12:00:00Z</updated>
<entry>
<id>tag:www.example.com,2005:Project/980190962</id>
<published>2015-03-01T12:00:00Z</published>
<updated>2015-03-01T12:00:00Z</updated>
<link rel="alternate" type="text/html" href="http://www.example.com/projects/980190962"/>
<title>Pathfinder OS</title>
<content type="html">
<![CDATA[<p><b>In Progress: 0%</b></p><p><a href='https://www.nasa.gov'>https://www.nasa.gov</a></p><p>Operating system for Pathfinder rover</p>
]]>
</content>
<author>
<name>Test</name>
</author>
</entry>
<entry>
<id>tag:www.example.com,2005:Project/298486374</id>
<published>2015-03-01T12:00:00Z</published>
<updated>2015-03-01T12:00:00Z</updated>
<link rel="alternate" type="text/html" href="http://www.example.com/projects/298486374"/>
<title>Mars Ascent Vehicle (MAV)</title>
<content type="html">
<![CDATA[<p><b>In Progress: 0%</b></p><p><a href='https://www.nasa.gov'>https://www.nasa.gov</a></p><p>Operating system for Pathfinder rover</p>
]]>
</content>
<author>
<name>Melissa Lewis</name>
</author>
</entry>
<entry>
<id>tag:www.example.com,2005:Project/832656895</id>
<published>2015-03-01T12:00:00Z</published>
<updated>2015-03-01T12:00:00Z</updated>
<link rel="alternate" type="text/html" href="http://www.example.com/projects/832656895"/>
<title>Unjustified perfect project</title>
<content type="html">
<![CDATA[<p><b>Failing: 88%</b></p><p><a href='https://www.example.org'>https://www.example.org</a></p><p>The perfect project</p>
]]>
</content>
<author>
<name>Test</name>
</author>
</entry>
<entry>
<id>tag:www.example.com,2005:Project/600182345</id>
<published>2015-03-01T12:00:00Z</published>
<updated>2015-03-01T12:00:00Z</updated>
<link rel="alternate" type="text/html" href="http://www.example.com/projects/600182345"/>
<title>Justified perfect project</title>
<content type="html">
<![CDATA[<p><b>Passing: 100%</b></p><p><a href='https://www.example.org'>https://www.example.org</a></p><p>The perfect project</p>
]]>
</content>
<author>
<name>Test</name>
</author>
</entry>
</feed>
22 changes: 22 additions & 0 deletions test/integration/feed_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
require 'test_helper'
load 'Rakefile'

class FeedTest < ActionDispatch::IntegrationTest
def setup
# Normalize time in order to match fixture file
travel_to Time.zone.parse('2015-03-01T12:00:00') do
silence_stream(STDOUT) do
# anything written to STDOUT here will be silenced
Rake::Task['db:schema:load'].reenable
Rake::Task['db:schema:load'].invoke
end
Rake::Task['db:fixtures:load'].reenable
Rake::Task['db:fixtures:load'].invoke
end
end

test 'feed matches fixture file' do
get feed_path
assert_equal contents('feed.atom'), response.body
end
end
4 changes: 4 additions & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ def configure_omniauth_mock
OmniAuth.config.add_mock(:github, omniauth_hash)
end

def contents(file_name)
IO.read "test/fixtures/files/#{file_name}"

This comment has been minimized.

Copy link
@ciibot

ciibot Oct 13, 2021

Collaborator

Security/IoMethods: File.read is safer than IO.read.

end

# rubocop:disable Metrics/MethodLength
def kill_sticky_headers
# https://alisdair.mcdiarmid.org/kill-sticky-headers/
Expand Down

0 comments on commit 9638581

Please sign in to comment.