Skip to content

Commit

Permalink
Merge pull request #355 from rubytoolbox/co-project-health
Browse files Browse the repository at this point in the history
Project health checks and display
  • Loading branch information
colszowka committed Dec 14, 2018
2 parents a0d955f + edf6c48 commit 772984e
Show file tree
Hide file tree
Showing 15 changed files with 421 additions and 4 deletions.
5 changes: 2 additions & 3 deletions .rubocop.yml
Expand Up @@ -84,9 +84,8 @@ Style/TrailingCommaInHashLiteral:
EnforcedStyleForMultiline: comma

Style/ClassAndModuleChildren:
Exclude:
# Prefer rails convention
- 'app/controllers/**/*'
# Against rails conventions
Enabled: false

RSpec/FilePath:
Exclude:
Expand Down
1 change: 1 addition & 0 deletions app/assets/stylesheets/application.css.sass
Expand Up @@ -32,6 +32,7 @@ $weight-bold: 700
$red: #A61414
$link: $red
$primary: $red
$success: #59A80F
$text: #222

$navbar-height: 5rem
Expand Down
14 changes: 14 additions & 0 deletions app/assets/stylesheets/components/project_health_tag.sass
@@ -0,0 +1,14 @@
.project-health-tags
@extend .field, .is-grouped, .is-grouped-multiline

.project-health-tag
@extend .control
&.level-green
.tag
@extend .is-success
&.level-yellow
.tag
@extend .is-warning
&.level-red
.tag
@extend .is-danger
4 changes: 4 additions & 0 deletions app/helpers/application_helper.rb
Expand Up @@ -79,6 +79,10 @@ def category_card(category, compact: false, inline: false)
render partial: "components/category_card", locals: locals
end

def project_health_tag(health_status)
render "components/project_health_tag", status: health_status
end

def component_example(heading, &block)
render "components/component_example", heading: heading, &block
end
Expand Down
3 changes: 3 additions & 0 deletions app/models/github_repo.rb
Expand Up @@ -9,6 +9,9 @@ class GithubRepo < ApplicationRecord
inverse_of: :github_repo,
dependent: :nullify

has_many :rubygems,
through: :projects

def self.update_batch
where("updated_at < ? ", 24.hours.ago.utc)
.order(updated_at: :asc)
Expand Down
5 changes: 4 additions & 1 deletion app/models/project.rb
Expand Up @@ -26,6 +26,7 @@ class Project < ApplicationRecord
inverse_of: :projects

scope :includes_associations, -> { includes(:github_repo, :rubygem, :categories) }
scope :with_score, -> { where.not(score: nil) }

include PgSearch
pg_search_scope :search_scope,
Expand All @@ -42,7 +43,7 @@ class Project < ApplicationRecord
ranked_by: ":tsearch * (#{table_name}.score + 1) * (#{table_name}.score + 1)"

def self.search(query)
where.not(score: nil).includes_associations.search_scope(query).limit(25)
with_score.includes_associations.search_scope(query).limit(25)
end

delegate :current_version,
Expand Down Expand Up @@ -70,6 +71,8 @@ def self.search(query)
:homepage_url,
:watchers_count,
:description,
:archived?,
:repo_pushed_at,
:wiki_url,
:issues_url,
:url,
Expand Down
48 changes: 48 additions & 0 deletions app/models/project/health.rb
@@ -0,0 +1,48 @@
# frozen_string_literal: true

#
# Creates a health assessment for given project instance based on health checks defined
# in Project::Health::Checks
#
class Project::Health
class Status
attr_accessor :key, :level, :icon, :check_block
def initialize(key, level, icon, &check_block)
self.key = key
self.level = level
self.icon = icon
self.check_block = check_block

raise ArgumentError, "Unknown level #{level}" unless %i[red yellow green].include? level
raise I18n::MissingTranslation.new(I18n.locale, i18n_key) unless I18n.exists? i18n_key
end

def label
I18n.t i18n_key
end

def applies?(project)
!!check_block.call(project)
end

private

def i18n_key
"project_health.#{key}"
end
end

HEALTHY_STATUS = Project::Health::Status.new(:healthy, :green, :heartbeat, &:present?)

attr_accessor :project, :checks
private :project=, :checks=

def initialize(project)
self.project = project
self.checks = Checks::ALL
end

def status
@status ||= checks.select { |check| check.applies? project }.presence || [HEALTHY_STATUS]
end
end
56 changes: 56 additions & 0 deletions app/models/project/health/checks.rb
@@ -0,0 +1,56 @@
# frozen_string_literal: true

module Project::Health::Checks
GITHUB_REPO_ARCHIVED = Project::Health::Status.new(:github_repo_archived, :red, :github, &:github_repo_archived?)

GITHUB_REPO_GONE = Project::Health::Status.new(:github_repo_gone, :red, :github) do |project|
project.github_repo_path? &&
project.github_repo.nil?
end

GITHUB_REPO_NO_COMMIT_ACTIVITY = Project::Health::Status.new(:no_commit_activity, :red, :github) do |project|
project.github_repo_repo_pushed_at &&
project.github_repo_repo_pushed_at < 3.years.ago
end

GITHUB_REPO_LOW_COMMIT_ACTIVITY = Project::Health::Status.new(:low_commit_activity, :yellow, :github) do |project|
!GITHUB_REPO_NO_COMMIT_ACTIVITY.applies?(project) &&
project.github_repo_average_recent_committed_at &&
project.github_repo_average_recent_committed_at < 3.years.ago
end

GITHUB_REPO_OPEN_ISSUES = Project::Health::Status.new(:github_repo_open_issues, :yellow, :github) do |project|
project.github_repo_total_issues_count &&
project.github_repo_total_issues_count > 5 &&
project.github_repo_issue_closure_rate < 75
end

RUBYGEM_ABANDONED = Project::Health::Status.new(:rubygem_abandoned, :red, :diamond) do |project|
project.rubygem_latest_release_on &&
project.rubygem_latest_release_on < 3.years.ago
end

RUBYGEM_STALE = Project::Health::Status.new(:rubygem_stale, :yellow, :diamond) do |project|
!RUBYGEM_ABANDONED.applies?(project) &&
project.rubygem_latest_release_on &&
project.rubygem_latest_release_on < 1.year.ago
end

RUBYGEM_LONG_RUNNING = Project::Health::Status.new(:rubygem_long_running, :green, :diamond) do |project|
project.rubygem_latest_release_on &&
project.rubygem_first_release_on &&
project.rubygem_first_release_on < 5.years.ago &&
project.rubygem_latest_release_on > 1.year.ago
end

ALL = [
GITHUB_REPO_ARCHIVED,
GITHUB_REPO_GONE,
GITHUB_REPO_NO_COMMIT_ACTIVITY,
RUBYGEM_ABANDONED,
GITHUB_REPO_LOW_COMMIT_ACTIVITY,
GITHUB_REPO_OPEN_ISSUES,
RUBYGEM_STALE,
RUBYGEM_LONG_RUNNING,
].freeze
end
5 changes: 5 additions & 0 deletions app/views/components/_project_health_tag.html.slim
@@ -0,0 +1,5 @@
.project-health-tag class="level-#{status.level}"
.tags.has-addons
span.tag
span.icon: i.fa class="fa-#{status.icon}"
strong.tag= status.label
10 changes: 10 additions & 0 deletions app/views/pages/components/project_health_tag.html.slim
@@ -0,0 +1,10 @@
.hero
section.section: .container
p.heading= link_to "Ruby Toolbox UI Components Styleguide", "/pages/components"
h2= current_page.split("/").last.humanize

= component_example "Project Health tags" do
.project-health-tags
= project_health_tag Project::Health::HEALTHY_STATUS
- Project::Health::Checks::ALL.each do |status|
= project_health_tag status
5 changes: 5 additions & 0 deletions app/views/projects/_project.html.slim
Expand Up @@ -17,6 +17,11 @@
i.fa.fa-flask
span= project.score

.columns: .column
.project-health-tags
- Project::Health.new(project).status.each do |status|
= project_health_tag status

- if local_assigns[:show_categories] and project.categories.any?
.columns.is-hidden-desktop: .column
- project.categories.each do |category|
Expand Down
5 changes: 5 additions & 0 deletions app/views/projects/show.html.slim
Expand Up @@ -19,6 +19,11 @@
- @project.categories.each do |category|
= category_card category, compact: true, inline: true

.columns: .column
.project-health-tags
- Project::Health.new(@project).status.each do |status|
= project_health_tag status

.columns: .links.column
= render partial: "projects/links", locals: { project: @project }

Expand Down
10 changes: 10 additions & 0 deletions config/locales/en.yml
Expand Up @@ -32,3 +32,13 @@ en:
name: The Ruby Toolbox
tagline: Know your options!
description: Explore and compare open source Ruby libraries
project_health:
github_repo_archived: Repository is archived
github_repo_gone: Repository is gone
github_repo_open_issues: There's a lot of open issues
rubygem_abandoned: No release in over 3 years
rubygem_stale: No release in over a year
low_commit_activity: Low commit activity in last 3 years
no_commit_activity: No commit activity in last 3 years
healthy: The project appears to be in a healthy, maintained state
rubygem_long_running: Has been around for more than 5 years and still receives updates

0 comments on commit 772984e

Please sign in to comment.