From 4df8201240b9e92c40ab2fa6f90987e63849ac49 Mon Sep 17 00:00:00 2001 From: Cedric Hurst Date: Sun, 27 Oct 2024 19:38:27 -0400 Subject: [PATCH 01/20] expose postgres port as 6432 --- docker-compose.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 2a851a4f..9de3099d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,6 +3,8 @@ services: image: postgres volumes: - postgresqldata:/var/lib/postgresql/data + ports: + - "6432:5432" environment: POSTGRES_USER: "postgres" POSTGRES_PASSWORD: "mysecretpassword" From 7f4ba772197d4806958e4603718ab94a316a8685 Mon Sep 17 00:00:00 2001 From: Cedric Hurst Date: Sun, 27 Oct 2024 19:38:40 -0400 Subject: [PATCH 02/20] set ruby local version in asdf (if using) --- .tool-versions | 1 + 1 file changed, 1 insertion(+) create mode 100644 .tool-versions diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 00000000..33a8789f --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +ruby 2.7.7 From 91fd50e749f4fa93a878d3e4a111f2a53c9aee7b Mon Sep 17 00:00:00 2001 From: Cedric Hurst Date: Sun, 27 Oct 2024 23:21:32 -0400 Subject: [PATCH 03/20] update bcrypt and nio4r for apple silicon --- Gemfile | 2 ++ Gemfile.lock | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Gemfile b/Gemfile index 8f7cc1eb..509d70a4 100644 --- a/Gemfile +++ b/Gemfile @@ -9,6 +9,7 @@ gem 'bootsnap','>= 1.1.0', require: false gem 'addressable' gem 'aws-sdk-s3' gem 'bourbon', '~> 4.0.2' +gem 'bcrypt', '~> 3.1.7' gem 'clearance', '~> 2.5.0' gem 'clearance-deprecated_password_strategies' gem 'coffee-rails' @@ -24,6 +25,7 @@ gem 'jquery-rails', '~> 4.1.1' gem 'jquery-ui-rails', '~> 3.0.1' gem 'magnific-popup-rails' gem 'mail', '~> 2.7.1' # TODO see if this is still required in Ruby 3 +gem 'nio4r', '>= 2.7.3' gem 'nokogiri', '~> 1.14.3' gem 'pg', '~> 1.4.4' gem 'puma', '< 7' diff --git a/Gemfile.lock b/Gemfile.lock index 26f259d6..acbcf4d5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -85,7 +85,7 @@ GEM babel-transpiler (0.7.0) babel-source (>= 4.0, < 6) execjs (~> 2.0) - bcrypt (3.1.18) + bcrypt (3.1.20) bindex (0.8.1) bootsnap (1.13.0) msgpack (~> 1.2) @@ -225,7 +225,7 @@ GEM metaclass (~> 0.0.1) msgpack (1.6.0) newrelic_rpm (7.2.0) - nio4r (2.5.8) + nio4r (2.7.3) nokogiri (1.14.3) mini_portile2 (~> 2.8.0) racc (~> 1.4) @@ -426,6 +426,7 @@ PLATFORMS DEPENDENCIES addressable aws-sdk-s3 + bcrypt (~> 3.1.7) bootsnap (>= 1.1.0) bourbon (~> 4.0.2) bourne @@ -456,6 +457,7 @@ DEPENDENCIES magnific-popup-rails mail (~> 2.7.1) newrelic_rpm (~> 7.2) + nio4r (>= 2.7.3) nokogiri (~> 1.14.3) pg (~> 1.4.4) pry From 81e0c4daa6fa669bafd14adc162a58136f5385e7 Mon Sep 17 00:00:00 2001 From: Cedric Hurst Date: Sun, 27 Oct 2024 23:22:25 -0400 Subject: [PATCH 04/20] add ruby standard for formatting, etc --- Gemfile | 2 ++ Gemfile.lock | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/Gemfile b/Gemfile index 509d70a4..4d131909 100644 --- a/Gemfile +++ b/Gemfile @@ -27,6 +27,7 @@ gem 'magnific-popup-rails' gem 'mail', '~> 2.7.1' # TODO see if this is still required in Ruby 3 gem 'nio4r', '>= 2.7.3' gem 'nokogiri', '~> 1.14.3' +# gem 'ruby-openai' gem 'pg', '~> 1.4.4' gem 'puma', '< 7' gem 'rack-attack' @@ -53,6 +54,7 @@ group :development do gem 'dotenv-rails' gem 'letter_opener' gem 'listen' + gem 'standard' gem 'web-console' end diff --git a/Gemfile.lock b/Gemfile.lock index acbcf4d5..d7927793 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -65,6 +65,7 @@ GEM argon2 (2.1.1) ffi (~> 1.14) ffi-compiler (~> 1.0) + ast (2.4.2) aws-eventstream (1.2.0) aws-partitions (1.646.0) aws-sdk-core (3.160.0) @@ -199,10 +200,13 @@ GEM jquery-ui-rails (3.0.1) jquery-rails railties (>= 3.1.0) + json (2.7.4) + language_server-protocol (3.17.0.3) launchy (2.5.0) addressable (~> 2.7) letter_opener (1.8.1) launchy (>= 2.2, < 3) + lint_roller (1.1.0) listen (3.1.5) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) @@ -229,6 +233,10 @@ GEM nokogiri (1.14.3) mini_portile2 (~> 2.8.0) racc (~> 1.4) + parallel (1.26.3) + parser (3.3.5.0) + ast (~> 2.4.1) + racc pg (1.4.4) protobuf-cucumber (3.10.8) activesupport (>= 3.2) @@ -288,6 +296,7 @@ GEM method_source rake (>= 12.2) thor (~> 1.0) + rainbow (3.1.1) rake (13.0.6) rb-fsevent (0.10.3) rb-inotify (0.10.0) @@ -300,6 +309,7 @@ GEM tilt redcarpet (3.5.1) regexp_parser (2.7.0) + rexml (3.3.9) roda (3.61.0) rack rollbar (3.3.2) @@ -324,6 +334,23 @@ GEM rspec-mocks (~> 3.11) rspec-support (~> 3.11) rspec-support (3.12.0) + rubocop (1.64.1) + json (~> 2.3) + language_server-protocol (>= 3.17.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.31.1, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.32.3) + parser (>= 3.3.1.0) + rubocop-performance (1.21.1) + rubocop (>= 1.48.1, < 2.0) + rubocop-ast (>= 1.31.1, < 2.0) + ruby-progressbar (1.13.0) ruby_dep (1.5.0) rubyzip (2.3.2) sass (3.7.3) @@ -371,6 +398,18 @@ GEM sprockets-redirect (0.1.0) activesupport (>= 3.1.0) rack + standard (1.37.0) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.0) + rubocop (~> 1.64.0) + standard-custom (~> 1.0.0) + standard-performance (~> 1.4) + standard-custom (1.0.2) + lint_roller (~> 1.0) + rubocop (~> 1.50) + standard-performance (1.4.0) + lint_roller (~> 1.1) + rubocop-performance (~> 1.21.0) stimulus-rails (1.2.1) railties (>= 6.0.0) sucker_punch (3.1.0) @@ -398,6 +437,7 @@ GEM unf (0.1.4) unf_ext unf_ext (0.0.8.2) + unicode-display_width (2.6.0) uppy-s3_multipart (1.2.0) aws-sdk-s3 (~> 1.0) content_disposition (~> 1.0) @@ -481,6 +521,7 @@ DEPENDENCIES shrine-tus (~> 2.1) simple_form (~> 5.1) sprockets-redirect + standard stimulus-rails sucker_punch (~> 3.1) terser From 8f8bb706c17434fea143b7262a2e8eceade85142 Mon Sep 17 00:00:00 2001 From: Cedric Hurst Date: Sun, 27 Oct 2024 23:23:11 -0400 Subject: [PATCH 05/20] install ruby-openai --- Gemfile | 2 +- Gemfile.lock | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 4d131909..a2769e5b 100644 --- a/Gemfile +++ b/Gemfile @@ -27,7 +27,7 @@ gem 'magnific-popup-rails' gem 'mail', '~> 2.7.1' # TODO see if this is still required in Ruby 3 gem 'nio4r', '>= 2.7.3' gem 'nokogiri', '~> 1.14.3' -# gem 'ruby-openai' +gem 'ruby-openai' gem 'pg', '~> 1.4.4' gem 'puma', '< 7' gem 'rack-attack' diff --git a/Gemfile.lock b/Gemfile.lock index d7927793..c2aa74be 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -86,6 +86,7 @@ GEM babel-transpiler (0.7.0) babel-source (>= 4.0, < 6) execjs (~> 2.0) + base64 (0.2.0) bcrypt (3.1.20) bindex (0.8.1) bootsnap (1.13.0) @@ -152,6 +153,7 @@ GEM email_validator (2.2.3) activemodel erubi (1.12.0) + event_stream_parser (1.0.0) execjs (2.8.1) factory_girl (3.5.0) activesupport (>= 3.0.0) @@ -160,6 +162,13 @@ GEM railties (>= 3.0.0) faker (2.23.0) i18n (>= 1.8.11, < 2) + faraday (2.8.1) + base64 + faraday-net_http (>= 2.0, < 3.1) + ruby2_keywords (>= 0.0.4) + faraday-multipart (1.0.4) + multipart-post (~> 2) + faraday-net_http (3.0.2) ffi (1.15.5) ffi-compiler (1.0.1) ffi (>= 1.0.0) @@ -228,6 +237,7 @@ GEM mocha (1.2.1) metaclass (~> 0.0.1) msgpack (1.6.0) + multipart-post (2.4.1) newrelic_rpm (7.2.0) nio4r (2.7.3) nokogiri (1.14.3) @@ -350,7 +360,12 @@ GEM rubocop-performance (1.21.1) rubocop (>= 1.48.1, < 2.0) rubocop-ast (>= 1.31.1, < 2.0) + ruby-openai (7.3.1) + event_stream_parser (>= 0.3.0, < 2.0.0) + faraday (>= 1) + faraday-multipart (>= 1) ruby-progressbar (1.13.0) + ruby2_keywords (0.0.5) ruby_dep (1.5.0) rubyzip (2.3.2) sass (3.7.3) @@ -514,6 +529,7 @@ DEPENDENCIES redcarpet rollbar rspec-rails (~> 6.0.1) + ruby-openai sassc-rails (~> 2.1) sham_rack shoulda-matchers (~> 3) From cfb90331b52ed9824ed82dfb158298849a73d842 Mon Sep 17 00:00:00 2001 From: Cedric Hurst Date: Sun, 27 Oct 2024 23:32:07 -0400 Subject: [PATCH 06/20] add initial project analysis models --- app/models/project_analysis.rb | 29 +++++++++++++ app/models/project_analysis_score.rb | 9 ++++ app/models/project_analysis_scores.rb | 34 +++++++++++++++ .../20241028031112_create_project_analyses.rb | 41 +++++++++++++++++++ 4 files changed, 113 insertions(+) create mode 100644 app/models/project_analysis.rb create mode 100644 app/models/project_analysis_score.rb create mode 100644 app/models/project_analysis_scores.rb create mode 100644 db/migrate/20241028031112_create_project_analyses.rb diff --git a/app/models/project_analysis.rb b/app/models/project_analysis.rb new file mode 100644 index 00000000..3ed711bd --- /dev/null +++ b/app/models/project_analysis.rb @@ -0,0 +1,29 @@ +class ProjectAnalysis < ApplicationRecord + belongs_to :project, optional: true + + validates :summary, :language_code, :applicant_role, :scores, presence: true + + validate :validate_scores + + # Serialize the tags array + serialize :tags, Array + + # Access scores as a nested object + def scores + @scores ||= ProjectAnalysisScores.new(self) + end + + private def validate_scores + scores_class = ProjectAnalysisScores::SCORE_FIELDS + scores_class.each do |field| + score_value = send(:"#{field}_score") + reason = send(:"#{field}_score_reason") + + if score_value.nil? || reason.blank? + errors.add(:base, "Both score and reason must be present for #{field}") + elsif score_value < 0 || score_value > 1 + errors.add(:base, "Score for #{field} must be between 0.0 and 1.0") + end + end + end +end diff --git a/app/models/project_analysis_score.rb b/app/models/project_analysis_score.rb new file mode 100644 index 00000000..46df58c1 --- /dev/null +++ b/app/models/project_analysis_score.rb @@ -0,0 +1,9 @@ +# app/models/project_analysis_score.rb +class ProjectAnalysisScore + attr_accessor :score, :reason + + def initialize(score:, reason:) + @score = score + @reason = reason + end +end diff --git a/app/models/project_analysis_scores.rb b/app/models/project_analysis_scores.rb new file mode 100644 index 00000000..af19f2a9 --- /dev/null +++ b/app/models/project_analysis_scores.rb @@ -0,0 +1,34 @@ +class ProjectAnalysisScores + SCORE_FIELDS = %w[ + reach + proximity + self_interest + budget_clarity + impact_of_grant + feasibility + credibility + innovation + sustainability + community_involvement + cultural_relevance + scalability + urgency + alignment_with_mission + whimsy + social_good + artistic_merit + ] + + def initialize(project_analysis) + @project_analysis = project_analysis + end + + SCORE_FIELDS.each do |field| + define_method(field) do + ProjectAnalysisScore.new( + score: @project_analysis.send(:"#{field}_score"), + reason: @project_analysis.send(:"#{field}_score_reason") + ) + end + end +end diff --git a/db/migrate/20241028031112_create_project_analyses.rb b/db/migrate/20241028031112_create_project_analyses.rb new file mode 100644 index 00000000..bf2240de --- /dev/null +++ b/db/migrate/20241028031112_create_project_analyses.rb @@ -0,0 +1,41 @@ +class CreateProjectAnalyses < ActiveRecord::Migration[6.1] + def change + create_table :project_analyses do |t| + t.references :project, foreign_key: true, null: false + + t.text :summary + t.string :language_code + t.string :applicant_role + t.date :funding_deadline + t.string :tags, array: true + + # Scores (flattened) + score_fields = [ + "reach", + "proximity", + "self_interest", + "budget_clarity", + "impact_of_grant", + "feasibility", + "credibility", + "innovation", + "sustainability", + "community_involvement", + "cultural_relevance", + "scalability", + "urgency", + "alignment_with_mission", + "whimsy", + "social_good", + "artistic_merit" + ] + + score_fields.each do |field| + t.decimal "#{field}_score", precision: 4, scale: 2 + t.text "#{field}_score_reason" + end + + t.timestamps + end + end +end From 8d2b6d41c9d4d1618b4fe34c27ba286ffbb0f6bf Mon Sep 17 00:00:00 2001 From: Cedric Hurst Date: Thu, 31 Oct 2024 19:04:56 -0500 Subject: [PATCH 07/20] add project analysis models --- app/models/project.rb | 49 ++++++++++--------- app/models/project_analysis.rb | 2 +- app/models/project_analysis_scores.rb | 13 ++--- ..._add_project_analysis_suggestion_column.rb | 5 ++ ...dd_project_analysis_prompt_info_columns.rb | 11 +++++ ...1603_add_project_analysis_novelty_score.rb | 6 +++ 6 files changed, 55 insertions(+), 31 deletions(-) create mode 100644 db/migrate/20241029213306_add_project_analysis_suggestion_column.rb create mode 100644 db/migrate/20241029213419_add_project_analysis_prompt_info_columns.rb create mode 100644 db/migrate/20241030001603_add_project_analysis_novelty_score.rb diff --git a/app/models/project.rb b/app/models/project.rb index 8f338c7a..739d56ad 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -10,15 +10,16 @@ class Project < ApplicationRecord belongs_to :hidden_by_user, class_name: "User", optional: true has_many :comments has_many :votes - has_many :users, :through => :votes + has_many :users, through: :votes has_many :photos, -> { merge(Photo.sorted) } has_many :real_photos, -> { merge(Photo.image_files.sorted) }, class_name: "Photo" - has_one :primary_photo, -> { merge(Photo.image_files.sorted) }, class_name: "Photo" + has_one :primary_photo, -> { merge(Photo.image_files.sorted) }, class_name: "Photo" + has_one :project_analysis, dependent: :destroy before_validation UrlNormalizer.new(:url, :rss_feed_url) - validates :url, :rss_feed_url, :url => true, :allow_blank => true - validates_length_of :url, :rss_feed_url, :maximum => 255 + validates :url, :rss_feed_url, url: true, allow_blank: true + validates_length_of :url, :rss_feed_url, maximum: 255 validates_presence_of :name validates_presence_of :title @@ -28,7 +29,7 @@ class Project < ApplicationRecord validates_presence_of :use_for_money validates_presence_of :chapter_id - delegate :name, :to => :chapter, :prefix => true + delegate :name, to: :chapter, prefix: true before_save :ensure_funded_description before_save :update_photo_order @@ -40,9 +41,9 @@ class Project < ApplicationRecord # specify the default fields on which full text search will be performed extend Searchable(:name, :title, :email, :about_me, :about_project, :use_for_money, - :extra_answer_1, :extra_answer_2, :extra_answer_3) + :extra_answer_1, :extra_answer_2, :extra_answer_3) - scope :public_search, lambda { |query| search({ name: query, title: query, funded_description: query, url: query }, false) } + scope :public_search, lambda { |query| search({name: query, title: query, funded_description: query, url: query}, false) } accepts_nested_attributes_for :photos, allow_destroy: true @@ -51,9 +52,9 @@ def self.winner_count end def self.visible_to(user) - joins(:chapter). - joins("LEFT OUTER JOIN roles ON roles.chapter_id = chapters.id"). - where("roles.user_id = #{user.id} OR chapters.name = ?", Chapter::ANY_CHAPTER_NAME) + joins(:chapter) + .joins("LEFT OUTER JOIN roles ON roles.chapter_id = chapters.id") + .where("roles.user_id = #{user.id} OR chapters.name = ?", Chapter::ANY_CHAPTER_NAME) end def self.with_votes_for_chapter(c) @@ -65,15 +66,15 @@ def self.with_votes_by_members_of_chapter(c) end def self.voted_for_by_members_of(chapter) - joins(:users => :chapters).where("chapters.id = ? OR chapters.name = ?", chapter.id, Chapter::ANY_CHAPTER_NAME) + joins(users: :chapters).where("chapters.id = ? OR chapters.name = ?", chapter.id, Chapter::ANY_CHAPTER_NAME) end def self.during_timeframe(start_date, end_date) # FIXME the database stores dates in UTC, whereas we parse the dates # provided in the local zone. This can result in mismatches when the # overlap crosses midnight. - start_date = 100.years.ago.strftime('%Y-%m-%d') if start_date.blank? - end_date = Time.zone.now.strftime('%Y-%m-%d') if end_date.blank? + start_date = 100.years.ago.strftime("%Y-%m-%d") if start_date.blank? + end_date = Time.zone.now.strftime("%Y-%m-%d") if end_date.blank? where( "projects.created_at > ? AND projects.created_at < ?", @@ -84,15 +85,15 @@ def self.during_timeframe(start_date, end_date) def self.by_vote_count(sort: nil) order = case sort - when "date" then "projects.created_at DESC, vote_count DESC" - when "title" then "projects.title ASC, vote_count DESC" - else "vote_count DESC, projects.created_at ASC" - end + when "date" then "projects.created_at DESC, vote_count DESC" + when "title" then "projects.title ASC, vote_count DESC" + else "vote_count DESC, projects.created_at ASC" + end - select("projects.chapter_id, projects.id, projects.title, projects.funded_on, COUNT(votes.project_id) as vote_count, projects.created_at"). - group("projects.id, projects.title, votes.project_id"). - joins(:users). - order(order) + select("projects.chapter_id, projects.id, projects.title, projects.funded_on, COUNT(votes.project_id) as vote_count, projects.created_at") + .group("projects.id, projects.title, votes.project_id") + .joins(:users) + .order(order) end def self.winners @@ -114,7 +115,7 @@ def self.csv_export(projects) end def self.attributes_for_export - %w(name title about_project use_for_money about_me url email phone chapter_name id created_at funded_on extra_question_1 extra_answer_1 extra_question_2 extra_answer_2 extra_question_3 extra_answer_3 rss_feed_url hidden_at hidden_reason) + %w[name title about_project use_for_money about_me url email phone chapter_name id created_at funded_on extra_question_1 extra_answer_1 extra_question_2 extra_answer_2 extra_question_3 extra_answer_3 rss_feed_url hidden_at hidden_reason] end def to_a @@ -184,11 +185,11 @@ def save end def extra_question(num) - (question = read_attribute("extra_question_#{num}".to_sym)) && question.present? ? question : nil + ((question = read_attribute(:"extra_question_#{num}")) && question.present?) ? question : nil end def extra_answer(num) - (answer = read_attribute("extra_answer_#{num}".to_sym)) && answer.present? ? answer : nil + ((answer = read_attribute(:"extra_answer_#{num}")) && answer.present?) ? answer : nil end def hide!(reason, user) diff --git a/app/models/project_analysis.rb b/app/models/project_analysis.rb index 3ed711bd..ed135073 100644 --- a/app/models/project_analysis.rb +++ b/app/models/project_analysis.rb @@ -6,7 +6,7 @@ class ProjectAnalysis < ApplicationRecord validate :validate_scores # Serialize the tags array - serialize :tags, Array + # serialize :tags, Array # Access scores as a nested object def scores diff --git a/app/models/project_analysis_scores.rb b/app/models/project_analysis_scores.rb index af19f2a9..bf50b871 100644 --- a/app/models/project_analysis_scores.rb +++ b/app/models/project_analysis_scores.rb @@ -1,21 +1,22 @@ class ProjectAnalysisScores SCORE_FIELDS = %w[ - reach - proximity self_interest + proximity + novelty budget_clarity - impact_of_grant + whimsy + social_good + reach feasibility + urgency + impact_of_grant credibility innovation sustainability community_involvement cultural_relevance scalability - urgency alignment_with_mission - whimsy - social_good artistic_merit ] diff --git a/db/migrate/20241029213306_add_project_analysis_suggestion_column.rb b/db/migrate/20241029213306_add_project_analysis_suggestion_column.rb new file mode 100644 index 00000000..46f3af42 --- /dev/null +++ b/db/migrate/20241029213306_add_project_analysis_suggestion_column.rb @@ -0,0 +1,5 @@ +class AddProjectAnalysisSuggestionColumn < ActiveRecord::Migration[6.1] + def change + add_column :project_analyses, :suggestion, :text + end +end diff --git a/db/migrate/20241029213419_add_project_analysis_prompt_info_columns.rb b/db/migrate/20241029213419_add_project_analysis_prompt_info_columns.rb new file mode 100644 index 00000000..b1f11613 --- /dev/null +++ b/db/migrate/20241029213419_add_project_analysis_prompt_info_columns.rb @@ -0,0 +1,11 @@ +class AddProjectAnalysisPromptInfoColumns < ActiveRecord::Migration[6.1] + def change + add_column :project_analyses, :prompt_usage_data, :jsonb, default: {}, null: false + add_column :project_analyses, :prompt_tokens, :integer + add_column :project_analyses, :completion_tokens, :integer + add_column :project_analyses, :cached_tokens, :integer + add_column :project_analyses, :prompt_estimated_cost, :float + add_column :project_analyses, :prompt_input_params, :jsonb, default: {}, null: false + add_column :project_analyses, :prompt_response_data, :jsonb, default: {}, null: false + end +end diff --git a/db/migrate/20241030001603_add_project_analysis_novelty_score.rb b/db/migrate/20241030001603_add_project_analysis_novelty_score.rb new file mode 100644 index 00000000..b17d98f0 --- /dev/null +++ b/db/migrate/20241030001603_add_project_analysis_novelty_score.rb @@ -0,0 +1,6 @@ +class AddProjectAnalysisNoveltyScore < ActiveRecord::Migration[6.1] + def change + add_column :project_analyses, :novelty_score, :decimal, precision: 4, scale: 2 + add_column :project_analyses, :novelty_score_reason, :text + end +end From b9a85f0807512784bf5589e5a630b2dfefc5a9e5 Mon Sep 17 00:00:00 2001 From: Cedric Hurst Date: Thu, 31 Oct 2024 19:05:43 -0500 Subject: [PATCH 08/20] add project analysis generator --- app/services/project_analysis_generator.rb | 133 +++++++++ config/openai.yml | 280 ++++++++++++++++++ ...ect_analysis_generator_integration_spec.rb | 25 ++ .../project_analysis_generator_spec.rb | 58 ++++ 4 files changed, 496 insertions(+) create mode 100644 app/services/project_analysis_generator.rb create mode 100644 config/openai.yml create mode 100644 spec/services/project_analysis_generator_integration_spec.rb create mode 100644 spec/services/project_analysis_generator_spec.rb diff --git a/app/services/project_analysis_generator.rb b/app/services/project_analysis_generator.rb new file mode 100644 index 00000000..eb265a3d --- /dev/null +++ b/app/services/project_analysis_generator.rb @@ -0,0 +1,133 @@ +require "openai" +require "nokogiri" + +class ProjectAnalysisGenerator + def initialize(project_id) + @project = Project.find(project_id) + openai_config = Rails.application.config_for(:openai) + + # puts(JSON.generate({openai_config: openai_config}, indent: " ")) + + @openai_api_key = openai_config[:api_key] + raise "Error: OPENAI_API_KEY is not set in the configuration." if @openai_api_key.blank? + + @client = OpenAI::Client.new( + access_token: @openai_api_key, + log_error: true + ) + + config = openai_config[:project_analysis] + @model = config[:model] + @prompt = config[:prompt_text] + @format_instructions = config[:format_instructions] + @response_schema = config[:response_schema] + end + + def call + project_data = prepare_project_data + response = make_api_call(project_data) + + if response.nil? + raise "Error: OpenAI API call failed." + end + + parse_and_save_response(response) + end + + private + + def prepare_project_data + { + "project_id" => @project.id, + "title" => @project.title, + "about_me" => @project.about_me, + "about_project" => @project.about_project, + "use_for_money" => @project.use_for_money, + "created_at" => @project.created_at, + "chapter_name" => @project.chapter.name, + "chapter_country" => @project.chapter.country, + "chapter_description" => Nokogiri::HTML.fragment(@project.chapter.description).text.strip, + "extra_questions" => [ + {question: @project.extra_question_1, answer: @project.extra_answer_1}, + {question: @project.extra_question_2, answer: @project.extra_answer_2}, + {question: @project.extra_question_3, answer: @project.extra_answer_3} + ].reject { |qa| qa[:question].blank? && qa[:answer].blank? } + } + end + + def make_api_call(project_data) + parameters = { + model: @model, + messages: [ + {role: "user", content: @prompt}, + {role: "user", content: project_data.to_json}, + {role: "user", content: @format_instructions} + ], + functions: [ + { + name: "structured_output", + parameters: @response_schema + } + ], + function_call: "auto" + } + response = @client.chat(parameters: parameters) + puts(JSON.generate({response: response}, indent: " ")) + # response.merge( + # "prompt_usage_data" => { "total_tokens" => 800, "prompt_tokens" => 500, "completion_tokens" => 300 }, + # "prompt_estimated_cost" => 0.02 + # ) + response + rescue => e + Rails.logger.error("OpenAI API call failed: #{e}") + nil + end + + def parse_and_save_response(response) + response_messages = response.dig("choices", 0, "message") + response_data = if response_messages["function_call"] + JSON.parse(response_messages["function_call"]["arguments"]) + else + JSON.parse(response_messages["content"]) + end + + project_analysis_attributes = map_response_to_attributes(response_data) + + project_analysis = ProjectAnalysis.find_or_initialize_by(project_id: @project.id) + project_analysis.assign_attributes(project_analysis_attributes) + project_analysis.save! + + project_analysis + rescue JSON::ParserError => e + Rails.logger.error("Failed to parse OpenAI response: #{e.message}") + nil + end + + def map_response_to_attributes(response_data) + project_analysis_attributes = { + summary: response_data["summary"], + language_code: response_data["language_code"], + applicant_role: response_data["applicant_role"], + funding_deadline: response_data["funding_deadline"], + tags: response_data["tags"], + suggestion: response_data["suggestions"] + # prompt_usage_data: response["usage"] + } + + # Parse scores + scores = response_data["scores"] + + ProjectAnalysisScores::SCORE_FIELDS.each do |field| + score_data = scores[field] + + if score_data + project_analysis_attributes["#{field}_score"] = score_data["score"] + project_analysis_attributes["#{field}_score_reason"] = score_data["reason"] + else + Rails.logger.warn("Score for #{field} is missing in the response.") + end + end + + project_analysis_attributes + end +end diff --git a/config/openai.yml b/config/openai.yml new file mode 100644 index 00000000..1136efba --- /dev/null +++ b/config/openai.yml @@ -0,0 +1,280 @@ +default_settings: &default + api_key: <%= ENV['OPENAI_API_KEY'] || '' %> + project_analysis: + model: gpt-4o + prompt_text: | + You are an Awesome Foundation trustee. You have been asked to review the + following grant application and provide your feedback to qualify it for the + monthly award. + + format_instructions: | + DO NOT start the summary with unnecessary setup like 'This grant application + seeks funding for a community garden in Rogers Park...'. Instead just say + something like 'A community garden in Rogers Park...' + + For the score fields, provide a decimal value between 0 and 1, as well as a short + 2-sentence explanation for why the application meets or does not meet the + criteria described in the field. + + For the score fields, provide a JSON object with the score value as a number + (without quotes) and the reason field as a string. + + DO provide values for all scores. + DO NOT provide any other text in the result beyond the JSON. + DO NOT surround the JSON in backticks. + + Return all responses in English. + + response_schema: + "$defs": + ApplicantRole: + enum: + - community_leader + - teacher + - student + - artist + - academic + - entrepreneur + - nonprofit_organizer + - activist + - healthcare_worker + - scientist + - engineer + - social_worker + - musician + - filmmaker + - writer + - performer + - environmentalist + - technologist + - craftsperson + - parent + title: ApplicantRole + type: string + ProjectTag: + enum: + - science + - music + - technology + - art + - education + - environment + - health + - community + - social_justice + - culture + - history + - literature + - sports + - mental_health + - youth + - seniors + - accessibility + - urban_gardening + - sustainability + - renewable_energy + - robotics + - coding + - wildlife_conservation + - food_security + - housing + - dance + - theater + - filmmaking + - photography + - public_art + - disaster_relief + - transportation + - civic_engagement + - public_health + - water_safety + - astronomy + - journalism + - advocacy + - peacebuilding + - maker_space + - entrepreneurship + - clean_technology + - urban_planning + - heritage + - migration + - storytelling + - digital_inclusion + - biodiversity + - maker_education + - language_preservation + - homelessness + - libraries + - autism + - arts_activism + - food + - disability + - gender_equality + - lgbtq + - bicycles + - media_literacy + - animals + - crafts + - parks + title: ProjectTag + type: string + Score: + properties: + score: + title: Score + type: number + description: + Decimal value between 0 and 1 representing whether or not the + project fits this criterion described in the field. Two digit precision. + reason: + title: Reason + type: string + description: + One to two sentences explaining why the application received its score + for this field. + required: + - score + - reason + title: Score + type: object + Scores: + properties: + reach: + "$ref": "#/$defs/Score" + description: Number of people impacted, focusing on those local to the chapter's region. + proximity: + "$ref": "#/$defs/Score" + description: Geographic closeness of project and applicant to chapter community. + novelty: + "$ref": "#/$defs/Score" + description: Originality and uniqueness of the project’s concept or approach. + public_beneficiary: + "$ref": "#/$defs/Score" + description: | + Prioritizes funds that benefit the public or community over the applicant’s personal gain. + Score low if funds are mainly used for applicant’s personal expenses (e.g., stipends). + budget_clarity: + "$ref": "#/$defs/Score" + description: Clarity and detail of fund allocation within the proposal. + impact_of_grant: + "$ref": "#/$defs/Score" + description: Significance of the $1000 grant in achieving the project’s goals. + feasibility: + "$ref": "#/$defs/Score" + description: Practicality of executing the project within the stated timeline and budget. + credibility: + "$ref": "#/$defs/Score" + description: Applicant’s experience and credibility relevant to the project’s scope. + innovation: + "$ref": "#/$defs/Score" + description: Originality of the idea and potential to introduce new solutions. + sustainability: + "$ref": "#/$defs/Score" + description: Long-term viability and potential for impact beyond the grant period. + community_involvement: + "$ref": "#/$defs/Score" + description: Degree of community engagement and opportunities for collaboration. + cultural_relevance: + "$ref": "#/$defs/Score" + description: Alignment with local cultural values and potential to raise awareness. + scalability: + "$ref": "#/$defs/Score" + description: Potential for project expansion or replication in other contexts. + urgency: + "$ref": "#/$defs/Score" + description: Immediacy of need for funds; score lower if impact isn’t time-sensitive. + alignment_with_mission: + "$ref": "#/$defs/Score" + description: Project alignment with the Awesome Foundation’s mission or chapter focus. + whimsy: + "$ref": "#/$defs/Score" + description: Playful, imaginative qualities that inspire curiosity or delight. + social_good: + "$ref": "#/$defs/Score" + description: Potential for positive social impact and contribution to community welfare. + artistic_merit: + "$ref": "#/$defs/Score" + description: Artistic quality or creativity, especially for arts-focused projects. + required: + - reach + - proximity + - public_beneficiary + - budget_clarity + - impact_of_grant + - feasibility + - innovation + - sustainability + - community_involvement + - cultural_relevance + - scalability + - urgency + - alignment_with_mission + - whimsy + - social_good + - artistic_merit + title: Scores + type: object + properties: + project_id: + title: Project Id + type: integer + summary: + description: + A brief overview of the project, outlining its goals, activities, + and potential risks. + title: Summary + type: string + tags: + description: + List of tags representing the project's themes. Should be instances + of the provided list of enums. + items: + "$ref": "#/$defs/ProjectTag" + title: Tags + type: array + applicant_role: + "$ref": "#/$defs/ApplicantRole" + description: + The applicant's primary role or background in relation to the project. + Should be an be one of the provided list of enums. + language_code: + description: + The ISO 639-1 code representing the language of the project description, + e.g., 'en' for English. + title: Language Code + type: string + funding_deadline: + anyOf: + - type: string + - type: "null" + default: + description: + The date by which the grant funds are needed to ensure timely execution + of the project, in ISO format. + title: Funding Deadline + suggestions: + description: + 4 - 6 sentences with any additional comments or suggestions for the + applicant. + title: Suggestions + type: string + scores: + "$ref": "#/$defs/Scores" + required: + - project_id + - summary + - tags + - applicant_role + - language_code + - scores + title: ProjectSummary + type: object + +development: + <<: *default + +production: + <<: *default + +test: + <<: *default diff --git a/spec/services/project_analysis_generator_integration_spec.rb b/spec/services/project_analysis_generator_integration_spec.rb new file mode 100644 index 00000000..91127baa --- /dev/null +++ b/spec/services/project_analysis_generator_integration_spec.rb @@ -0,0 +1,25 @@ +require "rails_helper" + +RSpec.describe "ProjectAnalysisGenerator Integration", type: :integration do + let(:project) { create(:project) } + + before do + skip "Integration test skipped: OPENAI_API_KEY is not configured." if ENV["OPENAI_API_KEY"].blank? + end + + it "calls the OpenAI API with a real key and validates the response", integration: true do + generator = ProjectAnalysisGenerator.new(project.id) + expect { generator.call }.not_to raise_error + + analysis = ProjectAnalysis.last + expect(analysis).not_to be_nil + expect(analysis.summary).to be_a(String) + expect(analysis.language_code).to be_a(String) + expect(analysis.scores).to be_a(Hash) + expect(analysis.scores).to include(*ProjectAnalysisScores::SCORE_FIELDS) + + [*ProjectAnalysisScores::SCORE_FIELDS].each do |field| + expect(analysis.scores[field][:reason]).to be_a(String) + end + end +end diff --git a/spec/services/project_analysis_generator_spec.rb b/spec/services/project_analysis_generator_spec.rb new file mode 100644 index 00000000..94f5691f --- /dev/null +++ b/spec/services/project_analysis_generator_spec.rb @@ -0,0 +1,58 @@ +require "rails_helper" + +RSpec.describe ProjectAnalysisGenerator, type: :service do + let(:project) { create(:project) } + + before do + # Mock the OpenAI API client + @mock_client = instance_double(OpenAI::Client) + allow(OpenAI::Client).to receive(:new).and_return(@mock_client) + end + + describe "#call" do + context "when OpenAI API returns a valid response" do + it "creates a ProjectAnalysis record" do + # Mock the API response + api_response = { + "choices" => [ + { + "message" => { + "content" => { + "summary" => "A great project.", + "language_code" => "en", + "applicant_role" => "Founder", + "scores" => { + "impact" => {"score" => 0.9, "reason" => "High impact"}, + "feasibility" => {"score" => 0.8, "reason" => "Feasible"}, + "innovation" => {"score" => 0.7, "reason" => "Innovative"} + } + }.to_json + } + } + ] + } + allow(@mock_client).to receive(:chat).and_return(api_response) + + generator = ProjectAnalysisGenerator.new(project.id) + expect { + generator.call + }.to change { ProjectAnalysis.count }.by(1) + + analysis = ProjectAnalysis.last + expect(analysis.summary).to eq("A great project.") + expect(analysis.language_code).to eq("en") + expect(analysis.scores.impact_score).to eq(0.9) + expect(analysis.scores.impact_reason).to eq("High impact") + end + end + + context "when OpenAI API fails" do + it "raises an error" do + allow(@mock_client).to receive(:chat).and_raise(StandardError.new("API Error")) + + generator = ProjectAnalysisGenerator.new(project.id) + expect { generator.call }.to raise_error("Error: OpenAI API call failed.") + end + end + end +end From 27d8fba3822f1c94792e724b1c846d7b4d7163e8 Mon Sep 17 00:00:00 2001 From: Cedric Hurst Date: Thu, 31 Oct 2024 19:06:18 -0500 Subject: [PATCH 09/20] add project analysis asynchronous job --- app/jobs/application_job.rb | 7 +++++++ app/jobs/project_analysis_job.rb | 9 +++++++++ 2 files changed, 16 insertions(+) create mode 100644 app/jobs/application_job.rb create mode 100644 app/jobs/project_analysis_job.rb diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb new file mode 100644 index 00000000..d394c3d1 --- /dev/null +++ b/app/jobs/application_job.rb @@ -0,0 +1,7 @@ +class ApplicationJob < ActiveJob::Base + # Automatically retry jobs that encountered a deadlock + # retry_on ActiveRecord::Deadlocked + + # Most jobs are safe to ignore if the underlying records are no longer available + # discard_on ActiveJob::DeserializationError +end diff --git a/app/jobs/project_analysis_job.rb b/app/jobs/project_analysis_job.rb new file mode 100644 index 00000000..3e7b7874 --- /dev/null +++ b/app/jobs/project_analysis_job.rb @@ -0,0 +1,9 @@ +class ProjectAnalysisJob < ApplicationJob + queue_as :default + + def perform(project_id) + ProjectAnalysisGenerator.new(project_id).call + rescue => e + Rails.logger.error("Failed to generate project analysis for Project #{project_id}: #{e.message}") + end +end From 34537329a28842c6a8356b2b53dc6354658c92d1 Mon Sep 17 00:00:00 2001 From: Cedric Hurst Date: Thu, 31 Oct 2024 19:06:48 -0500 Subject: [PATCH 10/20] add project analysis view --- app/assets/stylesheets/_base-variables.scss | 24 +++- app/assets/stylesheets/_project_analyses.scss | 45 ++++++ app/assets/stylesheets/_projects-index.scss | 132 ++++++++++-------- app/assets/stylesheets/application.css.scss | 65 ++++----- .../project_analyses_controller.rb | 27 ++++ app/views/project_analyses/show.html.erb | 51 +++++++ app/views/projects/_project_details.html.erb | 1 + config/locales/en.yml | 2 + config/routes.rb | 72 +++++----- spec/factories.rb | 20 ++- spec/jobs/project_analysis_job_spec.rb | 5 + 11 files changed, 305 insertions(+), 139 deletions(-) create mode 100644 app/assets/stylesheets/_project_analyses.scss create mode 100644 app/controllers/project_analyses_controller.rb create mode 100644 app/views/project_analyses/show.html.erb create mode 100644 spec/jobs/project_analysis_job_spec.rb diff --git a/app/assets/stylesheets/_base-variables.scss b/app/assets/stylesheets/_base-variables.scss index e62a646f..db3074c3 100644 --- a/app/assets/stylesheets/_base-variables.scss +++ b/app/assets/stylesheets/_base-variables.scss @@ -2,16 +2,28 @@ $gw-column: 60px; $gw-gutter: 20px; $serif: Georgia, Cambria, serif; -$sans-serif: 'museo-sans', 'proxima-nova', $helvetica; +$sans-serif: "museo-sans", "proxima-nova", $helvetica; -$base-font-color: rgb(55,55,55); -$lighter-base-font-color: rgb(46,46,46); +$base-font-color: rgb(55, 55, 55); +$lighter-base-font-color: rgb(46, 46, 46); $muted-color: #ccc; $pink: rgb(255, 75, 126); $button-pink: rgb(252, 103, 147); -$blue: rgb(34,122,255); //blue +$blue: rgb(34, 122, 255); //blue + +$green-background: #a7f3d0; +$green-text: #065f46; + +$yellow-background: #fef08a; +$yellow-text: #854d0e; + +$red-background: #fecaca; +$red-text: #7f1d1d; + +$base-font-color: rgb(55, 55, 55); +$lighter-base-font-color: rgb(46, 46, 46); /*** * Responsiveness Sizes @@ -22,7 +34,7 @@ $blue: rgb(34,122,255); //blue * mobile-small: for additional changes that accommdate very small screens like iPhone 5S ***/ -$maximum-width: 60em; // Equivalent to 969px; +$maximum-width: 60em; // Equivalent to 969px; $tablet-standard: 60em; $mobile-standard: 30em; // Equivalent to 480px; -$mobile-small: 20em; // Equivalent to 320px; +$mobile-small: 20em; // Equivalent to 320px; diff --git a/app/assets/stylesheets/_project_analyses.scss b/app/assets/stylesheets/_project_analyses.scss new file mode 100644 index 00000000..d7bf7825 --- /dev/null +++ b/app/assets/stylesheets/_project_analyses.scss @@ -0,0 +1,45 @@ +// @import "base_variables"; + +body.project-analysis-show { + h1 { + font-size: 24px; + margin-bottom: 16px; + } + + .scores-table { + width: 100%; + border-collapse: collapse; + margin-bottom: 20px; + + th, + td { + padding: 8px; + border: 1px solid #ddd; + } + + th { + background-color: #f0f0f0; + text-align: left; + font-weight: bold; + } + + // Define classes for score rows using the color variables + tr.score-row--high { + background-color: $green-background; + color: $green-text; + font-weight: 900; + } + + tr.score-row--medium { + background-color: $yellow-background; + color: $yellow-text; + } + + tr.score-row--low { + background-color: $red-background; + color: $red-text; + } + } + + // Additional styles can be defined here, following your SCSS strategies +} diff --git a/app/assets/stylesheets/_projects-index.scss b/app/assets/stylesheets/_projects-index.scss index 6d5ca16f..7c3df572 100644 --- a/app/assets/stylesheets/_projects-index.scss +++ b/app/assets/stylesheets/_projects-index.scss @@ -1,15 +1,13 @@ body.projects-index { - section.container { @extend .clearfix; } - + @media all and (max-width: $maximum-width) { section.container { margin-top: 0.5em !important; } } - } .application__image-wrapper { @@ -32,7 +30,7 @@ body.projects-index { } .admin-panel { - border-right: 1px solid rgb(200,200,200); + border-right: 1px solid rgb(200, 200, 200); float: left; margin-right: 39px; padding: 0px 20px 20px 0px; @@ -76,13 +74,13 @@ body.projects-index { } h2 { - border-bottom: 1px dotted rgb(200,200,200); + border-bottom: 1px dotted rgb(200, 200, 200); font: 300 18px/20px $sans-serif; margin: 15px 0px; padding-bottom: 30px; text-align: center; @media all and (max-width: $maximum-width) { - border-bottom: 1px solid rgb(200,200,200); + border-bottom: 1px solid rgb(200, 200, 200); margin-bottom: 0px; text-align: left; padding-left: 70px; @@ -94,7 +92,7 @@ body.projects-index { margin: 0px; li { - border-bottom: 1px dotted rgb(200,200,200); + border-bottom: 1px dotted rgb(200, 200, 200); list-style: none; margin-bottom: 15px; padding: 0px 0px 15px 0px; @@ -121,7 +119,7 @@ body.projects-index { } a { - color: rgb(55,55,55); + color: rgb(55, 55, 55); display: block; font: normal 12px/14px $sans-serif; padding: 5px 0px 5px 10px; @@ -132,7 +130,7 @@ body.projects-index { &:hover { background: $pink; - color: rgb(255,255,255); + color: rgb(255, 255, 255); } } } @@ -142,7 +140,7 @@ body.projects-index { .applications { float: left; max-width: grid-width(9); - + /* Ensure 'Back to Chapter' link on admin view of project page is the correct font size */ > p:nth-child(2) { font-size: 0.8em; @@ -169,11 +167,12 @@ body.projects-index { } .application-filters { - background: rgb(230,230,230); - border: 1px solid rgb(200,200,200); + background: rgb(230, 230, 230); + border: 1px solid rgb(200, 200, 200); border-radius: 4px; - box-shadow: inset 0 -1px 0 0 rgba(255,255,255, 0.3), inset 0 1px 0 0 rgba(255,255,255, 0.8); - @include linear-gradient(rgb(240,240,240), rgb(220,220,220)); + box-shadow: inset 0 -1px 0 0 rgba(255, 255, 255, 0.3), + inset 0 1px 0 0 rgba(255, 255, 255, 0.8); + @include linear-gradient(rgb(240, 240, 240), rgb(220, 220, 220)); margin-bottom: 10px; position: relative; width: grid-width(9); @@ -187,8 +186,8 @@ body.projects-index { position: relative; a.chapter-selection { - background: rgb(230,230,230); - border: 1px solid rgb(190,190,190); + background: rgb(230, 230, 230); + border: 1px solid rgb(190, 190, 190); border-radius: 3px; display: block; padding: 5px 40px 5px 10px; @@ -196,18 +195,18 @@ body.projects-index { z-index: 2; &:hover { - background: rgb(220,220,220); + background: rgb(220, 220, 220); } span { - color: rgb(55,55,55); + color: rgb(55, 55, 55); font: normal 16px/18px $sans-serif; - text-shadow: 0 1px rgba(255,255,255, 0.75); + text-shadow: 0 1px rgba(255, 255, 255, 0.75); } span.arrow { border: { - top: 7px solid rgb(150,150,150); + top: 7px solid rgb(150, 150, 150); right: 7px solid transparent; bottom: 7px solid transparent; left: 7px solid transparent; @@ -218,18 +217,18 @@ body.projects-index { } &.expanded { - background: rgb(220,220,220); - border: 1px solid rgb(150,150,150); + background: rgb(220, 220, 220); + border: 1px solid rgb(150, 150, 150); border-radius: 3px 3px 0px 0px; cursor: pointer; } } ol.chapter-selector { - background: rgb(255,255,255); - border: 1px solid rgb(150,150,150); + background: rgb(255, 255, 255); + border: 1px solid rgb(150, 150, 150); @include border-bottom-radius(4px); - box-shadow: 0 3px 5px 0 rgba(0,0,0, 0.2); + box-shadow: 0 3px 5px 0 rgba(0, 0, 0, 0.2); opacity: 0; overflow-y: auto; position: relative; @@ -237,13 +236,13 @@ body.projects-index { z-index: 1; margin-top: -1px; margin-bottom: 0px; - + &.bounded { - max-height: 428px; + max-height: 428px; } li { - border-bottom: 1px solid rgb(200,200,200); + border-bottom: 1px solid rgb(200, 200, 200); list-style: none; margin: 0 5px; padding: 3px 0px; @@ -253,14 +252,14 @@ body.projects-index { } a { - color: rgb(55,55,55); + color: rgb(55, 55, 55); display: block; padding: 5px 8px; text-decoration: none; &:hover { background: $pink; - color: rgb(255,255,255); + color: rgb(255, 255, 255); } } } @@ -276,7 +275,9 @@ body.projects-index { float: right; padding: 0px 10px 10px 10px; - div.short-list-toggle, div.funded-toggle, div.toggle { + div.short-list-toggle, + div.funded-toggle, + div.toggle { float: left; margin-right: 10px; margin-top: 20px; @@ -289,28 +290,29 @@ body.projects-index { } label { - background: rgb(230,230,230); - border: 1px solid rgb(190,190,190); + background: rgb(230, 230, 230); + border: 1px solid rgb(190, 190, 190); border-radius: 3px; font: normal 12px $sans-serif; height: 21px; margin: 0px; - text-shadow: 0 1px rgba(255,255,255, 0.8); + text-shadow: 0 1px rgba(255, 255, 255, 0.8); padding: 8px 12px 0px 30px; &:hover { - background: rgb(220,220,220); + background: rgb(220, 220, 220); cursor: pointer; } } input[type="checkbox"]:checked + label { - background: rgb(220,220,220); + background: rgb(220, 220, 220); cursor: pointer; } } - div.date-range, div.search { + div.date-range, + div.search { float: left; margin-right: 10px; margin-top: 20px; @@ -320,12 +322,12 @@ body.projects-index { } input[type="text"] { - color: rgb(180,180,180); + color: rgb(180, 180, 180); height: 30px; width: 90px; &:focus { - color: rgb(55,55,55); + color: rgb(55, 55, 55); } } @@ -334,8 +336,9 @@ body.projects-index { } } - input[type="submit"], button[type="submit"] { - @include button(rgb(230,230,230)); + input[type="submit"], + button[type="submit"] { + @include button(rgb(230, 230, 230)); margin: 0px; margin-top: 20px; width: auto; @@ -351,10 +354,10 @@ body.projects-index { } article.project { - border: 1px solid rgb(200,200,200); - background: rgb(255,255,255); + border: 1px solid rgb(200, 200, 200); + background: rgb(255, 255, 255); border-radius: 4px; - box-shadow: 0 1px 6px 0 rgba(0,0,0, 0.15); + box-shadow: 0 1px 6px 0 rgba(0, 0, 0, 0.15); margin-top: 20px; margin-bottom: 20px; @@ -363,7 +366,7 @@ body.projects-index { div.title { @extend .clearfix; - border-bottom: 1px solid rgb(200,200,200); + border-bottom: 1px solid rgb(200, 200, 200); padding: 10px; a.title { @@ -379,7 +382,7 @@ body.projects-index { .public-link { display: none; - padding-top: .5em; + padding-top: 0.5em; font-size: 0.8em; clear: both; @@ -400,7 +403,7 @@ body.projects-index { a.short-list, a.mark-as-winner, a.project-actions-toggle { - @include button(rgb(240,240,240)); + @include button(rgb(240, 240, 240)); float: right; margin-left: 10px; text-decoration: none; @@ -410,13 +413,28 @@ body.projects-index { } &.mark-as-winner:hover { - @include button(rgb(34,122,255)); + @include button(rgb(34, 122, 255)); } } a.project-actions-toggle { font-size: 1.1em; } + + a.auto-awesome { + @include button(rgb(240, 240, 240)); + float: right; + margin-left: 10px; + text-decoration: none; + + &.auto-awesome:hover { + @include button($pink); + } + + // &.mark-as-winner:hover { + // @include button(rgb(34,122,255)); + // } + } } .contact { @@ -429,7 +447,7 @@ body.projects-index { } .meta-data { - border-bottom: 1px solid rgb(200,200,200); + border-bottom: 1px solid rgb(200, 200, 200); padding: 10px; font-size: 0.8em; @@ -439,7 +457,7 @@ body.projects-index { } .project-actions-toggle { - margin-left: .25em; + margin-left: 0.25em; font-size: 1.2em; float: right; } @@ -474,7 +492,7 @@ body.projects-index { margin-bottom: 0; li { - padding-bottom: .5em; + padding-bottom: 0.5em; a { text-decoration: none; @@ -509,7 +527,8 @@ body.projects-index { font: normal 16px/20px $serif; padding: 10px; - h3, p { + h3, + p { margin: 0; padding: 0; font: normal 14px/20px $serif; @@ -544,7 +563,7 @@ body.projects-index { .see-more { background: rgba(245, 245, 245, 1); - border-top: 1px solid rgb(200,200,200); + border-top: 1px solid rgb(200, 200, 200); @include border-bottom-radius(4px); display: block; text-decoration: none; @@ -567,11 +586,12 @@ body.projects-index { } .filtering { - border-top: 1px solid rgb(200,200,200); + border-top: 1px solid rgb(200, 200, 200); padding: 10px; font-size: 0.8em; - .hiding-form, .unhiding-form { + .hiding-form, + .unhiding-form { p { margin: 0; } @@ -592,7 +612,7 @@ body.projects-index { } input.hide-action { - @include button(rgb(230,230,230)); + @include button(rgb(230, 230, 230)); width: 80px; position: relative; margin: 0; diff --git a/app/assets/stylesheets/application.css.scss b/app/assets/stylesheets/application.css.scss index 042988d8..887fcc64 100644 --- a/app/assets/stylesheets/application.css.scss +++ b/app/assets/stylesheets/application.css.scss @@ -5,37 +5,38 @@ *= require_self */ -@import 'flutie'; -@import 'bourbon'; -@import 'flexbox-ultralight'; -@import 'lightgallery'; +@import "flutie"; +@import "bourbon"; +@import "flexbox-ultralight"; +@import "lightgallery"; -@import 'base-variables'; -@import 'base-extends'; -@import 'base-mixins'; -@import 'base-animations'; -@import 'base'; -@import 'buttons'; -@import 'navigation'; -@import 'shared-feed'; -@import 'shared-forms'; -@import 'chapters'; -@import 'projects'; -@import 'comments'; -@import 'home'; -@import 'invitations-new'; -@import 'users'; -@import 'finalists'; -@import 'shared-header'; -@import 'shared-footer'; -@import 'shared-date-picker'; -@import 'shared-flash-messages'; -@import 'about'; -@import 'faq'; -@import 'winners'; -@import 'funded-projects'; -@import 'pagination'; -@import 'magnific-popup'; +@import "base-variables"; +@import "base-extends"; +@import "base-mixins"; +@import "base-animations"; +@import "base"; +@import "buttons"; +@import "navigation"; +@import "shared-feed"; +@import "shared-forms"; +@import "chapters"; +@import "projects"; +@import "project_analyses"; +@import "comments"; +@import "home"; +@import "invitations-new"; +@import "users"; +@import "finalists"; +@import "shared-header"; +@import "shared-footer"; +@import "shared-date-picker"; +@import "shared-flash-messages"; +@import "about"; +@import "faq"; +@import "winners"; +@import "funded-projects"; +@import "pagination"; +@import "magnific-popup"; -@import 'owl.carousel.min'; -@import 'owl.theme.default.min'; +@import "owl.carousel.min"; +@import "owl.theme.default.min"; diff --git a/app/controllers/project_analyses_controller.rb b/app/controllers/project_analyses_controller.rb new file mode 100644 index 00000000..fed46dc7 --- /dev/null +++ b/app/controllers/project_analyses_controller.rb @@ -0,0 +1,27 @@ +class ProjectAnalysesController < ApplicationController + before_action :require_login + before_action :set_project + + def show_or_create + # Delete existing analysis if force is true + if params[:force].to_s == "true" && @project.project_analysis.present? + @project.project_analysis.destroy + end + + # Use cached analysis if it exists, otherwise generate a new one + @project_analysis = @project.project_analysis || ProjectAnalysisGenerator.new(@project.id).call + + flash[:notice] = if @project_analysis.persisted? + "Project analysis loaded from cache." + else + "New project analysis generated." + end + render :show + end + + private + + def set_project + @project = Project.find(params[:project_id]) + end +end diff --git a/app/views/project_analyses/show.html.erb b/app/views/project_analyses/show.html.erb new file mode 100644 index 00000000..8dfc46f5 --- /dev/null +++ b/app/views/project_analyses/show.html.erb @@ -0,0 +1,51 @@ +<% provide(:body_class, 'project-analysis-show') %> + +

Project Analysis for "<%= @project.title %>"

+ + +

Summary: <%= @project_analysis.summary %>

+

Applicant Role: <%= @project_analysis.applicant_role.humanize %>

+

Language: <%= @project_analysis.language_code %>

+

Tags: <%= @project_analysis.tags.join(', ') %>

+ + + + + + + + + + + <% # Collect and sort the scores from highest to lowest %> + <% scores = ProjectAnalysisScores::SCORE_FIELDS.map do |field| + score_obj = @project_analysis.scores.send(field) + { + field: field.humanize, + score: score_obj.score.to_f * 100, + reason: score_obj.reason + } + end + # end.sort_by { |s| -s[:score] } %> + + <% # Loop through the sorted scores and apply color coding %> + <% scores.each do |score_hash| %> + <% # Determine the color classes based on the score %> + <% if score_hash[:score] >= 80 %> + <% row_style = 'background-color: #d1fae5; color: #022c22;' %> + <% elsif score_hash[:score] >= 50 %> + <% row_style = 'background-color: #fef9c3; color: #422006;' %> + <% else %> + <% row_style = 'background-color: #ffe4e6; color: #4c0519;' %> + <% end %> + + + + + + <% end %> + +
CriterionScoreReason
<%= score_hash[:field] %><%= (score_hash[:score]).round %><%= score_hash[:reason] %>
+ +

Suggestions: <%= @project_analysis.suggestion %>

+ \ No newline at end of file diff --git a/app/views/projects/_project_details.html.erb b/app/views/projects/_project_details.html.erb index cd31b902..0490553e 100644 --- a/app/views/projects/_project_details.html.erb +++ b/app/views/projects/_project_details.html.erb @@ -12,6 +12,7 @@ <% if display_project_actions?(current_user, project) %> <%= link_to ''.html_safe, "#", class: "project-actions-toggle", data: { action: "click->toggler#toggle" } %> + <%= link_to I18n.t("auto_awesome", scope: "projects.project.buttons"), show_or_create_project_project_analysis_path(project), class: "auto-awesome" %> <% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 10963a5c..3a7d10ed 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -168,6 +168,8 @@ en: add-comment: Add a comment confirm-delete-comment: Are you sure you want to remove this comment? delete-comment: Delete comment + buttons: + auto_awesome: Auto-awesome hide_form: hide: Hide unhide: Unhide diff --git a/config/routes.rb b/config/routes.rb index 1a5ae987..31d411b2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,53 +1,53 @@ Rails.application.routes.draw do if ENV["AWS_BUCKET"].present? - mount Shrine.uppy_s3_multipart(:cache) => '/s3/multipart' + mount Shrine.uppy_s3_multipart(:cache) => "/s3/multipart" else - mount Tus::Server => '/uploads' + mount Tus::Server => "/uploads" end constraints(SubdomainConstraint) do get "/apply" => "subdomains#apply" - get "*url" => "subdomains#chapter" - get "/" => "subdomains#chapter" + get "*url" => "subdomains#chapter" + get "/" => "subdomains#chapter" end constraints(DomainConstraint) do get "*url" => "subdomains#canonical" - get "/" => "subdomains#canonical" + get "/" => "subdomains#canonical" end get "/blog/contact/" => redirect("/en/contact") - get "/blog/about/" => redirect("/en/about_us") - get "/blog" => redirect("http://blog.awesomefoundation.org") - get "/blog/*path" => redirect { |params, request| "http://blog.awesomefoundation.org/#{Addressable::URI.escape(params[:path])}" }, :format => false - get "/apply" => redirect("/en/submissions/new") + get "/blog/about/" => redirect("/en/about_us") + get "/blog" => redirect("http://blog.awesomefoundation.org") + get "/blog/*path" => redirect { |params, request| "http://blog.awesomefoundation.org/#{Addressable::URI.escape(params[:path])}" }, :format => false + get "/apply" => redirect("/en/submissions/new") - resources :passwords, :controller => 'clearance/passwords', :only => [:new, :create] + resources :passwords, controller: "clearance/passwords", only: [:new, :create] - resources :users, :shallow => true do - resource :password, :controller => 'clearance/passwords', :only => [:create, :edit, :update] + resources :users, shallow: true do + resource :password, controller: "clearance/passwords", only: [:create, :edit, :update] end scope "(:locale)", locale: /#{I18n.available_locales.join("|")}/ do - resource :session, controller: :sessions, only: [:new, :create, :destroy] + resource :session, controller: :sessions, only: [:new, :create, :destroy] - get "sign_in", :to => "sessions#new" - delete "sign_out", :to => "sessions#destroy" + get "sign_in", to: "sessions#new" + delete "sign_out", to: "sessions#destroy" - resources :users, :only => [:index, :update, :edit] do - resource :admins, :only => [:create, :destroy] + resources :users, only: [:index, :update, :edit] do + resource :admins, only: [:create, :destroy] end - resources :chapters, :only => [:index, :show, :new, :create, :edit, :update] do - resources :finalists, :only => [:index] - resources :projects, :only => [:index, :show] do - resource :winner, :only => [:edit] + resources :chapters, only: [:index, :show, :new, :create, :edit, :update] do + resources :finalists, only: [:index] + resources :projects, only: [:index, :show] do + resource :winner, only: [:edit] end - resources :users, :only => [:index] + resources :users, only: [:index] end - resources :invitations, :only => [:new, :create] do - resources :acceptances, :only => [:new, :create] + resources :invitations, only: [:new, :create] do + resources :acceptances, only: [:new, :create] end resources :funded_projects, path: "projects", only: [:index, :show] @@ -58,8 +58,12 @@ put "unhide" end resources :comments - resource :winner, :only => [:create, :update, :destroy] - resource :vote, :only => [:create, :destroy] + resource :winner, only: [:create, :update, :destroy] + resource :vote, only: [:create, :destroy] + + resource :project_analysis, only: [] do + get "show_or_create", to: "project_analyses#show_or_create" + end end resources :submissions, controller: "projects" do @@ -68,21 +72,21 @@ end end - resources :roles, :only => [:destroy] do - resource :promotions, :only => [:create, :destroy] + resources :roles, only: [:destroy] do + resource :promotions, only: [:create, :destroy] end - %w(about_us faq start_a_chapter).each do |page| - get page, :to => 'high_voltage/pages#show', :id => page + %w[about_us faq start_a_chapter].each do |page| + get page, to: "high_voltage/pages#show", id: page end - root :to => 'home#index' + root to: "home#index" end - get "/404", :to => "errors#not_found" - get "/500", :to => "errors#internal_server_error" + get "/404", to: "errors#not_found" + get "/500", to: "errors#internal_server_error" # All other routes are considered 404s. ActionController::RoutingError # will catch them, but that fills our logs with noisy exceptions. - get '*url', :to => 'errors#not_found' + get "*url", to: "errors#not_found" end diff --git a/spec/factories.rb b/spec/factories.rb index 4b600208..365e8c31 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -1,7 +1,6 @@ -# coding: utf-8 FactoryGirl.define do - sequence(:email) {|n| "user#{n}@example.com" } - sequence(:title) {|n| "Something Awesome ##{n}" } + sequence(:email) { |n| "user#{n}@example.com" } + sequence(:title) { |n| "Something Awesome ##{n}" } sequence(:index) factory :chapter do @@ -14,7 +13,7 @@ end end - factory :user, :aliases => [:inviter, :invitee] do + factory :user, aliases: [:inviter, :invitee] do first_name "Joe" last_name "Schmoe" email @@ -27,13 +26,13 @@ factory :user_with_dean_role do after(:create) do |user| - FactoryGirl.create(:role, :user => user, :name => "dean") + FactoryGirl.create(:role, user: user, name: "dean") user.reload end end factory :user_with_trustee_role do - chapters { [association(:chapter) ] } + chapters { [association(:chapter)] } end end @@ -48,7 +47,7 @@ factory :invitation do email - association :inviter, :factory => :user_with_dean_role + association :inviter, factory: :user_with_dean_role after(:build) do |invitation, proxy| invitation.chapter = invitation.inviter.chapters.first @@ -66,11 +65,11 @@ chapter factory :project_with_rss_feed do - rss_feed_url Rails.root.join('spec', 'support', 'feed.xml').to_s + rss_feed_url Rails.root.join("spec", "support", "feed.xml").to_s end factory :winning_project do - sequence(:funded_on) { |n| (3000-n.to_i).days.ago } + sequence(:funded_on) { |n| (3000 - n.to_i).days.ago } end factory :hidden_project do @@ -107,11 +106,10 @@ # custom storage the way we do. factory :s3_photo, class: "Photo" do project - storage_keys { { store: :s3_store, cache: :s3_cache } } + storage_keys { {store: :s3_store, cache: :s3_cache} } image_data { FakeData.shrine_uploaded_file("1.JPG") } end - factory :comment do project user diff --git a/spec/jobs/project_analysis_job_spec.rb b/spec/jobs/project_analysis_job_spec.rb new file mode 100644 index 00000000..5fcd287f --- /dev/null +++ b/spec/jobs/project_analysis_job_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe ProjectAnalysisJob, type: :job do + pending "add some examples to (or delete) #{__FILE__}" +end From d5e76d535afd8b4b9348660a9e1de5f39ac20b1b Mon Sep 17 00:00:00 2001 From: Cedric Hurst Date: Fri, 1 Nov 2024 14:38:42 -0500 Subject: [PATCH 11/20] cull gpt prompt criteria scores reduce the number of scores for fewer input tokens and less redundancy --- config/openai.yml | 76 +++++++++++++++++++---------------------------- 1 file changed, 30 insertions(+), 46 deletions(-) diff --git a/config/openai.yml b/config/openai.yml index 1136efba..717d1053 100644 --- a/config/openai.yml +++ b/config/openai.yml @@ -141,77 +141,61 @@ default_settings: &default properties: reach: "$ref": "#/$defs/Score" - description: Number of people impacted, focusing on those local to the chapter's region. - proximity: + description: | + Number of people impacted, focusing on those local to the chapter's region. + alignment: "$ref": "#/$defs/Score" - description: Geographic closeness of project and applicant to chapter community. + description: | + Project alignment with the Awesome Foundation’s mission and geographical focus. novelty: - "$ref": "#/$defs/Score" - description: Originality and uniqueness of the project’s concept or approach. - public_beneficiary: "$ref": "#/$defs/Score" description: | - Prioritizes funds that benefit the public or community over the applicant’s personal gain. - Score low if funds are mainly used for applicant’s personal expenses (e.g., stipends). - budget_clarity: + Originality, cleverness, and uniqueness of the project’s concept or approach. Should + not be an obvious idea that replicates other existing work. + use_of_proceeds: "$ref": "#/$defs/Score" - description: Clarity and detail of fund allocation within the proposal. + description: | + Clarity and detail of fund allocation within the proposal, as well as ensuring the + funds will NOT finacially enrich the applicant directly (e.g., stipends paid to the + applicant). impact_of_grant: "$ref": "#/$defs/Score" - description: Significance of the $1000 grant in achieving the project’s goals. + description: | + Significance of the $1000 grant in achieving the project’s goals. The grant should + be executable solely with no additional outside funding. feasibility: "$ref": "#/$defs/Score" - description: Practicality of executing the project within the stated timeline and budget. - credibility: - "$ref": "#/$defs/Score" - description: Applicant’s experience and credibility relevant to the project’s scope. - innovation: - "$ref": "#/$defs/Score" - description: Originality of the idea and potential to introduce new solutions. + description: | + Practicality of executing the project within the stated timeline and budget. Also + evaluate applicant’s experience and credibility relevant to the project’s scope. sustainability: "$ref": "#/$defs/Score" - description: Long-term viability and potential for impact beyond the grant period. - community_involvement: - "$ref": "#/$defs/Score" - description: Degree of community engagement and opportunities for collaboration. - cultural_relevance: - "$ref": "#/$defs/Score" - description: Alignment with local cultural values and potential to raise awareness. - scalability: - "$ref": "#/$defs/Score" - description: Potential for project expansion or replication in other contexts. + description: | + Long-term viability and potential for impact beyond the grant period, + as well as potential for project expansion or replication in other contexts. urgency: "$ref": "#/$defs/Score" - description: Immediacy of need for funds; score lower if impact isn’t time-sensitive. - alignment_with_mission: - "$ref": "#/$defs/Score" - description: Project alignment with the Awesome Foundation’s mission or chapter focus. + description: | + Immediacy of need for funds; score lower if impact isn’t time-sensitive. whimsy: "$ref": "#/$defs/Score" - description: Playful, imaginative qualities that inspire curiosity or delight. + description: | + Playful, imaginative qualities that inspire curiosity or delight. social_good: "$ref": "#/$defs/Score" - description: Potential for positive social impact and contribution to community welfare. - artistic_merit: - "$ref": "#/$defs/Score" - description: Artistic quality or creativity, especially for arts-focused projects. + description: | + Potential for positive social impact and contribution to community welfare. required: - reach - - proximity - - public_beneficiary - - budget_clarity + - alignment + - novelty + - use_of_proceeds - impact_of_grant - feasibility - - innovation - sustainability - - community_involvement - - cultural_relevance - - scalability - urgency - - alignment_with_mission - whimsy - social_good - - artistic_merit title: Scores type: object properties: From 432f4666f3b37d53962ff8ff1d7c8d492856fd82 Mon Sep 17 00:00:00 2001 From: Jesse Chan-Norris Date: Tue, 3 Feb 2026 16:12:23 -0500 Subject: [PATCH 12/20] First pass at fixing specs & bad master merge --- .tool-versions | 2 +- Gemfile.lock | 66 +++++++++---------- app/models/project.rb | 1 + spec/factories.rb | 5 +- spec/jobs/project_analysis_job_spec.rb | 2 +- ...ect_analysis_generator_integration_spec.rb | 2 +- .../project_analysis_generator_spec.rb | 2 +- 7 files changed, 39 insertions(+), 41 deletions(-) diff --git a/.tool-versions b/.tool-versions index 33a8789f..5aa8e0c3 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1 @@ -ruby 2.7.7 +ruby 3.3.6 diff --git a/Gemfile.lock b/Gemfile.lock index 47b1f363..eb58781b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -81,7 +81,7 @@ GEM argon2 (2.3.2) ffi (~> 1.15) ffi-compiler (~> 1.0) - ast (2.4.2) + ast (2.4.3) aws-eventstream (1.3.2) aws-partitions (1.1083.0) aws-sdk-core (3.222.1) @@ -198,7 +198,7 @@ GEM faraday-multipart (1.0.4) multipart-post (~> 2) faraday-net_http (3.0.2) - ffi (1.17.1) + ffi (1.17.1-x86_64-darwin) ffi-compiler (1.3.2) ffi (>= 1.15.5) rake @@ -245,12 +245,13 @@ GEM jquery-ui-rails (3.0.1) jquery-rails railties (>= 3.1.0) - json (2.7.4) - language_server-protocol (3.17.0.3) + json (2.18.1) + language_server-protocol (3.17.0.5) launchy (2.5.2) addressable (~> 2.8) letter_opener (1.8.1) launchy (>= 2.2, < 3) + lint_roller (1.1.0) listen (3.9.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) @@ -272,7 +273,6 @@ GEM metaclass (0.0.4) method_source (1.1.0) mini_mime (1.1.5) - mini_portile2 (2.8.9) minitest (5.25.5) mocha (1.2.1) metaclass (~> 0.0.1) @@ -289,22 +289,17 @@ GEM net-protocol newrelic_rpm (9.22.0) nio4r (2.7.5) - nokogiri (1.18.9) - mini_portile2 (~> 2.8.2) + nokogiri (1.18.9-x86_64-darwin) racc (~> 1.4) - parallel (1.26.3) - parser (3.3.5.0) + parallel (1.27.0) + parser (3.3.10.1) ast (~> 2.4.1) racc pg (1.4.6) pp (0.6.2) prettyprint prettyprint (0.2.0) - protobuf-cucumber (3.10.8) - activesupport (>= 3.2) - middleware - thor - thread_safe + prism (1.9.0) pry (0.14.2) coderay (~> 1.1) method_source (~> 1.0) @@ -412,28 +407,30 @@ GEM rspec-mocks (~> 3.13) rspec-support (~> 3.13) rspec-support (3.13.2) - rubocop (1.64.1) + rubocop (1.82.1) json (~> 2.3) - language_server-protocol (>= 3.17.0) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) parallel (~> 1.10) parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 1.8, < 3.0) - rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.31.1, < 2.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.48.0, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.32.3) - parser (>= 3.3.1.0) - rubocop-performance (1.21.1) - rubocop (>= 1.48.1, < 2.0) - rubocop-ast (>= 1.31.1, < 2.0) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.49.0) + parser (>= 3.3.7.2) + prism (~> 1.7) + rubocop-performance (1.26.1) + lint_roller (~> 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.47.1, < 2.0) + ruby-next-core (1.1.2) ruby-openai (7.3.1) event_stream_parser (>= 0.3.0, < 2.0.0) faraday (>= 1) faraday-multipart (>= 1) ruby-progressbar (1.13.0) - ruby-next-core (1.1.2) ruby2_keywords (0.0.5) rubyzip (2.4.1) sass (3.7.4) @@ -484,18 +481,18 @@ GEM sprockets-redirect (0.1.0) activesupport (>= 3.1.0) rack - standard (1.37.0) + standard (1.53.0) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.0) - rubocop (~> 1.64.0) + rubocop (~> 1.82.0) standard-custom (~> 1.0.0) - standard-performance (~> 1.4) + standard-performance (~> 1.8) standard-custom (1.0.2) lint_roller (~> 1.0) rubocop (~> 1.50) - standard-performance (1.4.0) + standard-performance (1.9.0) lint_roller (~> 1.1) - rubocop-performance (~> 1.21.0) + rubocop-performance (~> 1.26.0) stimulus-rails (1.3.4) railties (>= 6.0.0) stringio (3.1.6) @@ -521,10 +518,9 @@ GEM roda (>= 2.27, < 4) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - unf (0.1.4) - unf_ext - unf_ext (0.0.8.2) - unicode-display_width (2.6.0) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.2.0) uppy-s3_multipart (1.2.1) aws-sdk-s3 (~> 1.0) content_disposition (~> 1.0) diff --git a/app/models/project.rb b/app/models/project.rb index 76246612..a04287de 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -14,6 +14,7 @@ class Project < ApplicationRecord has_many :photos, -> { merge(Photo.sorted) } has_many :real_photos, -> { merge(Photo.image_files.sorted) }, class_name: "Photo" has_one :primary_photo, -> { merge(Photo.image_files.sorted) }, class_name: "Photo" + has_one :project_moderation, dependent: :destroy has_one :project_analysis, dependent: :destroy before_validation UrlNormalizer.new(:url, :rss_feed_url) diff --git a/spec/factories.rb b/spec/factories.rb index 8e9dc1d2..0d745e09 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -1,4 +1,5 @@ -FactoryGirl.define do +# coding: utf-8 +FactoryBot.define do sequence(:email) { |n| "user#{n}@example.com" } sequence(:title) { |n| "Something Awesome ##{n}" } sequence(:index) @@ -26,7 +27,7 @@ factory :user_with_dean_role do after(:create) do |user| - FactoryGirl.create(:role, user: user, name: "dean") + FactoryBot.create(:role, user: user, name: "dean") user.reload end end diff --git a/spec/jobs/project_analysis_job_spec.rb b/spec/jobs/project_analysis_job_spec.rb index 5fcd287f..5e6d9ce9 100644 --- a/spec/jobs/project_analysis_job_spec.rb +++ b/spec/jobs/project_analysis_job_spec.rb @@ -1,4 +1,4 @@ -require 'rails_helper' +require 'spec_helper' RSpec.describe ProjectAnalysisJob, type: :job do pending "add some examples to (or delete) #{__FILE__}" diff --git a/spec/services/project_analysis_generator_integration_spec.rb b/spec/services/project_analysis_generator_integration_spec.rb index 91127baa..fad2620d 100644 --- a/spec/services/project_analysis_generator_integration_spec.rb +++ b/spec/services/project_analysis_generator_integration_spec.rb @@ -1,4 +1,4 @@ -require "rails_helper" +require "spec_helper" RSpec.describe "ProjectAnalysisGenerator Integration", type: :integration do let(:project) { create(:project) } diff --git a/spec/services/project_analysis_generator_spec.rb b/spec/services/project_analysis_generator_spec.rb index 94f5691f..bf3d8891 100644 --- a/spec/services/project_analysis_generator_spec.rb +++ b/spec/services/project_analysis_generator_spec.rb @@ -1,4 +1,4 @@ -require "rails_helper" +require "spec_helper" RSpec.describe ProjectAnalysisGenerator, type: :service do let(:project) { create(:project) } From 766d5b1b64f2189bcac122f3eb85698590199096 Mon Sep 17 00:00:00 2001 From: Jesse Chan-Norris Date: Tue, 3 Feb 2026 16:58:01 -0500 Subject: [PATCH 13/20] Fix tests --- app/services/project_analysis_generator.rb | 3 +- .../project_analysis_generator_spec.rb | 36 ++++++++++++------- spec/spec_helper.rb | 1 + 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/app/services/project_analysis_generator.rb b/app/services/project_analysis_generator.rb index eb265a3d..29f2332a 100644 --- a/app/services/project_analysis_generator.rb +++ b/app/services/project_analysis_generator.rb @@ -72,7 +72,8 @@ def make_api_call(project_data) function_call: "auto" } response = @client.chat(parameters: parameters) - puts(JSON.generate({response: response}, indent: " ")) + # Avoid noisy output in tests/production. Use logger if needed. + # puts(JSON.generate({response: response}, indent: " ")) # response.merge( # "prompt_usage_data" => { "total_tokens" => 800, "prompt_tokens" => 500, "completion_tokens" => 300 }, # "prompt_estimated_cost" => 0.02 diff --git a/spec/services/project_analysis_generator_spec.rb b/spec/services/project_analysis_generator_spec.rb index bf3d8891..191eda3e 100644 --- a/spec/services/project_analysis_generator_spec.rb +++ b/spec/services/project_analysis_generator_spec.rb @@ -2,16 +2,32 @@ RSpec.describe ProjectAnalysisGenerator, type: :service do let(:project) { create(:project) } + let(:openai_config) do + { + api_key: "test-api-key", + project_analysis: { + model: "gpt-test", + prompt_text: "Prompt", + format_instructions: "Format", + response_schema: {} + } + } + end + + let(:mock_client) { mock("OpenAI::Client") } before do - # Mock the OpenAI API client - @mock_client = instance_double(OpenAI::Client) - allow(OpenAI::Client).to receive(:new).and_return(@mock_client) + # Mock the OpenAI API client (Mocha) + OpenAI::Client.stubs(:new).returns(mock_client) + Rails.application.stubs(:config_for).with(:openai).returns(openai_config) end describe "#call" do context "when OpenAI API returns a valid response" do it "creates a ProjectAnalysis record" do + scores = ProjectAnalysisScores::SCORE_FIELDS.each_with_object({}) do |field, hash| + hash[field] = {"score" => 0.8, "reason" => "#{field} ok"} + end # Mock the API response api_response = { "choices" => [ @@ -21,17 +37,13 @@ "summary" => "A great project.", "language_code" => "en", "applicant_role" => "Founder", - "scores" => { - "impact" => {"score" => 0.9, "reason" => "High impact"}, - "feasibility" => {"score" => 0.8, "reason" => "Feasible"}, - "innovation" => {"score" => 0.7, "reason" => "Innovative"} - } + "scores" => scores }.to_json } } ] } - allow(@mock_client).to receive(:chat).and_return(api_response) + mock_client.stubs(:chat).returns(api_response) generator = ProjectAnalysisGenerator.new(project.id) expect { @@ -41,14 +53,14 @@ analysis = ProjectAnalysis.last expect(analysis.summary).to eq("A great project.") expect(analysis.language_code).to eq("en") - expect(analysis.scores.impact_score).to eq(0.9) - expect(analysis.scores.impact_reason).to eq("High impact") + expect(analysis.scores.feasibility.score).to eq(0.8) + expect(analysis.scores.feasibility.reason).to eq("feasibility ok") end end context "when OpenAI API fails" do it "raises an error" do - allow(@mock_client).to receive(:chat).and_raise(StandardError.new("API Error")) + mock_client.stubs(:chat).raises(StandardError.new("API Error")) generator = ProjectAnalysisGenerator.new(project.id) expect { generator.call }.to raise_error("Error: OpenAI API call failed.") diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 423b08c3..88dcec9c 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -19,6 +19,7 @@ config.infer_spec_type_from_file_location! config.mock_with :mocha + config.include FactoryBot::Syntax::Methods config.before(:each){ DatabaseCleaner.start } config.append_after(:each){ DatabaseCleaner.clean } config.before(:each){ FactoryBot.create(:chapter, :name => "Any") } From 400a69e646f5ac72f94c0cbd3b00d130d873af91 Mon Sep 17 00:00:00 2001 From: Jesse Chan-Norris Date: Tue, 3 Feb 2026 16:58:24 -0500 Subject: [PATCH 14/20] I18n project analysis views --- .../project_analyses_controller.rb | 4 ++-- app/views/project_analyses/show.html.erb | 20 +++++++++---------- config/locales/en.yml | 14 +++++++++++++ 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/app/controllers/project_analyses_controller.rb b/app/controllers/project_analyses_controller.rb index fed46dc7..2ada5b14 100644 --- a/app/controllers/project_analyses_controller.rb +++ b/app/controllers/project_analyses_controller.rb @@ -12,9 +12,9 @@ def show_or_create @project_analysis = @project.project_analysis || ProjectAnalysisGenerator.new(@project.id).call flash[:notice] = if @project_analysis.persisted? - "Project analysis loaded from cache." + I18n.t("project_analyses.flash.cache_loaded") else - "New project analysis generated." + I18n.t("project_analyses.flash.generated") end render :show end diff --git a/app/views/project_analyses/show.html.erb b/app/views/project_analyses/show.html.erb index 8dfc46f5..2ef8b374 100644 --- a/app/views/project_analyses/show.html.erb +++ b/app/views/project_analyses/show.html.erb @@ -1,19 +1,19 @@ <% provide(:body_class, 'project-analysis-show') %> -

Project Analysis for "<%= @project.title %>"

+

<%= t(".title", title: @project.title) %>

-

Summary: <%= @project_analysis.summary %>

-

Applicant Role: <%= @project_analysis.applicant_role.humanize %>

-

Language: <%= @project_analysis.language_code %>

-

Tags: <%= @project_analysis.tags.join(', ') %>

+

<%= t(".summary") %>: <%= @project_analysis.summary %>

+

<%= t(".applicant_role") %>: <%= @project_analysis.applicant_role.humanize %>

+

<%= t(".language") %>: <%= @project_analysis.language_code %>

+

<%= t(".tags") %>: <%= @project_analysis.tags.join(', ') %>

- - - + + + @@ -47,5 +47,5 @@
CriterionScoreReason<%= t(".criteria") %><%= t(".score") %><%= t(".reason") %>
-

Suggestions: <%= @project_analysis.suggestion %>

- \ No newline at end of file +

<%= t(".suggestions") %>: <%= @project_analysis.suggestion %>

+ diff --git a/config/locales/en.yml b/config/locales/en.yml index a2c83055..9647e7f1 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -34,6 +34,20 @@ en: error: There was a problem saving your project flashes: failure_when_not_signed_in: You must be logged in. + project_analyses: + flash: + cache_loaded: Project analysis loaded from cache. + generated: New project analysis generated. + show: + title: Project Analysis for "%{title}" + summary: Summary + applicant_role: Applicant Role + language: Language + tags: Tags + criteria: Criterion + score: Score + reason: Reason + suggestions: Suggestions layouts: application: main-header: The Awesome Foundation From a34833c9920c058b5d6094f4cc219d473e582ac2 Mon Sep 17 00:00:00 2001 From: Jesse Chan-Norris Date: Tue, 3 Feb 2026 17:58:33 -0500 Subject: [PATCH 15/20] Revert formatting-only changes --- app/assets/stylesheets/_base-variables.scss | 15 +-- ..._analyses.scss => _projects-analyses.scss} | 0 app/assets/stylesheets/_projects-index.scss | 114 ++++++++---------- app/assets/stylesheets/_projects.scss | 1 + app/assets/stylesheets/application.css.scss | 65 +++++----- app/models/project.rb | 48 ++++---- config/routes.rb | 74 ++++++------ spec/factories.rb | 15 +-- 8 files changed, 160 insertions(+), 172 deletions(-) rename app/assets/stylesheets/{_project_analyses.scss => _projects-analyses.scss} (100%) diff --git a/app/assets/stylesheets/_base-variables.scss b/app/assets/stylesheets/_base-variables.scss index db3074c3..1c017aa7 100644 --- a/app/assets/stylesheets/_base-variables.scss +++ b/app/assets/stylesheets/_base-variables.scss @@ -2,16 +2,16 @@ $gw-column: 60px; $gw-gutter: 20px; $serif: Georgia, Cambria, serif; -$sans-serif: "museo-sans", "proxima-nova", $helvetica; +$sans-serif: 'museo-sans', 'proxima-nova', $helvetica; -$base-font-color: rgb(55, 55, 55); -$lighter-base-font-color: rgb(46, 46, 46); +$base-font-color: rgb(55,55,55); +$lighter-base-font-color: rgb(46,46,46); $muted-color: #ccc; $pink: rgb(255, 75, 126); $button-pink: rgb(252, 103, 147); -$blue: rgb(34, 122, 255); //blue +$blue: rgb(34,122,255); //blue $green-background: #a7f3d0; $green-text: #065f46; @@ -22,9 +22,6 @@ $yellow-text: #854d0e; $red-background: #fecaca; $red-text: #7f1d1d; -$base-font-color: rgb(55, 55, 55); -$lighter-base-font-color: rgb(46, 46, 46); - /*** * Responsiveness Sizes * @@ -34,7 +31,7 @@ $lighter-base-font-color: rgb(46, 46, 46); * mobile-small: for additional changes that accommdate very small screens like iPhone 5S ***/ -$maximum-width: 60em; // Equivalent to 969px; +$maximum-width: 60em; // Equivalent to 969px; $tablet-standard: 60em; $mobile-standard: 30em; // Equivalent to 480px; -$mobile-small: 20em; // Equivalent to 320px; +$mobile-small: 20em; // Equivalent to 320px; diff --git a/app/assets/stylesheets/_project_analyses.scss b/app/assets/stylesheets/_projects-analyses.scss similarity index 100% rename from app/assets/stylesheets/_project_analyses.scss rename to app/assets/stylesheets/_projects-analyses.scss diff --git a/app/assets/stylesheets/_projects-index.scss b/app/assets/stylesheets/_projects-index.scss index 41d90479..64bca324 100644 --- a/app/assets/stylesheets/_projects-index.scss +++ b/app/assets/stylesheets/_projects-index.scss @@ -1,4 +1,5 @@ body.projects-index { + section.container { @extend .clearfix; } @@ -73,7 +74,7 @@ body.projects-index { } .admin-panel { - border-right: 1px solid rgb(200, 200, 200); + border-right: 1px solid rgb(200,200,200); float: left; margin-right: 39px; padding: 0px 20px 20px 0px; @@ -117,13 +118,13 @@ body.projects-index { } h2 { - border-bottom: 1px dotted rgb(200, 200, 200); + border-bottom: 1px dotted rgb(200,200,200); font: 300 18px/20px $sans-serif; margin: 15px 0px; padding-bottom: 30px; text-align: center; @media all and (max-width: $maximum-width) { - border-bottom: 1px solid rgb(200, 200, 200); + border-bottom: 1px solid rgb(200,200,200); margin-bottom: 0px; text-align: left; padding-left: 70px; @@ -135,7 +136,7 @@ body.projects-index { margin: 0px; li { - border-bottom: 1px dotted rgb(200, 200, 200); + border-bottom: 1px dotted rgb(200,200,200); list-style: none; margin-bottom: 15px; padding: 0px 0px 15px 0px; @@ -162,7 +163,7 @@ body.projects-index { } a { - color: rgb(55, 55, 55); + color: rgb(55,55,55); display: block; font: normal 12px/14px $sans-serif; padding: 5px 0px 5px 10px; @@ -173,7 +174,7 @@ body.projects-index { &:hover { background: $pink; - color: rgb(255, 255, 255); + color: rgb(255,255,255); } } } @@ -210,12 +211,11 @@ body.projects-index { } .application-filters { - background: rgb(230, 230, 230); - border: 1px solid rgb(200, 200, 200); + background: rgb(230,230,230); + border: 1px solid rgb(200,200,200); border-radius: 4px; - box-shadow: inset 0 -1px 0 0 rgba(255, 255, 255, 0.3), - inset 0 1px 0 0 rgba(255, 255, 255, 0.8); - @include linear-gradient(rgb(240, 240, 240), rgb(220, 220, 220)); + box-shadow: inset 0 -1px 0 0 rgba(255,255,255, 0.3), inset 0 1px 0 0 rgba(255,255,255, 0.8); + @include linear-gradient(rgb(240,240,240), rgb(220,220,220)); margin-bottom: 10px; position: relative; width: grid-width(9); @@ -229,8 +229,8 @@ body.projects-index { position: relative; a.chapter-selection { - background: rgb(230, 230, 230); - border: 1px solid rgb(190, 190, 190); + background: rgb(230,230,230); + border: 1px solid rgb(190,190,190); border-radius: 3px; display: block; padding: 5px 40px 5px 10px; @@ -238,18 +238,18 @@ body.projects-index { z-index: 2; &:hover { - background: rgb(220, 220, 220); + background: rgb(220,220,220); } span { - color: rgb(55, 55, 55); + color: rgb(55,55,55); font: normal 16px/18px $sans-serif; - text-shadow: 0 1px rgba(255, 255, 255, 0.75); + text-shadow: 0 1px rgba(255,255,255, 0.75); } span.arrow { border: { - top: 7px solid rgb(150, 150, 150); + top: 7px solid rgb(150,150,150); right: 7px solid transparent; bottom: 7px solid transparent; left: 7px solid transparent; @@ -260,18 +260,18 @@ body.projects-index { } &.expanded { - background: rgb(220, 220, 220); - border: 1px solid rgb(150, 150, 150); + background: rgb(220,220,220); + border: 1px solid rgb(150,150,150); border-radius: 3px 3px 0px 0px; cursor: pointer; } } ol.chapter-selector { - background: rgb(255, 255, 255); - border: 1px solid rgb(150, 150, 150); + background: rgb(255,255,255); + border: 1px solid rgb(150,150,150); @include border-bottom-radius(4px); - box-shadow: 0 3px 5px 0 rgba(0, 0, 0, 0.2); + box-shadow: 0 3px 5px 0 rgba(0,0,0, 0.2); opacity: 0; overflow-y: auto; position: relative; @@ -285,7 +285,7 @@ body.projects-index { } li { - border-bottom: 1px solid rgb(200, 200, 200); + border-bottom: 1px solid rgb(200,200,200); list-style: none; margin: 0 5px; padding: 3px 0px; @@ -295,14 +295,14 @@ body.projects-index { } a { - color: rgb(55, 55, 55); + color: rgb(55,55,55); display: block; padding: 5px 8px; text-decoration: none; &:hover { background: $pink; - color: rgb(255, 255, 255); + color: rgb(255,255,255); } } } @@ -318,9 +318,7 @@ body.projects-index { float: right; padding: 0px 10px 10px 10px; - div.short-list-toggle, - div.funded-toggle, - div.toggle { + div.short-list-toggle, div.funded-toggle, div.toggle { float: left; margin-right: 10px; margin-top: 20px; @@ -333,29 +331,28 @@ body.projects-index { } label { - background: rgb(230, 230, 230); - border: 1px solid rgb(190, 190, 190); + background: rgb(230,230,230); + border: 1px solid rgb(190,190,190); border-radius: 3px; font: normal 12px $sans-serif; height: 21px; margin: 0px; - text-shadow: 0 1px rgba(255, 255, 255, 0.8); + text-shadow: 0 1px rgba(255,255,255, 0.8); padding: 8px 12px 0px 30px; &:hover { - background: rgb(220, 220, 220); + background: rgb(220,220,220); cursor: pointer; } } input[type="checkbox"]:checked + label { - background: rgb(220, 220, 220); + background: rgb(220,220,220); cursor: pointer; } } - div.date-range, - div.search { + div.date-range, div.search { float: left; margin-right: 10px; margin-top: 20px; @@ -365,12 +362,12 @@ body.projects-index { } input[type="text"] { - color: rgb(180, 180, 180); + color: rgb(180,180,180); height: 30px; width: 90px; &:focus { - color: rgb(55, 55, 55); + color: rgb(55,55,55); } } @@ -379,9 +376,8 @@ body.projects-index { } } - input[type="submit"], - button[type="submit"] { - @include button(rgb(230, 230, 230)); + input[type="submit"], button[type="submit"] { + @include button(rgb(230,230,230)); margin: 0px; margin-top: 20px; width: auto; @@ -397,10 +393,10 @@ body.projects-index { } article.project { - border: 1px solid rgb(200, 200, 200); - background: rgb(255, 255, 255); + border: 1px solid rgb(200,200,200); + background: rgb(255,255,255); border-radius: 4px; - box-shadow: 0 1px 6px 0 rgba(0, 0, 0, 0.15); + box-shadow: 0 1px 6px 0 rgba(0,0,0, 0.15); margin-top: 20px; margin-bottom: 20px; @@ -409,7 +405,7 @@ body.projects-index { div.title { @extend .clearfix; - border-bottom: 1px solid rgb(200, 200, 200); + border-bottom: 1px solid rgb(200,200,200); padding: 10px; a.title { @@ -425,7 +421,7 @@ body.projects-index { .public-link { display: none; - padding-top: 0.5em; + padding-top: .5em; font-size: 0.8em; clear: both; @@ -446,7 +442,7 @@ body.projects-index { a.short-list, a.mark-as-winner, a.project-actions-toggle { - @include button(rgb(240, 240, 240)); + @include button(rgb(240,240,240)); float: right; margin-left: 10px; text-decoration: none; @@ -456,7 +452,7 @@ body.projects-index { } &.mark-as-winner:hover { - @include button(rgb(34, 122, 255)); + @include button(rgb(34,122,255)); } } @@ -465,7 +461,7 @@ body.projects-index { } a.auto-awesome { - @include button(rgb(240, 240, 240)); + @include button(rgb(240,240,240)); float: right; margin-left: 10px; text-decoration: none; @@ -473,10 +469,6 @@ body.projects-index { &.auto-awesome:hover { @include button($pink); } - - // &.mark-as-winner:hover { - // @include button(rgb(34,122,255)); - // } } } @@ -490,7 +482,7 @@ body.projects-index { } .meta-data { - border-bottom: 1px solid rgb(200, 200, 200); + border-bottom: 1px solid rgb(200,200,200); padding: 10px; font-size: 0.8em; @@ -500,7 +492,7 @@ body.projects-index { } .project-actions-toggle { - margin-left: 0.25em; + margin-left: .25em; font-size: 1.2em; float: right; } @@ -534,7 +526,7 @@ body.projects-index { margin-bottom: 0; li { - padding-bottom: 0.5em; + padding-bottom: .5em; a { text-decoration: none; @@ -569,8 +561,7 @@ body.projects-index { font: normal 16px/20px $serif; padding: 10px; - h3, - p { + h3, p { margin: 0; padding: 0; font: normal 14px/20px $serif; @@ -605,7 +596,7 @@ body.projects-index { .see-more { background: rgba(245, 245, 245, 1); - border-top: 1px solid rgb(200, 200, 200); + border-top: 1px solid rgb(200,200,200); @include border-bottom-radius(4px); display: block; text-decoration: none; @@ -628,12 +619,11 @@ body.projects-index { } .filtering { - border-top: 1px solid rgb(200, 200, 200); + border-top: 1px solid rgb(200,200,200); padding: 10px; font-size: 0.8em; - .hiding-form, - .unhiding-form { + .hiding-form, .unhiding-form { p { margin: 0; } @@ -654,7 +644,7 @@ body.projects-index { } input.hide-action { - @include button(rgb(230, 230, 230)); + @include button(rgb(230,230,230)); width: 80px; position: relative; margin: 0; diff --git a/app/assets/stylesheets/_projects.scss b/app/assets/stylesheets/_projects.scss index a1176d17..bf6afc9d 100644 --- a/app/assets/stylesheets/_projects.scss +++ b/app/assets/stylesheets/_projects.scss @@ -1,3 +1,4 @@ @import 'projects-index'; @import 'projects-new'; @import 'projects-edit'; +@import 'projects-analyses'; diff --git a/app/assets/stylesheets/application.css.scss b/app/assets/stylesheets/application.css.scss index 887fcc64..042988d8 100644 --- a/app/assets/stylesheets/application.css.scss +++ b/app/assets/stylesheets/application.css.scss @@ -5,38 +5,37 @@ *= require_self */ -@import "flutie"; -@import "bourbon"; -@import "flexbox-ultralight"; -@import "lightgallery"; +@import 'flutie'; +@import 'bourbon'; +@import 'flexbox-ultralight'; +@import 'lightgallery'; -@import "base-variables"; -@import "base-extends"; -@import "base-mixins"; -@import "base-animations"; -@import "base"; -@import "buttons"; -@import "navigation"; -@import "shared-feed"; -@import "shared-forms"; -@import "chapters"; -@import "projects"; -@import "project_analyses"; -@import "comments"; -@import "home"; -@import "invitations-new"; -@import "users"; -@import "finalists"; -@import "shared-header"; -@import "shared-footer"; -@import "shared-date-picker"; -@import "shared-flash-messages"; -@import "about"; -@import "faq"; -@import "winners"; -@import "funded-projects"; -@import "pagination"; -@import "magnific-popup"; +@import 'base-variables'; +@import 'base-extends'; +@import 'base-mixins'; +@import 'base-animations'; +@import 'base'; +@import 'buttons'; +@import 'navigation'; +@import 'shared-feed'; +@import 'shared-forms'; +@import 'chapters'; +@import 'projects'; +@import 'comments'; +@import 'home'; +@import 'invitations-new'; +@import 'users'; +@import 'finalists'; +@import 'shared-header'; +@import 'shared-footer'; +@import 'shared-date-picker'; +@import 'shared-flash-messages'; +@import 'about'; +@import 'faq'; +@import 'winners'; +@import 'funded-projects'; +@import 'pagination'; +@import 'magnific-popup'; -@import "owl.carousel.min"; -@import "owl.theme.default.min"; +@import 'owl.carousel.min'; +@import 'owl.theme.default.min'; diff --git a/app/models/project.rb b/app/models/project.rb index a04287de..c08d4308 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -10,17 +10,17 @@ class Project < ApplicationRecord belongs_to :hidden_by_user, class_name: "User", optional: true has_many :comments has_many :votes - has_many :users, through: :votes + has_many :users, :through => :votes has_many :photos, -> { merge(Photo.sorted) } has_many :real_photos, -> { merge(Photo.image_files.sorted) }, class_name: "Photo" - has_one :primary_photo, -> { merge(Photo.image_files.sorted) }, class_name: "Photo" + has_one :primary_photo, -> { merge(Photo.image_files.sorted) }, class_name: "Photo" has_one :project_moderation, dependent: :destroy has_one :project_analysis, dependent: :destroy before_validation UrlNormalizer.new(:url, :rss_feed_url) - validates :url, :rss_feed_url, url: true, allow_blank: true - validates_length_of :url, :rss_feed_url, maximum: 255 + validates :url, :rss_feed_url, :url => true, :allow_blank => true + validates_length_of :url, :rss_feed_url, :maximum => 255 validates_presence_of :name validates_presence_of :title @@ -30,7 +30,7 @@ class Project < ApplicationRecord validates_presence_of :use_for_money validates_presence_of :chapter_id - delegate :name, to: :chapter, prefix: true + delegate :name, :to => :chapter, :prefix => true before_save :ensure_funded_description before_save :update_photo_order @@ -44,9 +44,9 @@ class Project < ApplicationRecord # specify the default fields on which full text search will be performed extend Searchable(:name, :title, :email, :about_me, :about_project, :use_for_money, - :extra_answer_1, :extra_answer_2, :extra_answer_3) + :extra_answer_1, :extra_answer_2, :extra_answer_3) - scope :public_search, lambda { |query| search({name: query, title: query, funded_description: query, url: query}, false) } + scope :public_search, lambda { |query| search({ name: query, title: query, funded_description: query, url: query }, false) } accepts_nested_attributes_for :photos, allow_destroy: true @@ -55,9 +55,9 @@ def self.winner_count end def self.visible_to(user) - joins(:chapter) - .joins("LEFT OUTER JOIN roles ON roles.chapter_id = chapters.id") - .where("roles.user_id = #{user.id} OR chapters.name = ?", Chapter::ANY_CHAPTER_NAME) + joins(:chapter). + joins("LEFT OUTER JOIN roles ON roles.chapter_id = chapters.id"). + where("roles.user_id = #{user.id} OR chapters.name = ?", Chapter::ANY_CHAPTER_NAME) end def self.with_votes_for_chapter(c) @@ -69,15 +69,15 @@ def self.with_votes_by_members_of_chapter(c) end def self.voted_for_by_members_of(chapter) - joins(users: :chapters).where("chapters.id = ? OR chapters.name = ?", chapter.id, Chapter::ANY_CHAPTER_NAME) + joins(:users => :chapters).where("chapters.id = ? OR chapters.name = ?", chapter.id, Chapter::ANY_CHAPTER_NAME) end def self.during_timeframe(start_date, end_date) # FIXME the database stores dates in UTC, whereas we parse the dates # provided in the local zone. This can result in mismatches when the # overlap crosses midnight. - start_date = 100.years.ago.strftime("%Y-%m-%d") if start_date.blank? - end_date = Time.zone.now.strftime("%Y-%m-%d") if end_date.blank? + start_date = 100.years.ago.strftime('%Y-%m-%d') if start_date.blank? + end_date = Time.zone.now.strftime('%Y-%m-%d') if end_date.blank? where( "projects.created_at > ? AND projects.created_at < ?", @@ -88,15 +88,15 @@ def self.during_timeframe(start_date, end_date) def self.by_vote_count(sort: nil) order = case sort - when "date" then "projects.created_at DESC, vote_count DESC" - when "title" then "projects.title ASC, vote_count DESC" - else "vote_count DESC, projects.created_at ASC" - end + when "date" then "projects.created_at DESC, vote_count DESC" + when "title" then "projects.title ASC, vote_count DESC" + else "vote_count DESC, projects.created_at ASC" + end - select("projects.chapter_id, projects.id, projects.title, projects.funded_on, COUNT(votes.project_id) as vote_count, projects.created_at") - .group("projects.id, projects.title, votes.project_id") - .joins(:users) - .order(order) + select("projects.chapter_id, projects.id, projects.title, projects.funded_on, COUNT(votes.project_id) as vote_count, projects.created_at"). + group("projects.id, projects.title, votes.project_id"). + joins(:users). + order(order) end def self.winners @@ -118,7 +118,7 @@ def self.csv_export(projects) end def self.attributes_for_export - %w[name title about_project use_for_money about_me url email phone chapter_name id created_at funded_on extra_question_1 extra_answer_1 extra_question_2 extra_answer_2 extra_question_3 extra_answer_3 rss_feed_url hidden_at hidden_reason] + %w(name title about_project use_for_money about_me url email phone chapter_name id created_at funded_on extra_question_1 extra_answer_1 extra_question_2 extra_answer_2 extra_question_3 extra_answer_3 rss_feed_url hidden_at hidden_reason) end def to_a @@ -179,11 +179,11 @@ def has_images? end def extra_question(num) - ((question = read_attribute(:"extra_question_#{num}")) && question.present?) ? question : nil + (question = read_attribute("extra_question_#{num}".to_sym)) && question.present? ? question : nil end def extra_answer(num) - ((answer = read_attribute(:"extra_answer_#{num}")) && answer.present?) ? answer : nil + (answer = read_attribute("extra_answer_#{num}".to_sym)) && answer.present? ? answer : nil end def hide!(reason, user) diff --git a/config/routes.rb b/config/routes.rb index 40f0a541..7ee77e9a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,54 +1,54 @@ Rails.application.routes.draw do if ENV["AWS_BUCKET"].present? - mount Shrine.uppy_s3_multipart(:cache) => "/s3/multipart" + mount Shrine.uppy_s3_multipart(:cache) => '/s3/multipart' else - mount Tus::Server => "/uploads" + mount Tus::Server => '/uploads' end constraints(SubdomainConstraint) do get "/apply" => "subdomains#apply" - get "*url" => "subdomains#chapter" - get "/" => "subdomains#chapter" + get "*url" => "subdomains#chapter" + get "/" => "subdomains#chapter" end constraints(DomainConstraint) do get "*url" => "subdomains#canonical" - get "/" => "subdomains#canonical" + get "/" => "subdomains#canonical" end get "/blog/contact/" => redirect("/en/contact") - get "/blog/about/" => redirect("/en/about_us") - get "/blog" => redirect("http://blog.awesomefoundation.org") - get "/blog/*path" => redirect { |params, request| "http://blog.awesomefoundation.org/#{Addressable::URI.escape(params[:path])}" }, :format => false - get "/apply" => redirect("/en/submissions/new") + get "/blog/about/" => redirect("/en/about_us") + get "/blog" => redirect("http://blog.awesomefoundation.org") + get "/blog/*path" => redirect { |params, request| "http://blog.awesomefoundation.org/#{Addressable::URI.escape(params[:path])}" }, :format => false + get "/apply" => redirect("/en/submissions/new") - resources :passwords, controller: "clearance/passwords", only: [:new, :create] + resources :passwords, :controller => 'clearance/passwords', :only => [:new, :create] - resources :users, shallow: true do - resource :password, controller: "clearance/passwords", only: [:create, :edit, :update] + resources :users, :shallow => true do + resource :password, :controller => 'clearance/passwords', :only => [:create, :edit, :update] end scope "(:locale)", locale: /#{I18n.available_locales.join("|")}/ do - resource :session, controller: :sessions, only: [:new, :create, :destroy] + resource :session, controller: :sessions, only: [:new, :create, :destroy] - get "sign_in", to: "sessions#new" - delete "sign_out", to: "sessions#destroy" + get "sign_in", :to => "sessions#new" + delete "sign_out", :to => "sessions#destroy" - resources :users, only: [:index, :update, :edit] do - resource :admins, only: [:create, :destroy] + resources :users, :only => [:index, :update, :edit] do + resource :admins, :only => [:create, :destroy] end - resources :chapters, only: [:index, :show, :new, :create, :edit, :update] do - resources :finalists, only: [:index] - resources :projects, only: [:index, :show] do - resource :winner, only: [:edit] + resources :chapters, :only => [:index, :show, :new, :create, :edit, :update] do + resources :finalists, :only => [:index] + resources :projects, :only => [:index, :show] do + resource :winner, :only => [:edit] end resources :moderations, only: [:index] - resources :users, only: [:index] + resources :users, :only => [:index] end - resources :invitations, only: [:new, :create] do - resources :acceptances, only: [:new, :create] + resources :invitations, :only => [:new, :create] do + resources :acceptances, :only => [:new, :create] end resources :funded_projects, path: "projects", only: [:index, :show] @@ -59,15 +59,15 @@ put "unhide" end resources :comments - resource :winner, only: [:create, :update, :destroy] - resource :vote, only: [:create, :destroy] - resource :moderation, only: [] do + resource :winner, :only => [:create, :update, :destroy] + resource :vote, :only => [:create, :destroy] + resource :moderation, :only => [] do put :confirm_spam put :confirm_legit put :undo end - resource :project_analysis, only: [] do - get "show_or_create", to: "project_analyses#show_or_create" + resource :project_analysis, :only => [] do + get "show_or_create", :to => "project_analyses#show_or_create" end end @@ -79,21 +79,21 @@ end end - resources :roles, only: [:destroy] do - resource :promotions, only: [:create, :destroy] + resources :roles, :only => [:destroy] do + resource :promotions, :only => [:create, :destroy] end - %w[about_us faq start_a_chapter].each do |page| - get page, to: "high_voltage/pages#show", id: page + %w(about_us faq start_a_chapter).each do |page| + get page, :to => 'high_voltage/pages#show', :id => page end - root to: "home#index" + root :to => 'home#index' end - get "/404", to: "errors#not_found" - get "/500", to: "errors#internal_server_error" + get "/404", :to => "errors#not_found" + get "/500", :to => "errors#internal_server_error" # All other routes are considered 404s. ActionController::RoutingError # will catch them, but that fills our logs with noisy exceptions. - get "*url", to: "errors#not_found" + get '*url', :to => 'errors#not_found' end diff --git a/spec/factories.rb b/spec/factories.rb index 0d745e09..2411d2c1 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -1,7 +1,7 @@ # coding: utf-8 FactoryBot.define do - sequence(:email) { |n| "user#{n}@example.com" } - sequence(:title) { |n| "Something Awesome ##{n}" } + sequence(:email) {|n| "user#{n}@example.com" } + sequence(:title) {|n| "Something Awesome ##{n}" } sequence(:index) factory :chapter do @@ -27,13 +27,13 @@ factory :user_with_dean_role do after(:create) do |user| - FactoryBot.create(:role, user: user, name: "dean") + FactoryBot.create(:role, :user => user, :name => "dean") user.reload end end factory :user_with_trustee_role do - chapters { [association(:chapter)] } + chapters { [association(:chapter) ] } end end @@ -48,7 +48,7 @@ factory :invitation do email - association :inviter, factory: :user_with_dean_role + association :inviter, :factory => :user_with_dean_role after(:build) do |invitation, proxy| invitation.chapter = invitation.inviter.chapters.first @@ -70,7 +70,7 @@ end factory :winning_project do - sequence(:funded_on) { |n| (3000 - n.to_i).days.ago } + sequence(:funded_on) { |n| (3000-n.to_i).days.ago } end factory :hidden_project do @@ -107,10 +107,11 @@ # custom storage the way we do. factory :s3_photo, class: "Photo" do project - storage_keys { {store: :s3_store, cache: :s3_cache} } + storage_keys { { store: :s3_store, cache: :s3_cache } } image_data { FakeData.shrine_uploaded_file("1.JPG") } end + factory :comment do project user From 5f4bf5f80b8761942c1b7dd9527fa16bd10f063a Mon Sep 17 00:00:00 2001 From: Jesse Chan-Norris Date: Tue, 3 Feb 2026 18:09:30 -0500 Subject: [PATCH 16/20] Remove standardrb --- Gemfile | 1 - Gemfile.lock | 55 ++++++++++------------------------------------------ 2 files changed, 10 insertions(+), 46 deletions(-) diff --git a/Gemfile b/Gemfile index 85595870..39e0708c 100644 --- a/Gemfile +++ b/Gemfile @@ -56,7 +56,6 @@ group :development do gem 'dotenv-rails' gem 'letter_opener' gem 'listen' - gem 'standard' gem 'web-console' end diff --git a/Gemfile.lock b/Gemfile.lock index eb58781b..14bfae1a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -81,7 +81,6 @@ GEM argon2 (2.3.2) ffi (~> 1.15) ffi-compiler (~> 1.0) - ast (2.4.3) aws-eventstream (1.3.2) aws-partitions (1.1083.0) aws-sdk-core (3.222.1) @@ -198,7 +197,9 @@ GEM faraday-multipart (1.0.4) multipart-post (~> 2) faraday-net_http (3.0.2) + ffi (1.17.1) ffi (1.17.1-x86_64-darwin) + ffi (1.17.1-x86_64-linux) ffi-compiler (1.3.2) ffi (>= 1.15.5) rake @@ -245,13 +246,10 @@ GEM jquery-ui-rails (3.0.1) jquery-rails railties (>= 3.1.0) - json (2.18.1) - language_server-protocol (3.17.0.5) launchy (2.5.2) addressable (~> 2.8) letter_opener (1.8.1) launchy (>= 2.2, < 3) - lint_roller (1.1.0) listen (3.9.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) @@ -273,6 +271,7 @@ GEM metaclass (0.0.4) method_source (1.1.0) mini_mime (1.1.5) + mini_portile2 (2.8.9) minitest (5.25.5) mocha (1.2.1) metaclass (~> 0.0.1) @@ -289,17 +288,17 @@ GEM net-protocol newrelic_rpm (9.22.0) nio4r (2.7.5) + nokogiri (1.18.9) + mini_portile2 (~> 2.8.2) + racc (~> 1.4) nokogiri (1.18.9-x86_64-darwin) racc (~> 1.4) - parallel (1.27.0) - parser (3.3.10.1) - ast (~> 2.4.1) - racc + nokogiri (1.18.9-x86_64-linux) + racc (~> 1.4) pg (1.4.6) pp (0.6.2) prettyprint prettyprint (0.2.0) - prism (1.9.0) pry (0.14.2) coderay (~> 1.1) method_source (~> 1.0) @@ -365,7 +364,6 @@ GEM thor (~> 1.0, >= 1.2.2) tsort (>= 0.2) zeitwerk (~> 2.6) - rainbow (3.1.1) rake (13.2.1) rb-fsevent (0.11.2) rb-inotify (0.11.1) @@ -407,30 +405,11 @@ GEM rspec-mocks (~> 3.13) rspec-support (~> 3.13) rspec-support (3.13.2) - rubocop (1.82.1) - json (~> 2.3) - language_server-protocol (~> 3.17.0.2) - lint_roller (~> 1.1.0) - parallel (~> 1.10) - parser (>= 3.3.0.2) - rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 2.9.3, < 3.0) - rubocop-ast (>= 1.48.0, < 2.0) - ruby-progressbar (~> 1.7) - unicode-display_width (>= 2.4.0, < 4.0) - rubocop-ast (1.49.0) - parser (>= 3.3.7.2) - prism (~> 1.7) - rubocop-performance (1.26.1) - lint_roller (~> 1.1) - rubocop (>= 1.75.0, < 2.0) - rubocop-ast (>= 1.47.1, < 2.0) ruby-next-core (1.1.2) ruby-openai (7.3.1) event_stream_parser (>= 0.3.0, < 2.0.0) faraday (>= 1) faraday-multipart (>= 1) - ruby-progressbar (1.13.0) ruby2_keywords (0.0.5) rubyzip (2.4.1) sass (3.7.4) @@ -481,18 +460,6 @@ GEM sprockets-redirect (0.1.0) activesupport (>= 3.1.0) rack - standard (1.53.0) - language_server-protocol (~> 3.17.0.2) - lint_roller (~> 1.0) - rubocop (~> 1.82.0) - standard-custom (~> 1.0.0) - standard-performance (~> 1.8) - standard-custom (1.0.2) - lint_roller (~> 1.0) - rubocop (~> 1.50) - standard-performance (1.9.0) - lint_roller (~> 1.1) - rubocop-performance (~> 1.26.0) stimulus-rails (1.3.4) railties (>= 6.0.0) stringio (3.1.6) @@ -518,9 +485,6 @@ GEM roda (>= 2.27, < 4) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - unicode-display_width (3.2.0) - unicode-emoji (~> 4.1) - unicode-emoji (4.2.0) uppy-s3_multipart (1.2.1) aws-sdk-s3 (~> 1.0) content_disposition (~> 1.0) @@ -546,6 +510,8 @@ GEM PLATFORMS ruby + x86_64-darwin-20 + x86_64-linux DEPENDENCIES addressable @@ -609,7 +575,6 @@ DEPENDENCIES simple_form (~> 5.1) sprockets-rails sprockets-redirect - standard stimulus-rails sucker_punch (~> 3.1) terser From 7249d7cb05a683c57bf16d1eaf784bdda98bd62e Mon Sep 17 00:00:00 2001 From: Jesse Chan-Norris Date: Tue, 3 Feb 2026 18:36:17 -0500 Subject: [PATCH 17/20] Bump nokogiri from 1.18.9 to 1.19.0 --- Gemfile | 2 +- Gemfile.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index 39e0708c..a292e52e 100644 --- a/Gemfile +++ b/Gemfile @@ -27,7 +27,7 @@ gem 'jquery-rails', '~> 4.1.1' gem 'jquery-ui-rails', '~> 3.0.1' gem 'magnific-popup-rails' gem 'nio4r', '>= 2.7.3' -gem 'nokogiri', '~> 1.18.9' +gem 'nokogiri', '~> 1.19' gem 'ruby-openai' gem 'pg', '~> 1.4.4' gem 'puma' diff --git a/Gemfile.lock b/Gemfile.lock index 14bfae1a..82ac61d4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -288,12 +288,12 @@ GEM net-protocol newrelic_rpm (9.22.0) nio4r (2.7.5) - nokogiri (1.18.9) + nokogiri (1.19.0) mini_portile2 (~> 2.8.2) racc (~> 1.4) - nokogiri (1.18.9-x86_64-darwin) + nokogiri (1.19.0-x86_64-darwin) racc (~> 1.4) - nokogiri (1.18.9-x86_64-linux) + nokogiri (1.19.0-x86_64-linux-gnu) racc (~> 1.4) pg (1.4.6) pp (0.6.2) @@ -549,7 +549,7 @@ DEPENDENCIES magnific-popup-rails newrelic_rpm nio4r (>= 2.7.3) - nokogiri (~> 1.18.9) + nokogiri (~> 1.19) pg (~> 1.4.4) pry pry-nav From 76f4fe4c1dbc65359d62af7c4fb6a07fe57c8a84 Mon Sep 17 00:00:00 2001 From: Jesse Chan-Norris Date: Tue, 3 Feb 2026 18:55:24 -0500 Subject: [PATCH 18/20] try to fix ffi platform-specific build --- Gemfile.lock | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Gemfile.lock b/Gemfile.lock index 82ac61d4..a6ce55f5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -200,6 +200,7 @@ GEM ffi (1.17.1) ffi (1.17.1-x86_64-darwin) ffi (1.17.1-x86_64-linux) + ffi (1.17.1-x86_64-linux-gnu) ffi-compiler (1.3.2) ffi (>= 1.15.5) rake @@ -512,6 +513,7 @@ PLATFORMS ruby x86_64-darwin-20 x86_64-linux + x86_64-linux-gnu DEPENDENCIES addressable From 12c79adfd89cc11a91e2e39ef689103e94f896d3 Mon Sep 17 00:00:00 2001 From: Jesse Chan-Norris Date: Tue, 3 Feb 2026 19:03:00 -0500 Subject: [PATCH 19/20] Another attempt to fix Gemfile --- Gemfile.lock | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index a6ce55f5..87f80418 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -200,7 +200,7 @@ GEM ffi (1.17.1) ffi (1.17.1-x86_64-darwin) ffi (1.17.1-x86_64-linux) - ffi (1.17.1-x86_64-linux-gnu) + ffi (1.17.1-x86_64-linux-musl) ffi-compiler (1.3.2) ffi (>= 1.15.5) rake @@ -296,6 +296,8 @@ GEM racc (~> 1.4) nokogiri (1.19.0-x86_64-linux-gnu) racc (~> 1.4) + nokogiri (1.19.0-x86_64-linux-musl) + racc (~> 1.4) pg (1.4.6) pp (0.6.2) prettyprint @@ -513,7 +515,7 @@ PLATFORMS ruby x86_64-darwin-20 x86_64-linux - x86_64-linux-gnu + x86_64-linux-musl DEPENDENCIES addressable From 14780473c168ea91131365a2b580d4b377e2c6d4 Mon Sep 17 00:00:00 2001 From: Jesse Chan-Norris Date: Tue, 3 Feb 2026 19:43:17 -0500 Subject: [PATCH 20/20] Bump ffi to try to build properly --- Gemfile.lock | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 87f80418..8f2d38eb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -197,10 +197,9 @@ GEM faraday-multipart (1.0.4) multipart-post (~> 2) faraday-net_http (3.0.2) - ffi (1.17.1) - ffi (1.17.1-x86_64-darwin) - ffi (1.17.1-x86_64-linux) - ffi (1.17.1-x86_64-linux-musl) + ffi (1.17.3) + ffi (1.17.3-x86_64-darwin) + ffi (1.17.3-x86_64-linux-gnu) ffi-compiler (1.3.2) ffi (>= 1.15.5) rake @@ -296,8 +295,6 @@ GEM racc (~> 1.4) nokogiri (1.19.0-x86_64-linux-gnu) racc (~> 1.4) - nokogiri (1.19.0-x86_64-linux-musl) - racc (~> 1.4) pg (1.4.6) pp (0.6.2) prettyprint @@ -515,7 +512,7 @@ PLATFORMS ruby x86_64-darwin-20 x86_64-linux - x86_64-linux-musl + x86_64-linux-gnu DEPENDENCIES addressable