diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e0ee7753f..a4076be75 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,6 +1,9 @@ name: build -on: [push, pull_request] +on: + - push + - pull_request + - workflow_dispatch jobs: build: @@ -11,10 +14,10 @@ jobs: TZ: America/Los_Angeles steps: - uses: actions/checkout@v1 - - name: Install Ruby (3.0) + - name: Install Ruby (3.2.7) uses: ruby/setup-ruby@v1 with: - ruby-version: 3.0.3 + ruby-version: 3.2.7 bundler-cache: true - name: Set up Code Climate test-reporter diff --git a/.github/workflows/pull_request_template.md b/.github/workflows/pull_request_template.md new file mode 100644 index 000000000..de3b4a6a9 --- /dev/null +++ b/.github/workflows/pull_request_template.md @@ -0,0 +1,31 @@ +### [Pivotal Tracker Link][tracker] + + +[tracker]: https://www.pivotaltracker.com/story/show/your-story-id + +## What this PR does: + + +This pull request fixes|implements (pick one...) ______. + +### Include screenshots, videos, etc. + +#### Who authored this PR? + + + +### How should this PR be tested? + +* Is there a deploy we can view? +* What do the specs/features test? +* Are there edge cases to watch out for? + +#### Are there any complications to deploying this? + + + +### Checklist: + +- [ ] Has this been deployed to a staging environment or reviewed by a customer? +- [ ] Tag someone for code review (either a coach / team member) +- [ ] I have renamed the branch to match PivotTracker's suggested one (necessary for BlueJay) (e.g. `michael/12345-add-new-feature` Any branch name will do as long as the story ID is there. You can use `git checkout -b [new-branch-name]`) diff --git a/.github/workflows/rubocop.yml b/.github/workflows/rubocop.yml new file mode 100644 index 000000000..dba393e39 --- /dev/null +++ b/.github/workflows/rubocop.yml @@ -0,0 +1,19 @@ +name: RuboCop + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + env: + BUNDLE_WITHOUT: default doc job cable storage ujs test db + + steps: + - uses: actions/checkout@v2 + - name: Set up Ruby 3.2.7 + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.2.7 + bundler-cache: true + - name: Run RuboCop + run: bundle exec rubocop --parallel \ No newline at end of file diff --git a/.gitignore b/.gitignore index 4204322fe..d43c3c23c 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,15 @@ # Ignore application configuration /config/application.yml + +/.DS_Store +/app/.DS_Store +/app/views/.DS_Store +/app/assets/.DS_Store +/app/assets/images/.DS_Store +/db/.DS_Store +/lib/.DS_Store +.DS_Store +.vs/ +.vs +.vscode diff --git a/.rubocop.yml b/.rubocop.yml index 091816f87..307218239 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,17 +1,292 @@ -Style/Documentation: - Enabled: false +plugins: + - rubocop-performance + - rubocop-rails + - rubocop-rspec -Metrics: +AllCops: + TargetRubyVersion: 3.2 + # RuboCop has a bunch of cops enabled by default. This setting tells RuboCop + # to ignore them, so only the ones explicitly set in this file are enabled. + DisabledByDefault: true + SuggestExtensions: false Exclude: - - 'bin/*' - - 'db/*' + - '**/tmp/**/*' + - '**/templates/**/*' + - '**/vendor/**/*' + - '**/node_modules/**/*' + - 'bin/*' + - db/migrate/*.rb + - db/schema.rb + - '.vs/**/*' -Lint: +Performance: Exclude: - - 'bin/*' - - 'db/*' + - '**/sprc/**/*' -Layout: - Exclude: - - 'bin/*' - - 'db/*' \ No newline at end of file +# Prefer assert_not over assert ! +Rails/AssertNot: + Include: + - '**/test/**/*' + +# Prefer assert_not_x over refute_x +Rails/RefuteMethods: + Include: + - '**/test/**/*' + +Rails/IndexBy: + Enabled: true + +Rails/IndexWith: + Enabled: true + +# Prefer &&/|| over and/or. +Style/AndOr: + Enabled: true + +# Align `when` with `case`. +Layout/CaseIndentation: + Enabled: true + +Layout/ClosingHeredocIndentation: + Enabled: true + +Layout/ClosingParenthesisIndentation: + Enabled: true + +# Align comments with method definitions. +Layout/CommentIndentation: + Enabled: true + +Layout/ElseAlignment: + Enabled: true + +# Align `end` with the matching keyword or starting expression except for +# assignments, where it should be aligned with the LHS. +Layout/EndAlignment: + Enabled: true + EnforcedStyleAlignWith: variable + AutoCorrect: true + +Layout/EndOfLine: + Enabled: true + +Layout/EmptyLineAfterMagicComment: + Enabled: true + +Layout/EmptyLinesAroundAccessModifier: + Enabled: true + EnforcedStyle: only_before + +Layout/EmptyLinesAroundBlockBody: + Enabled: true + +# In a regular class definition, no empty lines around the body. +Layout/EmptyLinesAroundClassBody: + Enabled: true + +# In a regular method definition, no empty lines around the body. +Layout/EmptyLinesAroundMethodBody: + Enabled: true + +# In a regular module definition, no empty lines around the body. +Layout/EmptyLinesAroundModuleBody: + Enabled: true + +# Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }. +Style/HashSyntax: + Enabled: true + +# Method definitions after `private` or `protected` isolated calls need one +# extra level of indentation. +Layout/IndentationConsistency: + Enabled: true + EnforcedStyle: normal + +# Two spaces, no tabs (for indentation). +Layout/IndentationWidth: + Enabled: true + Width: 2 + +Layout/LeadingCommentSpace: + Enabled: true + +Layout/SpaceAfterColon: + Enabled: true + +Layout/SpaceAfterComma: + Enabled: true + +Layout/SpaceAfterSemicolon: + Enabled: true + +Layout/SpaceAroundEqualsInParameterDefault: + Enabled: true + +Layout/SpaceAroundKeyword: + Enabled: true + +Layout/SpaceAroundOperators: + Enabled: true + +Layout/SpaceBeforeComma: + Enabled: true + +Layout/SpaceBeforeComment: + Enabled: true + +Layout/SpaceBeforeFirstArg: + Enabled: true + +Style/DefWithParentheses: + Enabled: true + +# Defining a method with parameters needs parentheses. +Style/MethodDefParentheses: + Enabled: true + +Style/ExplicitBlockArgument: + Enabled: true + +Style/FrozenStringLiteralComment: + Enabled: true + EnforcedStyle: always + +Style/RedundantFreeze: + Enabled: true + +# Use `foo {}` not `foo{}`. +Layout/SpaceBeforeBlockBraces: + Enabled: true + +# Use `foo { bar }` not `foo {bar}`. +Layout/SpaceInsideBlockBraces: + Enabled: true + EnforcedStyleForEmptyBraces: space + +# Use `{ a: 1 }` not `{a:1}`. +Layout/SpaceInsideHashLiteralBraces: + Enabled: true + +Layout/SpaceInsideParens: + Enabled: true + +# Check quotes usage according to lint rule below. +Style/StringLiterals: + Enabled: true + EnforcedStyle: double_quotes + +# Detect hard tabs, no hard tabs. +Layout/IndentationStyle: + Enabled: true + +# Empty lines should not have any spaces. +Layout/TrailingEmptyLines: + Enabled: true + +# No trailing whitespace. +Layout/TrailingWhitespace: + Enabled: true + +# Use quotes for string literals when they are enough. +Style/RedundantPercentQ: + Enabled: true + +Lint/AmbiguousOperator: + Enabled: true + +Lint/AmbiguousRegexpLiteral: + Enabled: true + +Lint/DuplicateRequire: + Enabled: true + +Lint/DuplicateMethods: + Enabled: true + +Lint/ErbNewArguments: + Enabled: true + +# Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg. +Lint/RequireParentheses: + Enabled: true + +Lint/RedundantStringCoercion: + Enabled: true + +Lint/UriEscapeUnescape: + Enabled: true + +Lint/UselessAssignment: + Enabled: true + +Lint/DeprecatedClassMethods: + Enabled: true + +Style/ParenthesesAroundCondition: + Enabled: true + +Style/HashTransformKeys: + Enabled: true + +Style/HashTransformValues: + Enabled: true + +Style/RedundantBegin: + Enabled: true + +Style/RedundantReturn: + Enabled: true + AllowMultipleReturnValues: true + +Style/RedundantRegexpEscape: + Enabled: true + +Style/Semicolon: + Enabled: true + AllowAsExpressionSeparator: true + +# Prefer Foo.method over Foo::method +Style/ColonMethodCall: + Enabled: true + +Style/TrivialAccessors: + Enabled: true + +Performance/BindCall: + Enabled: true + +Performance/FlatMap: + Enabled: true + +Performance/MapCompact: + Enabled: true + +Performance/SelectMap: + Enabled: true + +Performance/RedundantMerge: + Enabled: true + +Performance/StartWith: + Enabled: true + +Performance/EndWith: + Enabled: true + +Performance/RegexpMatch: + Enabled: true + +Performance/ReverseEach: + Enabled: true + +Performance/StringReplacement: + Enabled: true + +Performance/UnfreezeString: + Enabled: true + +Performance/DeletePrefix: + Enabled: true + +Performance/DeleteSuffix: + Enabled: true \ No newline at end of file diff --git a/.ruby-version b/.ruby-version index 75a22a26a..406ebcbd9 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.0.3 +3.2.7 diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 000000000..9c68d12cf --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +ruby 3.2.7 diff --git a/Gemfile b/Gemfile index e6c73b25d..ec0e274ec 100644 --- a/Gemfile +++ b/Gemfile @@ -1,45 +1,46 @@ # frozen_string_literal: true -source 'https://rubygems.org' +source "https://rubygems.org" git_source(:github) { |repo| "https://github.com/#{repo}.git" } -ruby '3.0.3' +ruby "3.2.7" -gem 'actionpack', '= 6.1.4' - -gem 'activesupport', '= 6.1.4' # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" -gem 'rails', '~> 6.1.4' +gem "rails", "~> 7.1.1" -gem 'pg' +gem "pg" # The original asset pipeline for Rails [https://github.com/rails/sprockets-rails] -gem 'sprockets', '~> 3.7.2' -gem 'sprockets-rails' +gem "sprockets", "~> 3.7.2" +gem "sprockets-rails" # Use the Puma web server [https://github.com/puma/puma] -gem 'puma', '~> 5.0' +gem "puma", "~> 5.0" # Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails] # gem "importmap-rails" # Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev] -gem 'turbo-rails' +gem "turbo-rails" -gem 'figaro' +gem "figaro" # Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev] # gem "stimulus-rails" # Bootstrap -gem 'bootstrap', '~> 5.0.0' +gem "bootstrap", "~> 5.0.0" # Use SCSS for stylesheets -gem 'sass-rails', '~> 5.0' +gem "sass-rails", "~> 5.0" # Use for Google Oauth 2.0 -gem 'omniauth-google-oauth2' +gem "omniauth-google-oauth2" + +# Use for Canvas authentication +gem "omniauth" +gem "omniauth-oauth2" # Build JSON APIs with ease [https://github.com/rails/jbuilder] -gem 'jbuilder', '~> 2.10.1' +gem "jbuilder", "~> 2.10.1" # Use Redis adapter to run Action Cable in production # gem "redis", "~> 4.0" @@ -51,52 +52,65 @@ gem 'jbuilder', '~> 2.10.1' # gem "bcrypt", "~> 3.1.7" # Windows does not include zoneinfo files, so bundle the tzinfo-data gem -gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby] +gem "tzinfo-data", platforms: %i[mingw mswin x64_mingw jruby] # Reduces boot times through caching; required in config/boot.rb -gem 'bootsnap', require: false +gem "bootsnap", require: false # Use Sass to process CSS # gem "sassc-rails" # Gemfile -gem 'omniauth-rails_csrf_protection' +gem "omniauth-rails_csrf_protection" # Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] -# gem "image_processing", "~> 1.2" +gem "image_processing", "~> 1.2" + +# Use for New Relic APM +gem "newrelic_rpm" group :development, :test do # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem - gem 'byebug' - gem 'debug', platforms: %i[mri mingw x64_mingw] - gem 'factory_bot_rails' - gem 'guard-rspec' - gem 'rspec-rails', '~> 4.1.2' - gem 'sqlite3' + gem "byebug" + gem "debug", platforms: %i[mri mingw x64_mingw] + gem "factory_bot_rails" + gem "guard-rspec" + gem "rspec-rails", "~> 4.1.2" + gem "sqlite3", "~>1.7.0" # is this ok? end group :development do # Use console on exceptions pages [https://github.com/rails/web-console] - gem 'web-console', '~> 3.7.0' + gem "web-console", "~> 4.2.0" # Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler] # gem "rack-mini-profiler" # Speed up commands on slow machines / big apps [https://github.com/rails/spring] # gem "spring" +end - gem 'rubocop', require: false +group :linters do + gem "rubocop", require: false + gem "rubocop-rails", require: false + gem "rubocop-performance", require: false + gem "rubocop-rspec", require: false end group :test do # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing] - gem 'capybara', '~> 3.0' - gem 'cucumber', '~> 3.0.0' - gem 'cucumber-rails', '~> 1.7.0', require: false - gem 'cucumber-rails-training-wheels' # basic imperative step defs like "Then I should see..." - gem 'database_cleaner', '~> 1.8.5' # required by Cucumber - gem 'rails-controller-testing' - gem 'selenium-webdriver' - gem 'simplecov', '~> 0.21.2', require: false - gem 'simplecov_json_formatter', '~> 0.1.2', require: false - gem 'timecop' - gem 'webdrivers' + gem "capybara", "~> 3.0" + gem "cucumber", "~> 3.2.0" + gem "cucumber-rails", "~> 2.6.1", require: false + gem "cucumber-rails-training-wheels" # basic imperative step defs like "Then I should see..." + gem "database_cleaner", "~> 1.8.5" # required by Cucumber + gem "rails-controller-testing", github: "rails/rails-controller-testing" + gem "selenium-webdriver" + gem "simplecov", "~> 0.22.0", require: false + gem "simplecov-html", "~> 0.13.1", require: false + gem "simplecov_json_formatter", "~> 0.1.4", require: false + gem "docile", "~> 1.4", ">= 1.4.1", require: false + gem "timecop" + gem "webdrivers" + gem "webmock" + gem "axe-core-rspec" + gem "axe-core-cucumber" end diff --git a/Gemfile.lock b/Gemfile.lock index 1e2b3e629..83870e1a0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,147 +1,212 @@ +GIT + remote: https://github.com/rails/rails-controller-testing.git + revision: c203673f8011a7cdc2a8edf995ae6b3eec3417ca + specs: + rails-controller-testing (1.0.5) + actionpack (>= 5.0.1.rc1) + actionview (>= 5.0.1.rc1) + activesupport (>= 5.0.1.rc1) + GEM remote: https://rubygems.org/ specs: - actioncable (6.1.4) - actionpack (= 6.1.4) - activesupport (= 6.1.4) + actioncable (7.1.5.1) + actionpack (= 7.1.5.1) + activesupport (= 7.1.5.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.4) - actionpack (= 6.1.4) - activejob (= 6.1.4) - activerecord (= 6.1.4) - activestorage (= 6.1.4) - activesupport (= 6.1.4) + zeitwerk (~> 2.6) + actionmailbox (7.1.5.1) + actionpack (= 7.1.5.1) + activejob (= 7.1.5.1) + activerecord (= 7.1.5.1) + activestorage (= 7.1.5.1) + activesupport (= 7.1.5.1) mail (>= 2.7.1) - actionmailer (6.1.4) - actionpack (= 6.1.4) - actionview (= 6.1.4) - activejob (= 6.1.4) - activesupport (= 6.1.4) + net-imap + net-pop + net-smtp + actionmailer (7.1.5.1) + actionpack (= 7.1.5.1) + actionview (= 7.1.5.1) + activejob (= 7.1.5.1) + activesupport (= 7.1.5.1) mail (~> 2.5, >= 2.5.4) - rails-dom-testing (~> 2.0) - actionpack (6.1.4) - actionview (= 6.1.4) - activesupport (= 6.1.4) - rack (~> 2.0, >= 2.0.9) + net-imap + net-pop + net-smtp + rails-dom-testing (~> 2.2) + actionpack (7.1.5.1) + actionview (= 7.1.5.1) + activesupport (= 7.1.5.1) + nokogiri (>= 1.8.5) + racc + rack (>= 2.2.4) + rack-session (>= 1.0.1) rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.4) - actionpack (= 6.1.4) - activerecord (= 6.1.4) - activestorage (= 6.1.4) - activesupport (= 6.1.4) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + actiontext (7.1.5.1) + actionpack (= 7.1.5.1) + activerecord (= 7.1.5.1) + activestorage (= 7.1.5.1) + activesupport (= 7.1.5.1) + globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (6.1.4) - activesupport (= 6.1.4) + actionview (7.1.5.1) + activesupport (= 7.1.5.1) builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (6.1.4) - activesupport (= 6.1.4) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (7.1.5.1) + activesupport (= 7.1.5.1) globalid (>= 0.3.6) - activemodel (6.1.4) - activesupport (= 6.1.4) - activerecord (6.1.4) - activemodel (= 6.1.4) - activesupport (= 6.1.4) - activestorage (6.1.4) - actionpack (= 6.1.4) - activejob (= 6.1.4) - activerecord (= 6.1.4) - activesupport (= 6.1.4) - marcel (~> 1.0.0) - mini_mime (>= 1.1.0) - activesupport (6.1.4) + activemodel (7.1.5.1) + activesupport (= 7.1.5.1) + activerecord (7.1.5.1) + activemodel (= 7.1.5.1) + activesupport (= 7.1.5.1) + timeout (>= 0.4.0) + activestorage (7.1.5.1) + actionpack (= 7.1.5.1) + activejob (= 7.1.5.1) + activerecord (= 7.1.5.1) + activesupport (= 7.1.5.1) + marcel (~> 1.0) + activesupport (7.1.5.1) + base64 + benchmark (>= 0.3) + bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb i18n (>= 1.6, < 2) + logger (>= 1.4.2) minitest (>= 5.1) + mutex_m + securerandom (>= 0.3) tzinfo (~> 2.0) - zeitwerk (~> 2.3) - addressable (2.8.0) - public_suffix (>= 2.0.2, < 5.0) - ast (2.4.2) - autoprefixer-rails (10.4.2.0) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + ast (2.4.3) + autoprefixer-rails (10.4.19.0) execjs (~> 2) - backports (3.23.0) + axe-core-api (4.10.2) + dumb_delegator + ostruct + virtus + axe-core-cucumber (4.10.2) + axe-core-api (= 4.10.2) + dumb_delegator + ostruct + virtus + axe-core-rspec (4.10.2) + axe-core-api (= 4.10.2) + dumb_delegator + ostruct + virtus + axiom-types (0.1.1) + descendants_tracker (~> 0.0.4) + ice_nine (~> 0.11.0) + thread_safe (~> 0.3, >= 0.3.1) + backports (3.25.0) + base64 (0.2.0) + benchmark (0.4.0) + bigdecimal (3.1.9) bindex (0.8.1) - bootsnap (1.10.3) + bootsnap (1.18.4) msgpack (~> 1.2) bootstrap (5.0.2) autoprefixer-rails (>= 9.1.0) popper_js (>= 2.9.2, < 3) sassc-rails (>= 2.0.0) - builder (3.2.4) + builder (3.3.0) byebug (11.1.3) - capybara (3.36.0) + capybara (3.40.0) addressable matrix mini_mime (>= 0.1.3) - nokogiri (~> 1.8) + nokogiri (~> 1.11) rack (>= 1.6.0) rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) - childprocess (4.1.0) coderay (1.1.3) - concurrent-ruby (1.1.9) + coercible (1.0.0) + descendants_tracker (~> 0.0.1) + concurrent-ruby (1.3.5) + connection_pool (2.5.0) + crack (1.0.0) + bigdecimal + rexml crass (1.0.6) - cucumber (3.0.2) + cucumber (3.2.0) builder (>= 2.1.2) - cucumber-core (~> 3.0.0) - cucumber-expressions (~> 4.0.3) + cucumber-core (~> 3.2.0) + cucumber-expressions (~> 6.0.1) cucumber-wire (~> 0.0.1) diff-lcs (~> 1.3) - gherkin (~> 4.0) + gherkin (~> 5.1.0) multi_json (>= 1.7.5, < 2.0) multi_test (>= 0.1.2) - cucumber-core (3.0.0) + cucumber-core (3.2.1) backports (>= 3.8.0) - cucumber-tag_expressions (>= 1.0.1) - gherkin (>= 4.1.3) - cucumber-expressions (4.0.4) - cucumber-rails (1.7.0) - capybara (>= 2.3.0, < 4) - cucumber (>= 3.0.2, < 4) - mime-types (>= 1.17, < 4) - nokogiri (~> 1.8) - railties (>= 4.2, < 7) + cucumber-tag_expressions (~> 1.1.0) + gherkin (~> 5.0) + cucumber-expressions (6.0.1) + cucumber-rails (2.6.1) + capybara (>= 2.18, < 4) + cucumber (>= 3.2, < 9) + mime-types (~> 3.3) + nokogiri (~> 1.10) + railties (>= 5.0, < 8) + rexml (~> 3.0) + webrick (~> 1.7) cucumber-rails-training-wheels (1.0.0) cucumber-rails (>= 1.1.1) - cucumber-tag_expressions (2.0.2) + cucumber-tag_expressions (1.1.1) cucumber-wire (0.0.1) database_cleaner (1.8.5) - debug (1.4.0) - irb (>= 1.3.6) - reline (>= 0.2.7) - diff-lcs (1.5.0) - docile (1.4.0) - erubi (1.10.0) - execjs (2.8.1) - factory_bot (6.2.0) - activesupport (>= 5.0.0) - factory_bot_rails (6.2.0) - factory_bot (~> 6.2.0) + date (3.4.1) + debug (1.10.0) + irb (~> 1.10) + reline (>= 0.3.8) + descendants_tracker (0.0.4) + thread_safe (~> 0.3, >= 0.3.1) + diff-lcs (1.6.0) + docile (1.4.1) + drb (2.2.1) + dumb_delegator (1.1.0) + erubi (1.13.1) + execjs (2.10.0) + factory_bot (6.5.1) + activesupport (>= 6.1.0) + factory_bot_rails (6.4.4) + factory_bot (~> 6.5) railties (>= 5.0.0) - faraday (2.2.0) - faraday-net_http (~> 2.0) - ruby2_keywords (>= 0.0.4) - faraday-net_http (2.0.1) - ffi (1.15.5) + faraday (2.12.2) + faraday-net_http (>= 2.0, < 3.5) + json + logger + faraday-net_http (3.4.0) + net-http (>= 0.5.0) + ffi (1.17.1-arm64-darwin) + ffi (1.17.1-x86_64-linux-gnu) figaro (1.2.0) thor (>= 0.14.0, < 2) formatador (1.1.0) - gherkin (4.1.3) - globalid (1.0.0) - activesupport (>= 5.0) - guard (2.18.0) + gherkin (5.1.0) + globalid (1.2.1) + activesupport (>= 6.1) + guard (2.19.1) formatador (>= 0.2.4) listen (>= 2.7, < 4.0) + logger (~> 1.6) lumberjack (>= 1.0.12, < 2.0) nenv (~> 0.1) notiffany (~> 0.0) + ostruct (~> 0.6) pry (>= 0.13.0) shellany (~> 0.0) thor (>= 0.18.1) @@ -150,134 +215,183 @@ GEM guard (~> 2.1) guard-compat (~> 1.1) rspec (>= 2.99.0, < 4.0) + hashdiff (1.1.2) hashie (5.0.0) - i18n (1.10.0) + i18n (1.14.7) concurrent-ruby (~> 1.0) - io-console (0.5.11) - irb (1.4.1) - reline (>= 0.3.0) + ice_nine (0.11.2) + image_processing (1.14.0) + mini_magick (>= 4.9.5, < 6) + ruby-vips (>= 2.0.17, < 3) + io-console (0.8.0) + irb (1.15.1) + pp (>= 0.6.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) jbuilder (2.10.2) activesupport (>= 5.0.0) - jwt (2.3.0) - listen (3.7.1) + json (2.10.2) + jwt (2.10.1) + base64 + language_server-protocol (3.17.0.4) + lint_roller (1.1.0) + listen (3.9.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) - loofah (2.14.0) + logger (1.6.6) + loofah (2.24.0) crass (~> 1.0.2) - nokogiri (>= 1.5.9) - lumberjack (1.2.8) - mail (2.7.1) + nokogiri (>= 1.12.0) + lumberjack (1.2.10) + mail (2.8.1) mini_mime (>= 0.1.1) - marcel (1.0.2) + net-imap + net-pop + net-smtp + marcel (1.0.4) matrix (0.4.2) - method_source (1.0.0) - mime-types (3.4.1) + method_source (1.1.0) + mime-types (3.6.1) + logger mime-types-data (~> 3.2015) - mime-types-data (3.2022.0105) - mini_mime (1.1.2) - minitest (5.15.0) - msgpack (1.4.4) + mime-types-data (3.2025.0318) + mini_magick (5.2.0) + benchmark + logger + mini_mime (1.1.5) + minitest (5.25.5) + msgpack (1.8.0) multi_json (1.15.0) - multi_test (0.1.2) - multi_xml (0.6.0) + multi_test (1.1.0) + multi_xml (0.7.1) + bigdecimal (~> 3.1) + mutex_m (0.3.0) nenv (0.3.0) - nio4r (2.5.8) - nokogiri (1.13.1-x86_64-darwin) + net-http (0.6.0) + uri + net-imap (0.5.6) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-smtp (0.5.1) + net-protocol + newrelic_rpm (9.17.0) + nio4r (2.7.4) + nokogiri (1.18.5-arm64-darwin) racc (~> 1.4) - nokogiri (1.13.1-x86_64-linux) + nokogiri (1.18.5-x86_64-linux-gnu) racc (~> 1.4) notiffany (0.1.3) nenv (~> 0.1) shellany (~> 0.0) - oauth2 (1.4.9) + oauth2 (2.0.9) faraday (>= 0.17.3, < 3.0) jwt (>= 1.0, < 3.0) - multi_json (~> 1.3) multi_xml (~> 0.5) - rack (>= 1.2, < 3) - omniauth (2.0.4) + rack (>= 1.2, < 4) + snaky_hash (~> 2.0) + version_gem (~> 1.1) + omniauth (2.1.3) hashie (>= 3.4.6) - rack (>= 1.6.2, < 3) + rack (>= 2.2.3) rack-protection - omniauth-google-oauth2 (1.0.1) - jwt (>= 2.0) - oauth2 (~> 1.1) + omniauth-google-oauth2 (1.2.1) + jwt (>= 2.9.2) + oauth2 (~> 2.0) omniauth (~> 2.0) - omniauth-oauth2 (~> 1.7.1) - omniauth-oauth2 (1.7.2) - oauth2 (~> 1.4) - omniauth (>= 1.9, < 3) - omniauth-rails_csrf_protection (1.0.1) + omniauth-oauth2 (~> 1.8) + omniauth-oauth2 (1.8.0) + oauth2 (>= 1.4, < 3) + omniauth (~> 2.0) + omniauth-rails_csrf_protection (1.0.2) actionpack (>= 4.2) omniauth (~> 2.0) - parallel (1.22.1) - parser (3.1.2.0) + ostruct (0.6.1) + parallel (1.26.3) + parser (3.3.7.1) ast (~> 2.4.1) - pg (1.3.2) - popper_js (2.9.3) - pry (0.14.1) + racc + pg (1.5.9) + popper_js (2.11.8) + pp (0.6.2) + prettyprint + prettyprint (0.2.0) + pry (0.15.2) coderay (~> 1.1) method_source (~> 1.0) - public_suffix (4.0.6) - puma (5.6.1) + psych (5.2.3) + date + stringio + public_suffix (6.0.1) + puma (5.6.9) nio4r (~> 2.0) - racc (1.6.0) - rack (2.2.3) - rack-protection (2.2.0) - rack - rack-test (1.1.0) - rack (>= 1.0, < 3) - rails (6.1.4) - actioncable (= 6.1.4) - actionmailbox (= 6.1.4) - actionmailer (= 6.1.4) - actionpack (= 6.1.4) - actiontext (= 6.1.4) - actionview (= 6.1.4) - activejob (= 6.1.4) - activemodel (= 6.1.4) - activerecord (= 6.1.4) - activestorage (= 6.1.4) - activesupport (= 6.1.4) + racc (1.8.1) + rack (2.2.13) + rack-protection (3.2.0) + base64 (>= 0.1.0) + rack (~> 2.2, >= 2.2.4) + rack-session (1.0.2) + rack (< 3) + rack-test (2.2.0) + rack (>= 1.3) + rackup (1.0.1) + rack (< 3) + webrick + rails (7.1.5.1) + actioncable (= 7.1.5.1) + actionmailbox (= 7.1.5.1) + actionmailer (= 7.1.5.1) + actionpack (= 7.1.5.1) + actiontext (= 7.1.5.1) + actionview (= 7.1.5.1) + activejob (= 7.1.5.1) + activemodel (= 7.1.5.1) + activerecord (= 7.1.5.1) + activestorage (= 7.1.5.1) + activesupport (= 7.1.5.1) bundler (>= 1.15.0) - railties (= 6.1.4) - sprockets-rails (>= 2.0.0) - rails-controller-testing (1.0.5) - actionpack (>= 5.0.1.rc1) - actionview (>= 5.0.1.rc1) - activesupport (>= 5.0.1.rc1) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) + railties (= 7.1.5.1) + rails-dom-testing (2.2.0) + activesupport (>= 5.0.0) + minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.4.2) - loofah (~> 2.3) - railties (6.1.4) - actionpack (= 6.1.4) - activesupport (= 6.1.4) - method_source - rake (>= 0.13) - thor (~> 1.0) + rails-html-sanitizer (1.6.2) + loofah (~> 2.21) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) + railties (7.1.5.1) + actionpack (= 7.1.5.1) + activesupport (= 7.1.5.1) + irb + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + zeitwerk (~> 2.6) rainbow (3.1.1) - rake (13.0.6) - rb-fsevent (0.11.1) - rb-inotify (0.10.1) + rake (13.2.1) + rb-fsevent (0.11.2) + rb-inotify (0.11.1) ffi (~> 1.0) - regexp_parser (2.2.0) - reline (0.3.1) + rdoc (6.12.0) + psych (>= 4.0.0) + regexp_parser (2.10.0) + reline (0.6.0) io-console (~> 0.5) - rexml (3.2.5) - rspec (3.11.0) - rspec-core (~> 3.11.0) - rspec-expectations (~> 3.11.0) - rspec-mocks (~> 3.11.0) - rspec-core (3.11.0) - rspec-support (~> 3.11.0) - rspec-expectations (3.11.0) + rexml (3.4.1) + rspec (3.13.0) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.3) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.3) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.11.0) - rspec-mocks (3.11.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.2) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.11.0) + rspec-support (~> 3.13.0) rspec-rails (4.1.2) actionpack (>= 4.2) activesupport (>= 4.2) @@ -286,21 +400,38 @@ GEM rspec-expectations (~> 3.10) rspec-mocks (~> 3.10) rspec-support (~> 3.10) - rspec-support (3.11.0) - rubocop (1.27.0) + rspec-support (3.13.2) + rubocop (1.74.0) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) parallel (~> 1.10) - parser (>= 3.1.0.0) + parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 1.8, < 3.0) - rexml - rubocop-ast (>= 1.16.0, < 2.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.38.0, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.17.0) - parser (>= 3.1.1.0) - ruby-progressbar (1.11.0) - ruby2_keywords (0.0.5) - rubyzip (2.3.2) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.40.0) + parser (>= 3.3.1.0) + rubocop-performance (1.24.0) + lint_roller (~> 1.1) + rubocop (>= 1.72.1, < 2.0) + rubocop-ast (>= 1.38.0, < 2.0) + rubocop-rails (2.30.3) + activesupport (>= 4.2.0) + lint_roller (~> 1.1) + rack (>= 1.1) + rubocop (>= 1.72.1, < 2.0) + rubocop-ast (>= 1.38.0, < 2.0) + rubocop-rspec (3.5.0) + lint_roller (~> 1.1) + rubocop (~> 1.72, >= 1.72.1) + ruby-progressbar (1.13.0) + ruby-vips (2.2.3) + ffi (~> 1.12) + logger + rubyzip (2.4.1) sass (3.7.4) sass-listen (~> 4.0.0) sass-listen (4.0.0) @@ -320,94 +451,127 @@ GEM sprockets (> 3.0) sprockets-rails tilt - selenium-webdriver (4.1.0) - childprocess (>= 0.5, < 5.0) + securerandom (0.4.1) + selenium-webdriver (4.10.0) rexml (~> 3.2, >= 3.2.5) - rubyzip (>= 1.2.2) + rubyzip (>= 1.2.2, < 3.0) + websocket (~> 1.0) shellany (0.0.1) - simplecov (0.21.2) + simplecov (0.22.0) docile (~> 1.1) simplecov-html (~> 0.11) simplecov_json_formatter (~> 0.1) - simplecov-html (0.12.3) + simplecov-html (0.13.1) simplecov_json_formatter (0.1.4) - sprockets (3.7.2) + snaky_hash (2.0.1) + hashie + version_gem (~> 1.1, >= 1.1.1) + sprockets (3.7.5) + base64 concurrent-ruby (~> 1.0) rack (> 1, < 3) - sprockets-rails (3.4.2) - actionpack (>= 5.2) - activesupport (>= 5.2) + sprockets-rails (3.5.2) + actionpack (>= 6.1) + activesupport (>= 6.1) sprockets (>= 3.0.0) - sqlite3 (1.4.2) - thor (1.2.1) - tilt (2.0.10) - timecop (0.9.5) - turbo-rails (1.0.1) - actionpack (>= 6.0.0) - railties (>= 6.0.0) - tzinfo (2.0.4) + sqlite3 (1.7.3-arm64-darwin) + sqlite3 (1.7.3-x86_64-linux) + stringio (3.1.5) + thor (1.3.2) + thread_safe (0.3.6) + tilt (2.6.0) + timecop (0.9.10) + timeout (0.4.3) + turbo-rails (2.0.13) + actionpack (>= 7.1.0) + railties (>= 7.1.0) + tzinfo (2.0.6) concurrent-ruby (~> 1.0) - unicode-display_width (2.1.0) - web-console (3.7.0) - actionview (>= 5.0) - activemodel (>= 5.0) + unicode-display_width (3.1.4) + unicode-emoji (~> 4.0, >= 4.0.4) + unicode-emoji (4.0.4) + uri (1.0.3) + version_gem (1.1.6) + virtus (2.0.0) + axiom-types (~> 0.1) + coercible (~> 1.0) + descendants_tracker (~> 0.0, >= 0.0.3) + web-console (4.2.1) + actionview (>= 6.0.0) + activemodel (>= 6.0.0) bindex (>= 0.4.0) - railties (>= 5.0) - webdrivers (5.0.0) + railties (>= 6.0.0) + webdrivers (5.3.1) nokogiri (~> 1.6) rubyzip (>= 1.3.0) - selenium-webdriver (~> 4.0) - websocket-driver (0.7.5) + selenium-webdriver (~> 4.0, < 4.11) + webmock (3.25.1) + addressable (>= 2.8.0) + crack (>= 0.3.2) + hashdiff (>= 0.4.0, < 2.0.0) + webrick (1.9.1) + websocket (1.2.11) + websocket-driver (0.7.7) + base64 websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.5.4) + zeitwerk (2.7.2) PLATFORMS - x86_64-darwin-20 - x86_64-darwin-21 + arm64-darwin-24 x86_64-linux DEPENDENCIES - actionpack (= 6.1.4) - activesupport (= 6.1.4) + axe-core-cucumber + axe-core-rspec bootsnap bootstrap (~> 5.0.0) byebug capybara (~> 3.0) - cucumber (~> 3.0.0) - cucumber-rails (~> 1.7.0) + cucumber (~> 3.2.0) + cucumber-rails (~> 2.6.1) cucumber-rails-training-wheels database_cleaner (~> 1.8.5) debug + docile (~> 1.4, >= 1.4.1) factory_bot_rails figaro guard-rspec + image_processing (~> 1.2) jbuilder (~> 2.10.1) + newrelic_rpm + omniauth omniauth-google-oauth2 + omniauth-oauth2 omniauth-rails_csrf_protection pg puma (~> 5.0) - rails (~> 6.1.4) - rails-controller-testing + rails (~> 7.1.1) + rails-controller-testing! rspec-rails (~> 4.1.2) rubocop + rubocop-performance + rubocop-rails + rubocop-rspec sass-rails (~> 5.0) selenium-webdriver - simplecov (~> 0.21.2) - simplecov_json_formatter (~> 0.1.2) + simplecov (~> 0.22.0) + simplecov-html (~> 0.13.1) + simplecov_json_formatter (~> 0.1.4) sprockets (~> 3.7.2) sprockets-rails - sqlite3 + sqlite3 (~> 1.7.0) timecop turbo-rails tzinfo-data - web-console (~> 3.7.0) + web-console (~> 4.2.0) webdrivers + webmock RUBY VERSION - ruby 3.0.3p157 + ruby 3.2.7p253 BUNDLED WITH - 2.3.7 + 2.4.19 diff --git a/Guardfile b/Guardfile index 5a83809c8..e35b7a4b5 100644 --- a/Guardfile +++ b/Guardfile @@ -26,8 +26,8 @@ # * zeus: 'zeus rspec' (requires the server to be started separately) # * 'just' rspec: 'rspec' -guard :rspec, cmd: 'bundle exec rspec' do - require 'guard/rspec/dsl' +guard :rspec, cmd: "bundle exec rspec" do + require "guard/rspec/dsl" dsl = Guard::RSpec::Dsl.new(self) # Feel free to open issues for suggestions and improvements @@ -67,6 +67,6 @@ guard :rspec, cmd: 'bundle exec rspec' do # Turnip features and steps watch(%r{^spec/acceptance/(.+)\.feature$}) watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) do |m| - Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance' + Dir[File.join("**/#{m[1]}.feature")][0] || "spec/acceptance" end end diff --git a/README.md b/README.md index 022e5a5c1..47815c1a1 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,119 @@ -[![Bluejay Dashboard](https://img.shields.io/badge/Bluejay-Dashboard_)](http://dashboard.bluejay.governify.io/dashboard/script/dashboardLoader.js?dashboardURL=https://reporter.bluejay.governify.io/api/v4/dashboards/tpa-CS169L-22-GH-ryan-garay89_berkeley-reentry-student-program/main) +# Berkeley ReEntry Student Program -![build](https://github.com/ryan-garay89/berkeley-reentry-student-program/actions/workflows/main.yml/badge.svg) +[![build](https://github.com/cs169/berkeley-reentry-student-program/actions/workflows/main.yml/badge.svg)](https://github.com/cs169/berkeley-reentry-student-program/actions/workflows/main.yml) +[![rubocop](https://github.com/cs169/berkeley-reentry-student-program/actions/workflows/rubocop.yml/badge.svg)](https://github.com/cs169/berkeley-reentry-student-program/actions/workflows/rubocop.yml) +[![test coverage](https://api.codeclimate.com/v1/badges/c34db83045f2d3756e29/test_coverage)](https://codeclimate.com/github/cs169/berkeley-reentry-student-program/test_coverage) +[![maintainability](https://api.codeclimate.com/v1/badges/c34db83045f2d3756e29/maintainability)](https://codeclimate.com/github/cs169/berkeley-reentry-student-program/maintainability) +[![pivotal tracker](https://user-images.githubusercontent.com/67244883/154180887-f803124e-0156-4322-899d-ba475139d60d.png)](https://www.pivotaltracker.com/n/projects/2553425) - +## Description +The Berkeley ReEntry Student Program is an application developed by UC Berkeley's CS169L students for the ReEntry Program at UC Berkeley. The application provides an easy-to-use interface for ReEntry students to sign-up for and use resources provided by UC Berkeley's ReEntry program. - +## Project Status +**Working** +- Community space check-in +- Appointments +- Scholarships +- Courses +- Events + +## Source/Golden Repo +This repo is forked from https://github.com/saasbook/berkeley-reentry-student-program + +## Heroku Deployment + +### Staging +https://sp25-03-reentry-181cb67be4ca.herokuapp.com/ +### Production +https://reentry-student-program-b0fde1ef8035.herokuapp.com/ - ## First-Time Setup Instructions 1. Clone the repository locally and then make your own branch!! -2. Install Ruby version 3.0.3, and switch to that version using `rvm use 3.0.3` -3. Run `bundle install --without production` -4. Run `rake db:schema:load` & `rake db:migrate` -5. Follow [these instructions](https://devcenter.heroku.com/articles/creating-apps) to create & setup a new Heroku app on the CLI - - You must have PostgreSQL installed locally to run the rails server. Then, you must start the server via the command line - - **For Mac**: Run `brew services start postgresql` - - **For Windows**: Run `pg_ctl -D "C:\Program Files\PostgreSQL\9.6\data" start` -6. Our code requires 4 environment variables to work correctly in production & local environments. - - _For local development_: You must set a non-empty string for the environment variables `ADMIN`, `STAFF`, `GOOGLE_CLIENT_ID`, and `GOOGLE_CLIENT_SECRET - - **For Mac**: With Terminal open, run `open ~/.bash_profile` - - At the bottom of the text file, add the following: `export ADMINS=string` & `export STAFF=string` where string is a comma-separated list of Berkeley email addresses (these do not have to be real); i.e. `person@berkeley.edu,person2@berkeley.edu` - Additionally, add `export GOOGLE_CLIENT_ID=some_value` & `export GOOGLE_CLIENT_SECRET=some_value` where some_value is some arbitrary string (these do not need to be valid to run the app locally, since google authentication is stubbed-out unless it is run on production). - - **For Windows**: follow [these instructions](https://devcenter.heroku.com/articles/creating-apps) to set environment variables - - Set `ADMINS` & `STAFF`, where the value of each is a comma-separated list of Berkeley email addresses (these do not have to be real); i.e. `person@berkeley.edu,person2@berkeley.edu`. - - Set `GOOGLE_CLIENT_ID` & `GOOGLE_CLIENT_SECRET` to some arbitrary string (these do not need to be valid to run the app locally, since google authentication is stubbed-out unless it is run on production). - - _For production_: Add the above environment variables to Heroku via the command line (assuming there is a Heroku app set up in your directory) - - Follow [these instructions](https://developers.google.com/adwords/api/docs/guides/authentication#webapp) (web app) to obtain a google client secret & a google client ID. For the callback URL, use https://*your-app-name*.herokuapp.com/auth/google_oauth2/ - - Use the command `heroku config:set VARIABLE=value` - - Add `ADMINS` & `STAFF` set to a comma-separated list of verified administrators and staff members for the app. For testing purposes, these variables can both be set to the string `”none”` - - Add `GOOGLE_CLIENT_SECRET` & `GOOGLE_CLIENT_ID` as provided by the instructions above. - - In order for the GitHub Actions build to pass, you must add a `CC_TEST_REPORTER_ID` as a repository secret on GitHub. To do this, first sign up for an account with codeclimate.com (quality, not velocity). Then, connect your repository and navigate to repo settings on the CodeClimate dashboard. Finally, copy the `test reporter ID` under the test coverage tab, and add it as a new repository secret under repository settings on GitHub. -7. That’s all! The app should now run on your local environment and any Heroku apps created from the codebase. +2. Install Ruby version 3.2.7 and switch to that version + - **For rvm**: + - `rvm install 3.2.7` + - `rvm use 3.2.7` +3. You must have PostgreSQL installed locally to run the rails server. + - **For Mac**: + - Install PostgreSQL: `brew install postgresql` + - Start PostgreSQL server: `brew services start postgresql` + - **For Windows**: + - Install PostgreSQL: download installer from [postgresql.org](https://www.postgresql.org/download/windows/) + - Start PostgreSQL server: `pg_ctl -D "C:\Program Files\PostgreSQL\9.6\data" start` + - **For Linux**: + - Install PostgreSQL: `sudo apt install postgresql postgresql-contrib` + - Start PostgreSQL server: `sudo service postgresql start` +4. Install the dependencies using bundle + - `bundle config set --local without 'production'` + - `bundle install` +5. Setup the database + - `rake db:create && rake db:schema:load && rake db:migrate && rake db:seed` +6. Follow [these instructions](https://devcenter.heroku.com/articles/creating-apps) to create & setup a new Heroku app on the CLI +7. Add database to the Heroku app. + - Go to the Resources page of your Heroku app. + - Add Heroku Postgres as an add-on service. + +## User Setup + +Our code requires 4 environment variables to work correctly in production & local environments. + +**Local Development**: You must set a non-empty string for the environment variables `ADMIN`, `STAFF`, `GOOGLE_CLIENT_ID`, and `GOOGLE_CLIENT_SECRET` + - **For Mac and Linux**: With Terminal open, run `open ~/.bash_profile` + - **Note**: If you are using a different shell, add the following exports to the appropriate shell profile e.g. for zsh, run `open ~/.zshrc` + - At the bottom of the text file, add the following: `export ADMINS=string` & `export STAFF=string` where string is a comma-separated list of Berkeley email addresses (these do not have to be real); i.e. `person@berkeley.edu,person2@berkeley.edu` + - Additionally, add `export GOOGLE_CLIENT_ID=some_value` & `export GOOGLE_CLIENT_SECRET=some_value` where some_value is some arbitrary string (these do not need to be valid to run the app locally, since google authentication is stubbed-out unless it is run on production). + - **For Windows**: follow [these instructions](https://devcenter.heroku.com/articles/creating-apps) to set environment variables + - Set `ADMINS` & `STAFF`, where the value of each is a comma-separated list of Berkeley email addresses (these do not have to be real); i.e. `person@berkeley.edu,person2@berkeley.edu`. + - Set `GOOGLE_CLIENT_ID` & `GOOGLE_CLIENT_SECRET` to some arbitrary string (these do not need to be valid to run the app locally, since google authentication is stubbed-out unless it is run on production). + +**For Production**: Add the `ADMIN`, `STAFF`, `GOOGLE_CLIENT_ID`, and `GOOGLE_CLIENT_SECRET` environment variables to Heroku via the command line (assuming there is a Heroku app set up in your directory) + - Follow [these instructions](https://developers.google.com/adwords/api/docs/guides/authentication#webapp) (web app) to obtain a google client secret & a google client ID. For the callback URL, use https://*your-app-name*.herokuapp.com/auth/google_oauth2/ + - Use the command `heroku config:set VARIABLE=value` to set the environment variables. + - Add `ADMINS` & `STAFF` set to a comma-separated list of verified administrators and staff members for the app. For testing purposes, these variables can both be set to the string `”none”` + - Add `GOOGLE_CLIENT_SECRET` & `GOOGLE_CLIENT_ID` as provided by the instructions above. + - **Note**: The environment variables can be manually entered in the Heroku app's Settings under "Config Vars". + - Attach "Heroku Postgres" as an add-on to the Heroku app's Resource tab. + - Using a terminal, run `heroku run rake db:schema:load` and `heroku run rake db:migrate` to load the database. + - **Note**: If you do not have command-line access to run these commands, you can run rake commands directly in the Heroku app's console. + +## Github Action + +In order for the GitHub Actions build to pass, you must add a `CC_TEST_REPORTER_ID` as a repository secret on GitHub. To do this, first sign up for an account with codeclimate.com (quality, not velocity). Then, connect your repository and navigate to repo settings on the CodeClimate dashboard. Finally, copy the `test reporter ID` under the test coverage tab, and add it as a new repository secret under repository settings on GitHub. + + +## RuboCop + +RuboCop is a Ruby code style checker (linter) and formatter. +1. You can run `rubocop -a` to make safe fixes and `rubocop -A` to make unsafe auto-refactors. +2. You can also run `rubocop --auto-gen-config` to configure some finer settings. + +## New Relic APM + +New Relic APM is a monitoring service that monitors your apps performance. You can add the monitoring service on Heroku by going to the Resources page of your Heroku app. + +## Canvas/Bcourse Authentication + +The app relies on Canvas/Bcourse to authenticate users. If you are testing the authentication in a sandbox Canvas environment, you must have access to the sandbox environment to be able to authenticate. Request access to the sandbox environment from the professors. + +**Authentication** + +The Canvas URL and redirect URI are stored in the `config/credentials.yml.enc` file. If you click on the `Login with Canvas` button on the homepage and cancel the authentication, you will be redirected to the redirect URI stored in the `config/credentials.yml.enc` file. **For local testing, use the mock login instead and do not use the `Login with Canvas` button.** + +**Mock Login/Authentication** + +To make the mock authentication for Canvas visible and usable, add the following to your terminal profile to create a local environment variable: +- `export MOCK_CANVAS_LOGIN=true` + +Setting the environment variable to false will disable the mock login. **Be sure to not set this environment variable to true in the production environment.** + + + +## Credit +Spring 2025 +- [Henry Wang](https://github.com/henwanfan) +- [Jeremy Richardson](https://github.com/WinbrosXP) +- [Kefeng Duan](https://github.com/mingyuyoooh) +- [Mandy Wong](https://github.com/mandywong0) +- [Sher Her](https://github.com/sherher21) diff --git a/Rakefile b/Rakefile index 488c551fe..d2a78aa25 100644 --- a/Rakefile +++ b/Rakefile @@ -3,6 +3,6 @@ # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. -require_relative 'config/application' +require_relative "config/application" Rails.application.load_tasks diff --git a/app/assets/images/198_info.png b/app/assets/images/198_info.png new file mode 100644 index 000000000..ca56a13b1 Binary files /dev/null and b/app/assets/images/198_info.png differ diff --git a/app/assets/images/new_logo.png b/app/assets/images/new_logo.png new file mode 100644 index 000000000..ea1b837ef Binary files /dev/null and b/app/assets/images/new_logo.png differ diff --git a/app/assets/stylesheets/actiontext.css b/app/assets/stylesheets/actiontext.css new file mode 100644 index 000000000..3cfcb2b75 --- /dev/null +++ b/app/assets/stylesheets/actiontext.css @@ -0,0 +1,31 @@ +/* + * Provides a drop-in pointer for the default Trix stylesheet that will format the toolbar and + * the trix-editor content (whether displayed or under editing). Feel free to incorporate this + * inclusion directly in any other asset bundle and remove this file. + * + *= require trix +*/ + +/* + * We need to override trix.css’s image gallery styles to accommodate the + * element we wrap around attachments. Otherwise, + * images in galleries will be squished by the max-width: 33%; rule. +*/ +.trix-content .attachment-gallery > action-text-attachment, +.trix-content .attachment-gallery > .attachment { + flex: 1 0 33%; + padding: 0 0.5em; + max-width: 33%; +} + +.trix-content .attachment-gallery.attachment-gallery--2 > action-text-attachment, +.trix-content .attachment-gallery.attachment-gallery--2 > .attachment, .trix-content .attachment-gallery.attachment-gallery--4 > action-text-attachment, +.trix-content .attachment-gallery.attachment-gallery--4 > .attachment { + flex-basis: 50%; + max-width: 50%; +} + +.trix-content action-text-attachment .attachment { + padding: 0 !important; + max-width: 100% !important; +} diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index b1fd3ab70..9861da89b 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -1,4 +1,5 @@ @import "bootstrap"; + /* * This is a manifest file that'll be compiled into application.css, which will include all the files * listed below. @@ -14,6 +15,18 @@ *= require_tree . *= require_self */ +body { + display: flex; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; +} + +main { + flex-grow: 1; +} + h1 { font-family: 'Poppins', sans-serif; font-style: normal; @@ -22,16 +35,19 @@ h1 { line-height: 120%; color: #04021D; } + h2 { font-family: 'Poppins', sans-serif; font-style: normal; font-weight: 600; } + h3 { font-family: 'Poppins', sans-serif; font-style: normal; font-weight: 500; } + p { font-family: 'Poppins', sans-serif; font-style: normal; @@ -41,6 +57,11 @@ p { /* Text / Dark - 60% */ color: #686777; } + +li { + text-align: match-parent; +} + button { font-family: 'Poppins', sans-serif; font-style: normal; @@ -48,40 +69,170 @@ button { font-size: 16px; line-height: 24px; } + b { font-family: 'Poppins', sans-serif; font-style: normal; font-weight: bolder; color: #262626; } + .btn-primary { background: #554AF0; border-radius: 12px; - } + .btn-primary:hover { background-color: #2112ee !important; } + .btn-secondary { background: #eaeaea; border-radius: 12px; border-color: #eaeaea; color: #554AF0; } + +.btn-info { + background-color: #c48828; + border-color: #c48828; + color: white; + font-size: 24px; +} + +.btn-info:hover { + background-color: #c48828; + border-color: #c48828; + color: white; + text-decoration: underline; + font-size: 24px; +} + .form-text { font-family: 'Poppins', sans-serif; font-style: normal; font-weight: 400; } + .form-label { font-family: 'Poppins', sans-serif; font-style: normal; font-weight: 500; } -.name-display{ + +.name-display { color: #554AF0; } + .form-select { text-align-last: center; } +#footer { + background-color: #003262; + box-sizing: border-box; + color: #ffffff; + margin: 36px 0; + margin-bottom: 0; + font-size: 13px; + display: block; +} + +#footer a { + color: #ffffffda; +} + +#footer p, +#footer span { + color: #ffffffda; +} + +#footer h2 { + font-size: 23px; +} + +.element-invisible { + position: absolute !important; + clip: rect(1px 1px 1px 1px); + clip: rect(1px, 1px, 1px, 1px); + overflow: hidden; + height: 1px; +} + +.social-links ul li { + display: inline; + margin: 10px; + font-size: 18px; +} + +.nav-pills>li+li { + margin-left: 2px; + float: left; +} + +.nav>li { + position: relative; + display: block; +} + +.nav-active { + color: white; +} + +.nav-dull { + color: darkgray; +} + +ul.nav, +ul.navl li, +ul.dropdown-menu, +ul.dropdown-menu li { + list-style: none outside none !important; +} + +.cry { + margin: 0; + padding: 0; + background: #003262; +} + +#primary-nav>ul>li.menu-fields-menu-link { + position: static; +} + +#primary-nav>ul>li.menu-fields-menu-link>.dropdown-menu { + left: 0; + padding: 0; + width: 100%; +} + +#primary-nav>ul>li.menu-fields-menu-link>.dropdown-menu>li>.openberkeley-megamenu { + padding: 20px; + width: 100%; +} + +.leaf a { + color: #999; + /* Set the generic color to a lighter gray */ + transition: color 0.3s; + /* Add a transition effect for smooth color change */ + text-decoration: none; + float: left; +} + +.leaf a:hover { + color: #555; + /* Slightly darken the color when hovered over */ +} + +#footer-bottom ul li { + display: grid; +} + +#footer-bottom { + margin-top: -15px; +} + +.berkeley-color { + background-color: #003262; +} \ No newline at end of file diff --git a/app/controllers/admin/events_controller.rb b/app/controllers/admin/events_controller.rb new file mode 100644 index 000000000..28c9d91d4 --- /dev/null +++ b/app/controllers/admin/events_controller.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +class Admin::EventsController < ApplicationController + before_action :check_if_admin + before_action :set_event, only: [:edit, :update, :show, :destroy] + + def index + @events = Event.all + render "admin/events/index" + end + + def new + @event = Event.new + end + + def create + @event = Event.new(event_params) + if @event.save + redirect_to admin_events_path, flash: { success: "Event created successfully." } + else + flash.now[:alert] = "Missing values. Failed to create event." + render :new + end + end + + def edit + # event set by set_event + end + + def show + # event set by set_event + redirect_to admin_events_path + end + + def update + if @event.update(event_params) + redirect_to admin_events_path, flash: { success: "Event updated successfully." } + else + flash.now[:alert] = "Failed to update event." + render :edit + end + end + + def destroy + Rails.logger.debug("PARAMS: #{params.inspect}") + Rails.logger.debug("REQUEST METHOD: #{request.method}") + if @event.destroy + redirect_to admin_events_path, flash: { success: "Event deleted successfully." } + else + redirect_to admin_events_path, flash: { error: "Failed to delete event." } + end + end + + def export_events + @events = Event.all + respond_to do |format| + format.csv { send_data Event.to_csv, filename: "events-#{Date.today}.csv" } + end + end + + private + def event_params + params.require(:event).permit(:title, :date, :start_time, :end_time, :location, :description, :flyer) # may change later, not sure what we want to include + end + + def set_event + @event = Event.find(params[:id]) + rescue ActiveRecord::RecordNotFound + redirect_to admin_events_path, alert: "Event not found." + end +end diff --git a/app/controllers/admins_controller.rb b/app/controllers/admins_controller.rb index f0c6ba7e3..141b9dff3 100644 --- a/app/controllers/admins_controller.rb +++ b/app/controllers/admins_controller.rb @@ -7,7 +7,7 @@ class AdminsController < ApplicationController def index; end def view_checkin_records - if (!params.key? :page) || (params[:page] < 1) + if (!params.key? :page) || (params[:page].to_i < 1) redirect_to view_checkin_records_path(page: 1) # return is needed here, otherwise the app will continue execute # the following instructions after redirect @@ -18,10 +18,134 @@ def view_checkin_records @has_next_page = @checkin_records.size == 20 end - private + def manage_advisors + @advisors = Advisor.all + end + + def manage_scholarships + @scholarships = Scholarship.all + render "admins/scholarships/manage" + end + + def manage_user_roles + @users = User.all + end + + def new + @scholarship = Scholarship.new + render "admins/scholarships/new" + end + + def create + @scholarship = Scholarship.new(scholarship_params) + if @scholarship.save + redirect_to manage_scholarships_path, notice: "Scholarship was successfully created." + else + render "admins/scholarships/new" + end + end + + def edit + @scholarship = Scholarship.find(params[:id]) + render "admins/scholarships/edit" + end + + def update + @scholarship = Scholarship.find(params[:id]) + if @scholarship.update(scholarship_params) + redirect_to manage_scholarships_path, notice: "Scholarship was successfully updated." + else + render "admins/scholarships/edit" + end + end + + def destroy + @scholarship = Scholarship.find(params[:id]) + @scholarship.destroy + redirect_to manage_scholarships_path, notice: "Scholarship was successfully deleted." + end + + def batch_delete + scholarship_ids = params[:scholarship_ids] + if scholarship_ids.present? + Scholarship.where(id: scholarship_ids).destroy_all + redirect_to manage_scholarships_path, notice: "Selected scholarships were successfully deleted." + else + redirect_to manage_scholarships_path, alert: "No scholarships were selected for deletion." + end + end + + def manage_courses + @courses = Course.all + render "admins/courses/manage" # Explicitly specify the template to render + end + + def new_course + @course = Course.new + render "admins/courses/new" + end + def create_course + @course = Course.new(course_params) + if @course.save + redirect_to manage_courses_path, notice: "Course was successfully created." + else + render "admins/courses/new" + end + end + + def edit_course + @course = Course.find(params[:id]) + render "admins/courses/edit" + end + + def update_course + @course = Course.find(params[:id]) + if @course.update(course_params) + redirect_to manage_courses_path, notice: "Course was successfully updated." + else + render "admins/courses/edit" + end + end + + def destroy_course + @course = Course.find(params[:id]) + @course.destroy + redirect_to manage_courses_path, notice: "Course was successfully deleted." + end + + def export_courses + @courses = Course.all + respond_to do |format| + format.csv { send_data Course.to_csv, filename: "courses-#{Date.today}.csv" } + end + end + + def export_scholarships + @scholarships = Scholarship.all + respond_to do |format| + format.csv { send_data Scholarship.to_csv, filename: "scholarships-#{Date.today}.csv" } + end + end + + private def check_permission admin = Admin.find_by_id(session[:current_user_id]) redirect_to root_path, flash: { error: "You don't have the permission to do that!" } if !admin || !admin.is_admin end + + def scholarship_params + params.require(:scholarship).permit(:name, :description, :status_text, :application_url) + end + + def course_params + params.require(:course).permit(:code, :title, :description, :units, :semester, :schedule, :ccn, :location, :available) + end + + def require_admin + unless current_user&.admin? + flash[:error] = "You must be an admin to access this page." + redirect_to root_path + end + end end diff --git a/app/controllers/advisors_controller.rb b/app/controllers/advisors_controller.rb new file mode 100644 index 000000000..259036977 --- /dev/null +++ b/app/controllers/advisors_controller.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +class AdvisorsController < ApplicationController + before_action :set_advisor, only: [:edit, :update, :destroy] + + def new + @advisor = Advisor.new + end + + def create + @advisor = Advisor.new(advisor_params) + if @advisor.save + redirect_to manage_advisors_path, flash: { success: "Advisor created successfully." } + else + render :new, flash: { error: "Failed to create advisor." } + end + end + def edit + end + + def update + if @advisor.update(advisor_params) + redirect_to manage_advisors_path, flash: { success: "Advisor updated successfully." } + else + render :edit, flash: { error: "Failed to update advisor." } + end + end + + def destroy + @advisor = Advisor.find(params[:id]) + if @advisor.destroy + redirect_to manage_advisors_path, flash: { success: "Advisor deleted successfully." } + else + redirect_to manage_advisors_path, flash: { error: "Failed to delete advisor." } + end + end + + def show + @advisor = Advisor.find(params[:id]) + end + + private + def set_advisor + @advisor = Advisor.find(params[:id]) + end + + def advisor_params + params.require(:advisor).permit(:name, :description, :calendar, :active) + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 7944f9f99..32e3d9b0e 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,4 +1,8 @@ # frozen_string_literal: true class ApplicationController < ActionController::Base + def check_if_admin + admin = Admin.find_by_id(session[:current_user_id]) + redirect_to root_path, flash: { error: "You must be an admin to access this page" } if !admin || !admin.is_admin + end end diff --git a/app/controllers/appointments_controller.rb b/app/controllers/appointments_controller.rb index 763563123..668a225ee 100644 --- a/app/controllers/appointments_controller.rb +++ b/app/controllers/appointments_controller.rb @@ -1,13 +1,15 @@ +# frozen_string_literal: true + class AppointmentsController < ApplicationController - before_action :require_login + before_action :require_login - def advisors - end + def advisors + @advisors = Advisor.where(active: true) + end - def require_login - unless session.has_key? :current_user_id and Student.find_by_id(session[:current_user_id]) - redirect_to login_path, :method => :get - end + def require_login + unless (session.has_key? :current_user_id) && Student.find_by_id(session[:current_user_id]) + redirect_to root_path, flash: { error: "Please log in first!" } end - -end \ No newline at end of file + end +end diff --git a/app/controllers/checkin_controller.rb b/app/controllers/checkin_controller.rb index c2dba3bda..11abacd85 100644 --- a/app/controllers/checkin_controller.rb +++ b/app/controllers/checkin_controller.rb @@ -2,28 +2,55 @@ class CheckinController < ApplicationController before_action :require_login + before_action :set_user_and_reasons def new flash.clear - @reasons = Checkin.reasons + @checkin = Checkin.new + set_default_reason end def create flash.clear - @user = Student.find_by_id(session[:current_user_id]) - @checkin = Checkin.new - @checkin.update(time: Time.current, student_id: @user.id, reason: params[:checkin][:reason]) + reason = params.dig(:checkin, :reason) + + # Only check if reason is present + if reason.blank? + flash[:error] = "Something went wrong, please try again" + redirect_to root_path + return + end + + @checkin = @user.checkins.build( + reason: reason, + time: Time.current + ) + if @checkin.save redirect_to root_path, flash: { success: "Success! You've been checked in!" } else - redirect_to root_path, flash: { error: 'Something went wrong, please try again' } + flash[:error] = "Something went wrong, please try again" + redirect_to root_path end end private + def set_user_and_reasons + @user = Student.find_by_id(session[:current_user_id]) + @reasons = Checkin.reasons + end + + def set_default_reason + @default_reason = if @user && @user.checkins.exists? + @user.checkins.order(time: :desc).first.reason + else + @reasons.first + end + end def require_login - redirect_to root_path, flash: { error: 'Please log-in first!' } unless - session.key?(:current_user_id) && Student.find_by_id(session[:current_user_id]) + unless session.key?(:current_user_id) && Student.find_by_id(session[:current_user_id]) + redirect_to root_path, flash: { error: "Please log-in first!" } + end end end diff --git a/app/controllers/courses_controller.rb b/app/controllers/courses_controller.rb new file mode 100644 index 000000000..5f8a9f2d0 --- /dev/null +++ b/app/controllers/courses_controller.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class CoursesController < ApplicationController + before_action :require_login, only: [:index] + + def index + @courses = Course.where(available: true) + end + + private + def require_login + unless session.key?(:current_user_id) && Student.find_by_id(session[:current_user_id]) + redirect_to root_path, flash: { error: "Please log in first!" } + end + end +end diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb new file mode 100644 index 000000000..60e03579e --- /dev/null +++ b/app/controllers/events_controller.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class EventsController < ApplicationController + before_action :require_login + + def index + @upcoming_events = Event.where("date >= ?", Date.today).order(:date) + @past_events = Event.where("date < ?", Date.today).order(date: :desc).limit(5) + end + + def require_login + unless session.key?(:current_user_id) && Student.find_by_id(session[:current_user_id]) + redirect_to root_path, flash: { error: "Please log in first!" } + end + end +end diff --git a/app/controllers/login_controller.rb b/app/controllers/login_controller.rb index c955108d4..bfab335d1 100644 --- a/app/controllers/login_controller.rb +++ b/app/controllers/login_controller.rb @@ -3,6 +3,21 @@ class LoginController < ApplicationController def confirm @user = User.find_by(id: session[:current_user_id]) - redirect_to root_path, flash: { error: 'Please log-in first!' } if @user.nil? + redirect_to root_path, flash: { error: "Please log-in first!" } if @user.nil? + end + + def canvas_login + unless Rails.application.credentials[:CANVAS_CLIENT_ID] && Rails.application.credentials[:CANVAS_URL] && Rails.application.credentials[:CANVAS_REDIRECT_URI] + redirect_to root_path, flash: { error: "Canvas configuration is missing" } + return + end + params = { + client_id: Rails.application.credentials[:CANVAS_CLIENT_ID], + response_type: "code", + redirect_uri: Rails.application.credentials[:CANVAS_REDIRECT_URI], + scope: "url:GET|/api/v1/users/self" + } + + redirect_to "#{Rails.application.credentials[:CANVAS_URL]}/login/oauth2/auth?#{params.to_query}", allow_other_host: true end end diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb index 3c8bce8d1..187089a38 100644 --- a/app/controllers/pages_controller.rb +++ b/app/controllers/pages_controller.rb @@ -6,15 +6,14 @@ class PagesController < ApplicationController def index; end private - def authenticate @user_type = [] user = User.find_by_id(session[:current_user_id]) if user @name = user.name - @user_type.push 'Admin' if user.is_admin - @user_type.push 'Staff' if user.is_staff - @user_type.push 'Student' if user.is_student + @user_type.push "Admin" if user.is_admin + @user_type.push "Staff" if user.is_staff + @user_type.push "Student" if user.is_student end @logged_out = !user end diff --git a/app/controllers/podcasts_controller.rb b/app/controllers/podcasts_controller.rb new file mode 100644 index 000000000..ccc2bdd11 --- /dev/null +++ b/app/controllers/podcasts_controller.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +# frozen_string_literal: true + +class PodcastsController < ApplicationController + before_action :require_login + + def index + end + + def require_login + unless session.key?(:current_user_id) && Student.find_by_id(session[:current_user_id]) + redirect_to root_path, flash: { error: "Please log in first!" } + end + end +end diff --git a/app/controllers/scholarships_controller.rb b/app/controllers/scholarships_controller.rb new file mode 100644 index 000000000..b886f3df4 --- /dev/null +++ b/app/controllers/scholarships_controller.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +class ScholarshipsController < ApplicationController + before_action :require_login + # Add admin permission check, only applies to actions that modify data + before_action :check_admin_permission, only: [:new, :create, :edit, :update, :destroy] + # Add set_scholarship callback, only applies to actions requiring a specific scholarship record + before_action :set_scholarship, only: [:edit, :update, :show, :destroy] # Add :show if needed + + # GET /scholarships (replaces the original index) + def index + # This index might now be for all logged-in users, or you can adjust as needed + @scholarships = Scholarship.all + # Add pagination if necessary + end + + # GET /scholarships/:id/edit (from admin) + def edit + # @scholarship is set by set_scholarship + end + + # PATCH/PUT /scholarships/:id (from admin) + def update + if @scholarship.update(scholarship_params) + flash[:notice] = "Scholarship updated successfully." + # Redirect to the scholarships list page, adjust the path according to your routes + redirect_to scholarships_path # Was admin_scholarships_path + else + flash.now[:alert] = "Failed to update scholarship." + render :edit, status: :unprocessable_entity + end + end + + # GET /scholarships/new (from admin) + def new + @scholarship = Scholarship.new + end + + # POST /scholarships (from admin) + def create + @scholarship = Scholarship.new(scholarship_params) + if @scholarship.save + flash[:notice] = "Scholarship created successfully." + # Redirect to the scholarships list page + redirect_to scholarships_path # Was admin_scholarships_path + else + flash.now[:alert] = "Failed to create scholarship." + render :new, status: :unprocessable_entity + end + end + + # DELETE /scholarships/:id (from admin) + def destroy + @scholarship.destroy + flash[:notice] = "Scholarship deleted successfully." + # Redirect to the scholarships list page + redirect_to scholarships_path # Was admin_scholarships_path + end + + # If you need a show action + def show + @scholarship = Scholarship.find(params[:id]) + end + + private # Move the original require_login under private and add other private methods + def require_login + unless session.key?(:current_user_id) && Student.find_by_id(session[:current_user_id]) + redirect_to root_path, flash: { error: "Please log in first!" } + end + end + + # set_scholarship from admin + def set_scholarship + @scholarship = Scholarship.find(params[:id]) + rescue ActiveRecord::RecordNotFound + flash[:alert] = "Scholarship not found." + # Adjust the redirect path as needed + redirect_to scholarships_path # Was admin_scholarships_path + end + + # scholarship_params from admin + def scholarship_params + # Ensure you permit all the attributes you want to be updatable through the form + params.require(:scholarship).permit(:name, :description, :status_text, :application_url) + end + + # check_admin_permission from admin + def check_admin_permission + # Implement your admin permission check logic here + # Check if the current user is an admin + # Note: This assumes admin info is stored in the Admin model and associated via session[:current_user_id] + # You might need to adjust this logic to match your user and permission model + # E.g., if the Student model has an is_admin flag: + # current_student = Student.find_by_id(session[:current_user_id]) + # unless current_student&.is_admin + + # Or if admins are a different model: + admin = Admin.find_by_id(session[:current_user_id]) # Assuming admins also use this session key + unless admin # Or admin&.is_admin if Admin model has an is_admin field + redirect_to root_path, flash: { error: "You don't have the permission to do that!" } + end + # If your admin and student user systems are separate, you need more complex logic to handle sessions + end +end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 03d9f66e3..acac67fa2 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -3,7 +3,7 @@ class SessionsController < ApplicationController def google_auth # Get access tokens from the google server - access_token = request.env['omniauth.auth'] + access_token = request.env["omniauth.auth"] existing_user = User.where(email: access_token.info.email).first if existing_user.present? session[:current_user_id] = existing_user.id @@ -20,11 +20,11 @@ def google_auth # set appropriate permissions user = set_user_permission(user, access_token.info.email) if !user - redirect_to root_path, flash: { error: 'Something went wrong, please try again later.' } + redirect_to root_path, flash: { error: "Something went wrong, please try again later." } elsif user.save user_first_login(user) else - redirect_to root_path, flash: { error: 'Something went wrong, please try again' } + redirect_to root_path, flash: { error: "Something went wrong, please try again" } end end @@ -33,8 +33,88 @@ def google_auth_logout redirect_to root_path, flash: { success: "You've been successfully logged-out!" } end - private + # Canvas authentication + def canvas_callback + if ENV["MOCK_CANVAS_LOGIN"] == "true" + # Read the role from params (default to "student" if none) + role = params[:mock_role] || "student" + + fake_email = case role + when "admin" + "169reentryadmin@berkeley.edu" + else + "studentuser@berkeley.edu" + end + + user = User.find_or_initialize_by(email: fake_email) + + if user.new_record? + user.first_name = role.capitalize + user.last_name = "Mock" + user.email = fake_email + if role == "student" + user.sid = "12345678" + end + end + + user = set_user_permission(user, fake_email) + user.save! + session[:current_user_id] = user.id + + redirect_to root_path, flash: { success: "Logged in as #{role.capitalize} mock user." } + return + end + if params[:error].present? || params[:code].blank? + redirect_to root_path, alert: "Authentication failed. Please try again." + return + end + + access_token = get_access_token(params[:code]) + + response = Faraday.get("#{Rails.application.credentials[:CANVAS_URL]}/api/v1/users/self?") do |req| + req.headers["Authorization"] = "Bearer #{access_token.token}" + end + + if response.success? + user_data = JSON.parse(response.body) + existing_user = User.where(email: user_data["email"]).first + if existing_user.present? + session[:current_user_id] = existing_user.id + redirect_to root_path, flash: { success: "Success! You've been logged-in!" } + return + end + end + user = User.from_canvas(user_data) + # user.canvas_token = access_token.token + # user.canvas_refresh_token = access_token.credentials.refresh_token if access_token.credentials.refresh_token.present? + user = set_user_permission(user, user_data["email"]) + if user.save + session[:current_user_id] = user.id + redirect_to root_path, flash: { success: "Success! You've been logged-in!" } + else + redirect_to root_path, flash: { error: "Something went wrong, please try again." } + end + end + + def get_access_token(code) + client = OAuth2::Client.new( + Rails.application.credentials[:CANVAS_CLIENT_ID], + Rails.application.credentials[:CANVAS_CLIENT_SECRET], + site: Rails.application.credentials[:CANVAS_URL], + token_url: "/login/oauth2/token" + ) + client.auth_code.get_token(code, redirect_uri: :canvas_callback) + end + + def canvas_auth_logout + session.delete(:current_user_id) + redirect_to root_path, flash: { success: "You've been successfully logged-out!" } + end + + + + private def user_first_login(user) session[:current_user_id] = user.id if user.is_student @@ -48,8 +128,8 @@ def user_first_login(user) def set_user_permission(user, email) # Get the official admins - admins = ENV['ADMINS'].split(',') - staff = ENV['STAFF'].split(',') + admins = Rails.application.credentials[:ADMINS].split(",") + staff = Rails.application.credentials[:STAFF].split(",") return nil if admins.blank? || staff.blank? user.is_admin = admins.include? email diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index bdc8bbc31..49e77d9e2 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -8,18 +8,18 @@ def update @user = User.find_by(id: session[:current_user_id]) sid = params[:user][:sid] email = params[:user][:email] - if sid.blank? || (sid.length != 10) - redirect_to login_confirm_path, flash: { error: 'Invalid Student ID Number.' } + if sid.blank? || sid.length < 8 || sid.length > 10 # TODO: check the format of sid in the future + redirect_to login_confirm_path, flash: { error: "Invalid Student ID Number." } return end if email.blank? || !email.match(/.+(@berkeley.edu)/) - redirect_to login_confirm_path, flash: { error: 'Please use your berkeley email to log-in.' } + redirect_to login_confirm_path, flash: { error: "Please use your berkeley email to log-in." } return end if @user.update(user_params) redirect_to user_profile_new_path else - redirect_to login_confirm_path, flash: { error: 'Something went wrong, please try again.' } + redirect_to login_confirm_path, flash: { error: "Something went wrong, please try again." } end end @@ -36,7 +36,7 @@ def profile_update @user.update!(profile_params) end if edited - redirect_to root_path, flash: { success: 'Success! Your profile has been updated.' } + redirect_to root_path, flash: { success: "Success! Your profile has been updated." } else redirect_to root_path, flash: { success: "Success! You've been logged-in!" } end @@ -47,10 +47,22 @@ def profile_edit @user = Student.find_by_id(session[:current_user_id]) end - private + def edit_user_role + @user = User.find(params[:id]) + end + def update_user_role + @user = User.find(params[:id]) + if @user.update(user_role_params) + redirect_to manage_user_roles_path, flash: { success: "User role updated successfully." } + else + redirect_to manage_user_roles_path, flash: { error: "Failed to update user role." } + end + end + + private def require_login - redirect_to root_path, flash: { error: 'Only students have access to profiles.' } unless + redirect_to root_path, flash: { error: "Only students have access to profiles." } unless session.key?(:current_user_id) && Student.find_by_id(session[:current_user_id]).present? end @@ -61,4 +73,8 @@ def profile_params def user_params params.require(:user).permit(:first_name, :last_name, :email, :sid) end + + def user_role_params + params.require(:user).permit(:first_name, :last_name, :email, :sid, :is_admin, :is_advisor, :is_student) + end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 15b06f0f6..5ef82a482 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,4 +1,21 @@ # frozen_string_literal: true module ApplicationHelper + # Removed the markdown method + + # Helper method to check if the current user is an admin + def admin? + # Assuming admin status is determined by the presence of an Admin record + # matching the session's user ID, similar to check_admin_permission + # Note: Ensure the Admin model exists and this logic matches your authentication setup. + # Also ensure session[:current_user_id] is correctly set upon login. + # Return false if no user is logged in. + return false unless session[:current_user_id] + + Admin.exists?(id: session[:current_user_id]) + rescue NameError + # Handle case where Admin model might not exist (optional, for robustness) + Rails.logger.error "Admin model not found. Cannot perform admin check." + false + end end diff --git a/app/javascript/application.js b/app/javascript/application.js index 0accd18f8..b29606c7d 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -5,4 +5,6 @@ import "controllers" //= require jquery_ujs //= require bootstrap-sprockets //= require turbolinks -//= require_tree . \ No newline at end of file +//= require_tree . +import "trix" +import "@rails/actiontext" diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index d84cb6e71..5cc63a0c6 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true class ApplicationMailer < ActionMailer::Base - default from: 'from@example.com' - layout 'mailer' + default from: "from@example.com" + layout "mailer" end diff --git a/app/models/advisor.rb b/app/models/advisor.rb new file mode 100644 index 000000000..0dbb51dac --- /dev/null +++ b/app/models/advisor.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class Advisor < ApplicationRecord + validates :name, :description, presence: true + validates :calendar, format: { with: URI::DEFAULT_PARSER.make_regexp, message: "must be a valid URL" }, allow_blank: true +end diff --git a/app/models/checkin.rb b/app/models/checkin.rb index 09afadf97..0d7021e4d 100644 --- a/app/models/checkin.rb +++ b/app/models/checkin.rb @@ -11,6 +11,6 @@ def self.get_20_checkin_records(n) end def self.reasons - ['Peer Support', 'Counseling Appointment', 'Studying', 'OWLs Meeting', 'Other'] + ["Peer Support", "Drop-in Advising", "Studying", "OWLs Meeting", "Other"] end end diff --git a/app/models/course.rb b/app/models/course.rb new file mode 100644 index 000000000..66d872c74 --- /dev/null +++ b/app/models/course.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class Course < ApplicationRecord + validates :code, presence: true + validates :title, presence: true + validates :description, presence: true + validates :units, presence: true + + def self.to_csv + require "csv" + attributes = %w{id code title description units semester schedule ccn location available created_at updated_at} + + CSV.generate(headers: true) do |csv| + csv << attributes + + all.each do |course| + csv << attributes.map { |attr| course.send(attr) } + end + end + end +end diff --git a/app/models/event.rb b/app/models/event.rb new file mode 100644 index 000000000..00978efcc --- /dev/null +++ b/app/models/event.rb @@ -0,0 +1,28 @@ +# app/models/event.rb +# frozen_string_literal: true + +class Event < ApplicationRecord + validates :title, :date, :start_time, :location, :description, presence: true + + # Helper method for displaying formatted time range + def formatted_time + if end_time.present? + "#{start_time.strftime('%l:%M %p').strip} - #{end_time.strftime('%l:%M %p').strip}" + else + start_time.strftime("%l:%M %p").strip + end + end + + # Helper method for displaying formatted date + def formatted_date + date.strftime("%A, %B %d, %Y") + end + + def self.to_csv + require "csv" + CSV.generate(headers: true) do |csv| + csv << column_names + all.each { |event| csv << event.attributes.values_at(*column_names) } + end + end +end diff --git a/app/models/scholarship.rb b/app/models/scholarship.rb new file mode 100644 index 000000000..e1ab33043 --- /dev/null +++ b/app/models/scholarship.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require "csv" + +class Scholarship < ApplicationRecord + validates :name, presence: true + validates :description, presence: true + validates :status_text, presence: true + validates :application_url, format: { with: URI.regexp(%w[http https]), message: "must be a valid URL" }, allow_blank: true + + def self.to_csv + attributes = %w[id name description status_text application_url created_at updated_at] + + CSV.generate(headers: true) do |csv| + csv << attributes + + all.each do |scholarship| + csv << attributes.map { |attr| scholarship.send(attr) } + end + end + end +end diff --git a/app/models/student.rb b/app/models/student.rb index 3e52a8123..768170b15 100644 --- a/app/models/student.rb +++ b/app/models/student.rb @@ -8,6 +8,6 @@ class Student < User validate :check_is_student def check_is_student - raise 'This user must be a student!!' if is_student == false + raise "This user must be a student!!" if is_student == false end end diff --git a/app/models/user.rb b/app/models/user.rb index c711e2cc8..bda64e3e3 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -11,6 +11,15 @@ def self.from_omniauth(auth) end end + def self.from_canvas(user_data) + where(email: user_data["email"]).first_or_initialize do |user| + user.first_name = user_data["first_name"] + user.last_name = user_data["last_name"] + user.email = user_data["email"] + user.sid = user_data["sis_user_id"] + end + end + def name "#{first_name} #{last_name}" end diff --git a/app/views/active_storage/blobs/_blob.html.erb b/app/views/active_storage/blobs/_blob.html.erb new file mode 100644 index 000000000..49ba357dd --- /dev/null +++ b/app/views/active_storage/blobs/_blob.html.erb @@ -0,0 +1,14 @@ +
attachment--<%= blob.filename.extension %>"> + <% if blob.representable? %> + <%= image_tag blob.representation(resize_to_limit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ]) %> + <% end %> + +
+ <% if caption = blob.try(:caption) %> + <%= caption %> + <% else %> + <%= blob.filename %> + <%= number_to_human_size blob.byte_size %> + <% end %> +
+
diff --git a/app/views/admin/events/edit.html.erb b/app/views/admin/events/edit.html.erb new file mode 100644 index 000000000..01f22947e --- /dev/null +++ b/app/views/admin/events/edit.html.erb @@ -0,0 +1,61 @@ +

Edit Event

+ +<% if flash[:notice] %> +
+ <%= flash[:notice] %> +
+<% end %> + +<% if flash[:alert] %> +
+ <%= flash[:alert] %> +
+<% end %> + +
+ <%= form_with model: [:admin, @event], local: true do |form| %> +
+ <%= form.label :title, class: 'form-label' %> + <%= form.text_field :title, class: 'form-control' %> +
+ +
+ <%= form.label :date %> + <%= form.date_field :date, class: 'form-control' %> +
+ +
+
+ <%= form.label :start_time %> + <%= form.time_field :start_time, class: 'form-control' %> +
+
+ <%= form.label :end_time %> + <%= form.time_field :end_time, class: 'form-control' %> +
+
+ +
+ <%= form.label :location %> + <%= form.text_field :location, class: 'form-control' %> +
+ +
+ <%= form.label :description, class: 'form-label' %> + <%= form.text_area :description, class: 'form-control', rows: 3 %> +
+ +
+ <%= form.label :flyer, class: 'form-label' %> + <%= form.file_field :flyer, class: 'form-control' %> + Upload an image file for the event flyer (max 5MB) +
+ +
+ <%= form.submit 'Update Event', class: 'mt-3 mb-3 d-block mx-auto btn-lg btn-primary' %> +
+ <% end %> +
+<%= link_to 'Back to Events', admin_events_path, class: 'btn btn-secondary btn-lg d-block mx-auto mb-5' %> + + diff --git a/app/views/admin/events/index.html.erb b/app/views/admin/events/index.html.erb new file mode 100644 index 000000000..be010388d --- /dev/null +++ b/app/views/admin/events/index.html.erb @@ -0,0 +1,62 @@ + + + <%= stylesheet_link_tag 'https://cdn.datatables.net/1.13.4/css/jquery.dataTables.min.css' %> + + +
+
+ <%= render "shared/flash_messages" %> +
+

Manage Events

+ +
+ <%= link_to "Export", export_admin_events_path(format: :csv), class: "btn btn-secondary" %> +
+ +
+ + + + + + + + + + + + + <% @events.each do |event| %> + + + + + + + + + <% end %> + +
TitleDescriptionDateTimeLocationActions
<%= event.title %><%= event.description %><%= event.date.strftime('%B %d, %Y') %><%= event.formatted_time %><%= event.location %> + <%= link_to "Edit", edit_admin_event_path(event), class: 'btn btn-primary btn-sm' %> + <%= button_to "Delete", admin_event_path(event), method: :delete, data: { confirm: 'Are you sure you want to delete this event?' }, class: 'btn btn-danger btn-sm' %> +
+
+
+ +
+
+ <%= link_to "Back to Admin Dashboard".html_safe, admins_path, class: "btn btn-secondary btn-lg" %> + <%= link_to "Add New Event".html_safe, new_admin_event_path, class: "btn-lg btn-success" %> +
+
+ <%= javascript_include_tag 'https://code.jquery.com/jquery-3.6.0.min.js' %> + <%= javascript_include_tag 'https://cdn.datatables.net/1.13.4/js/jquery.dataTables.min.js' %> + + + + diff --git a/app/views/admin/events/new.html.erb b/app/views/admin/events/new.html.erb new file mode 100644 index 000000000..7dd33f691 --- /dev/null +++ b/app/views/admin/events/new.html.erb @@ -0,0 +1,61 @@ +

Create New Event

+ +<% if flash[:notice] %> +
+ <%= flash[:notice] %> +
+<% end %> + +<% if flash[:alert] %> +
+ <%= flash[:alert] %> +
+<% end %> + +
+ <%= form_with model: @event, url: admin_events_path, method: :post, local: true do |form| %> +
+ <%= form.label :title, class: 'form-label' %> + <%= form.text_field :title, class: 'form-control' %> +
+ +
+ <%= form.label :date %> + <%= form.date_field :date, class: 'form-control' %> +
+ +
+
+ <%= form.label :start_time %> + <%= form.time_field :start_time, class: 'form-control' %> +
+
+ <%= form.label :end_time %> + <%= form.time_field :end_time, class: 'form-control' %> +
+
+ +
+ <%= form.label :location %> + <%= form.text_field :location, class: 'form-control' %> +
+ +
+ <%= form.label :description, class: 'form-label' %> + <%= form.text_area :description, class: 'form-control', rows: 3 %> +
+ +
+ <%= form.label :flyer, class: 'form-label' %> + <%= form.file_field :flyer, class: 'form-control' %> + Upload an image file for the event flyer (max 5MB) +
+ +
+ <%= form.submit 'Create Event', class: 'mt-3 mb-3 d-block mx-auto btn-lg btn-primary' %> +
+ <% end %> +
+<%= link_to 'Back to Events', admin_events_path, class: 'btn btn-secondary btn-lg d-block mx-auto mb-5' %> + + diff --git a/app/views/admins/courses/_form.html.erb b/app/views/admins/courses/_form.html.erb new file mode 100644 index 000000000..25d9995d0 --- /dev/null +++ b/app/views/admins/courses/_form.html.erb @@ -0,0 +1,62 @@ +<%= form_with(model: course, url: course.persisted? ? update_course_path(course) : create_course_path, method: course.persisted? ? :patch : :post, local: true) do |form| %> + <% if course.errors.any? %> +
+

<%= pluralize(course.errors.count, "error") %> prohibited this course from being saved:

+ +
+ <% end %> + +
+ <%= form.label :code, "Course Code (e.g., 'L&S 198')", class: "form-label" %> + <%= form.text_field :code, class: "form-control" %> +
+ +
+ <%= form.label :title, "Course Title (e.g., 'Re-entry Transition Course for Adult Learners')", class: "form-label" %> + <%= form.text_field :title, class: "form-control" %> +
+ +
+ <%= form.label :description, "Course Description", class: "form-label" %> + <%= form.text_area :description, class: "form-control", rows: 5 %> +
+ +
+ <%= form.label :units, "Units (e.g., 'One Unit – Pass/Not Pass')", class: "form-label" %> + <%= form.text_field :units, class: "form-control" %> +
+ +
+ <%= form.label :semester, "Semester (e.g., 'Spring 2025')", class: "form-label" %> + <%= form.text_field :semester, class: "form-control" %> +
+ +
+ <%= form.label :schedule, "Schedule (e.g., 'Wednesdays 3:00pm - 4:00 pm')", class: "form-label" %> + <%= form.text_field :schedule, class: "form-control" %> +
+ +
+ <%= form.label :ccn, "Course Control Number (CCN#)", class: "form-label" %> + <%= form.text_field :ccn, class: "form-control" %> +
+ +
+ <%= form.label :location, "Location", class: "form-label" %> + <%= form.text_field :location, class: "form-control" %> +
+ +
+ <%= form.check_box :available, class: "form-check-input" %> + <%= form.label :available, "Make this course available on the public courses page", class: "form-check-label" %> +
+ +
+ <%= form.submit class: "btn btn-primary" %> + <%= link_to "Cancel", manage_courses_path, class: "btn btn-secondary" %> +
+<% end %> \ No newline at end of file diff --git a/app/views/admins/courses/edit.html.erb b/app/views/admins/courses/edit.html.erb new file mode 100644 index 000000000..c728be8dc --- /dev/null +++ b/app/views/admins/courses/edit.html.erb @@ -0,0 +1,10 @@ +
+
+ <%= render "shared/flash_messages" %> +

Edit Course

+ + <%= render 'admins/courses/form', course: @course %> + + <%= link_to "Back to Courses", manage_courses_path, class: "btn btn-secondary mt-3" %> +
+
\ No newline at end of file diff --git a/app/views/admins/courses/manage.html.erb b/app/views/admins/courses/manage.html.erb new file mode 100644 index 000000000..e3a3919b6 --- /dev/null +++ b/app/views/admins/courses/manage.html.erb @@ -0,0 +1,63 @@ + + + <%= stylesheet_link_tag 'https://cdn.datatables.net/1.13.4/css/jquery.dataTables.min.css' %> + + +
+
+ <%= render "shared/flash_messages" %> +
+

Manage Courses

+ +
+ <%= link_to "Export", export_courses_path(format: :csv), class: "btn btn-secondary" %> +
+ + + + + + + + + + + + + + + <% @courses.each do |course| %> + + + + + + + + + + <% end %> + +
CodeTitleUnitsSemesterScheduleAvailableActions
<%= course.code %><%= course.title %><%= course.units %><%= course.semester %><%= course.schedule %><%= course.available ? "Yes" : "No" %> + <%= link_to "Edit", edit_course_path(course), class: "btn btn-warning btn-sm" %> + <%= button_to "Delete", destroy_course_path(course), + method: :delete, + data: { turbo_confirm: "Are you sure you want to delete this course?" }, + class: "btn btn-danger btn-sm mt-1" %> +
+ +
+ <%= link_to "Add New Course", new_course_path, class: "btn btn-primary me-2" %> + <%= link_to "Back to Admin Dashboard", admins_path, class: "btn btn-secondary" %> +
+
+ + <%= javascript_include_tag 'https://code.jquery.com/jquery-3.6.0.min.js' %> + <%= javascript_include_tag 'https://cdn.datatables.net/1.13.4/js/jquery.dataTables.min.js' %> + + + \ No newline at end of file diff --git a/app/views/admins/courses/new.html.erb b/app/views/admins/courses/new.html.erb new file mode 100644 index 000000000..3040a1819 --- /dev/null +++ b/app/views/admins/courses/new.html.erb @@ -0,0 +1,10 @@ +
+
+ <%= render "shared/flash_messages" %> +

New Course

+ + <%= render 'admins/courses/form', course: @course %> + + <%= link_to "Back to Courses", manage_courses_path, class: "btn btn-secondary mt-3" %> +
+
\ No newline at end of file diff --git a/app/views/admins/index.html.erb b/app/views/admins/index.html.erb index 0267b5214..b95091464 100644 --- a/app/views/admins/index.html.erb +++ b/app/views/admins/index.html.erb @@ -4,7 +4,14 @@ <%= image_tag("reentry-owl.jpg", width: 130, height: 130, class: "d-block rounded mx-auto mb-4") %>

Admin Dashboard

- <%= link_to "view check-in records".html_safe, view_checkin_records_path, class: "btn btn-primary mt-1 btn-lg px-4 gap-3" %> + <%= link_to "View Check-in Records".html_safe, view_checkin_records_path, class: "btn btn-primary mt-1 btn-lg px-4 gap-3" %> + <%= link_to "Manage Events".html_safe, admin_events_path, class: "btn btn-primary mt-1 btn-lg px-4 gap-3" %> + <%= link_to "Manage Scholarships".html_safe, manage_scholarships_path, class: "btn btn-primary mt-1 btn-lg px-4 gap-3" %> +
+
+ <%= link_to "Manage Advisors".html_safe, manage_advisors_path, class: "btn btn-primary mt-1 btn-lg px-4 gap-3" %> + <%= link_to "Manage Courses".html_safe, manage_courses_path, class: "btn btn-primary mt-1 btn-lg px-4 gap-3" %> + <%= link_to "Manage User Role".html_safe, manage_user_roles_path, class: "btn btn-primary mt-1 btn-lg px-4 gap-3" %>
diff --git a/app/views/admins/manage_advisors.html.erb b/app/views/admins/manage_advisors.html.erb new file mode 100644 index 000000000..35e8652a2 --- /dev/null +++ b/app/views/admins/manage_advisors.html.erb @@ -0,0 +1,54 @@ + + + <%= stylesheet_link_tag 'https://cdn.datatables.net/1.13.4/css/jquery.dataTables.min.css' %> + + +
+
+ <%= render "shared/flash_messages" %> +
+

Manage Advisors

+
+ + + + + + + + + + + + <% @advisors.each do |advisor| %> + + + + + + + + <% end %> + +
NameDescriptionCalendarActiveActions
<%= advisor.name %><%= advisor.description %><%= link_to advisor.calendar, advisor.calendar, target: "_blank" %><%= advisor.active ? "Yes" : "No" %> + + <%= link_to "Edit", edit_advisor_path(advisor), class: "btn btn-primary btn-sm" %> + <%= link_to "Delete", delete_advisor_path(advisor), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-danger btn-sm" %> +
+ +
+ <%= link_to "Back to Admin Dashboard", admins_path, class: "btn btn-secondary" %> + <%= link_to "Add New Advisor", advisors_new_path, class: "btn btn-success mb-3" %> +
+
+ + + <%= javascript_include_tag 'https://code.jquery.com/jquery-3.6.0.min.js' %> + <%= javascript_include_tag 'https://cdn.datatables.net/1.13.4/js/jquery.dataTables.min.js' %> + + + \ No newline at end of file diff --git a/app/views/admins/manage_user_roles.html.erb b/app/views/admins/manage_user_roles.html.erb new file mode 100644 index 000000000..8f072038c --- /dev/null +++ b/app/views/admins/manage_user_roles.html.erb @@ -0,0 +1,53 @@ + + + <%= stylesheet_link_tag 'https://cdn.datatables.net/1.13.4/css/jquery.dataTables.min.css' %> + + +
+
+ <%= render "shared/flash_messages" %> +
+

Manage User Roles

+
+ + + + + + + + + + + + + + <% @users.each do |user| %> + + + + + + + + + + <% end %> + +
SIDNameEmailIs Student?Is Staff?Is Admin?Actions
<%= user.sid %><%= user.name%><%= user.email %><%= user.is_student ? "Yes" : "No" %><%= user.is_staff ? "Yes" : "No" %><%= user.is_admin ? "Yes" : "No" %><%= link_to "Edit", edit_user_role_path(user), class: "btn btn-primary btn-sm" %>
+
+
+
+ <%= link_to "Back to Admin Dashboard".html_safe, admins_path, class: "btn btn-secondary" %> +
+
+ + <%= javascript_include_tag 'https://code.jquery.com/jquery-3.6.0.min.js' %> + <%= javascript_include_tag 'https://cdn.datatables.net/1.13.4/js/jquery.dataTables.min.js' %> + + + \ No newline at end of file diff --git a/app/views/admins/scholarships/_form.html.erb b/app/views/admins/scholarships/_form.html.erb new file mode 100644 index 000000000..f7864462e --- /dev/null +++ b/app/views/admins/scholarships/_form.html.erb @@ -0,0 +1,37 @@ +<%= form_with(model: scholarship, url: scholarship.persisted? ? update_scholarship_path(scholarship) : create_scholarship_path, method: scholarship.persisted? ? :patch : :post, local: true) do |form| %> + <% if scholarship.errors.any? %> +
+
<%= pluralize(scholarship.errors.count, "error") %> prohibited this scholarship from being saved:
+ +
+ <% end %> + +
+ <%= form.label :name, class: "form-label fw-bold" %> + <%= form.text_field :name, class: "form-control", placeholder: "Enter scholarship name" %> +
+ +
+ <%= form.label :description, class: "form-label fw-bold" %> + <%= form.text_area :description, class: "form-control", rows: 5, placeholder: "Enter scholarship description" %> +
+ +
+ <%= form.label :status_text, class: "form-label fw-bold" %> + <%= form.text_field :status_text, class: "form-control", placeholder: "e.g., Open, Closed, Coming Soon" %> +
+ +
+ <%= form.label :application_url, class: "form-label fw-bold" %> + <%= form.url_field :application_url, class: "form-control", placeholder: "https://example.com/apply" %> +
+ +
+ <%= form.submit class: "btn btn-primary" %> + <%= link_to "Cancel", manage_scholarships_path, class: "btn btn-secondary" %> +
+<% end %> \ No newline at end of file diff --git a/app/views/admins/scholarships/edit.html.erb b/app/views/admins/scholarships/edit.html.erb new file mode 100644 index 000000000..baf468d76 --- /dev/null +++ b/app/views/admins/scholarships/edit.html.erb @@ -0,0 +1,27 @@ + + + <%= stylesheet_link_tag 'https://cdn.datatables.net/1.13.4/css/jquery.dataTables.min.css' %> + + +
+
+ <%= render "shared/flash_messages" %> +

Edit Scholarship

+
+ +
+
+
+
+ <%= render 'admins/scholarships/form', scholarship: @scholarship %> +
+
+ +
+ <%= link_to "Back to Scholarships", manage_scholarships_path, class: "btn btn-secondary" %> +
+
+
+
+ + \ No newline at end of file diff --git a/app/views/admins/scholarships/index.html.erb b/app/views/admins/scholarships/index.html.erb new file mode 100644 index 000000000..e9141401c --- /dev/null +++ b/app/views/admins/scholarships/index.html.erb @@ -0,0 +1,78 @@ + + +
+

Scholarships & Awards



+
+

Scholarship Opportunities for Re-entry Students

+

The Re-entry Student Program offers two scholarships: Crankstart for newly admitted students, + and Osher for continuing students.

+

Applications for the 2024 Osher Scholarship opened in May 2024 and closed in July 2024. + We will open the Osher Scholarship for continuing students again in May 2025.

+

Applications for the 2024 Crankstart Scholarship opened in August 2024 and + closed in October 2024. We will open the Crankstart Scholarship for new students again in August 2025.

+
+

+ + <% @scholarships.each do |scholarship| %> +
<%# Add margin-bottom for spacing between scholarships %> +
+

<%= scholarship.name %>

+
+
+ <%= simple_format(scholarship.description) %> + + <%# Display the status text and link %> + <% if scholarship.application_url.present? %> + <%= link_to scholarship.status_text.presence || "Apply Now", + scholarship.application_url, + class: "btn btn-info", + target: "_blank" %> + <% else %> +

<%= scholarship.status_text.presence || "Information not available" %>

+ <% end %> +
+
+
+
+ <% end %> + + <% if @scholarships.empty? %> +
+

There are currently no scholarships listed.

+
+ <% end %> + +
+
+
+ <%= link_to "Back", root_path, method: :get, class:"btn btn-secondary btn-lg my-4", style:"color:#003262" %> +
+ + diff --git a/app/views/admins/scholarships/manage.html.erb b/app/views/admins/scholarships/manage.html.erb new file mode 100644 index 000000000..3996ed3fe --- /dev/null +++ b/app/views/admins/scholarships/manage.html.erb @@ -0,0 +1,77 @@ + + + <%= stylesheet_link_tag 'https://cdn.datatables.net/1.13.4/css/jquery.dataTables.min.css' %> + + +
+
+ <%= render "shared/flash_messages" %> +
+

Manage Scholarships

+ +
+ <%= link_to "Export", export_scholarships_path(format: :csv), class: "btn btn-secondary" %> +
+ + + + + + + + + + + + + <% @scholarships.each do |scholarship| %> + + + + + + + + <% end %> + +
NameDescriptionStatusApplication URLActions
<%= scholarship.name %><%= truncate(scholarship.description, length: 100) %><%= scholarship.status_text %> + <% if scholarship.application_url.present? %> + <%= link_to "View Link", scholarship.application_url, target: "_blank", class: "btn btn-sm btn-outline-primary" %> + <% else %> + No URL + <% end %> + +
+ <%= link_to "Edit", edit_scholarship_path(scholarship), class: "btn btn-warning btn-sm me-2" %> + <%= button_to "Delete", destroy_scholarship_path(scholarship), + method: :delete, + data: { turbo_confirm: "Are you sure you want to delete this scholarship?" }, + class: "btn btn-danger btn-sm" %> +
+
+ +
+ <%= link_to "Add New Scholarship", new_scholarship_path, class: "btn btn-primary me-2" %> + <%= link_to "Back to Admin Dashboard", admins_path, class: "btn btn-secondary" %> +
+
+ + <%= javascript_include_tag 'https://code.jquery.com/jquery-3.6.0.min.js' %> + <%= javascript_include_tag 'https://cdn.datatables.net/1.13.4/js/jquery.dataTables.min.js' %> + + + \ No newline at end of file diff --git a/app/views/admins/scholarships/new.html.erb b/app/views/admins/scholarships/new.html.erb new file mode 100644 index 000000000..b053e8e19 --- /dev/null +++ b/app/views/admins/scholarships/new.html.erb @@ -0,0 +1,27 @@ + + + <%= stylesheet_link_tag 'https://cdn.datatables.net/1.13.4/css/jquery.dataTables.min.css' %> + + +
+
+ <%= render "shared/flash_messages" %> +

New Scholarship

+
+ +
+
+
+
+ <%= render 'admins/scholarships/form', scholarship: @scholarship %> +
+
+ +
+ <%= link_to "Back to Scholarships", manage_scholarships_path, class: "btn btn-secondary" %> +
+
+
+
+ + \ No newline at end of file diff --git a/app/views/advisors/edit.html.erb b/app/views/advisors/edit.html.erb new file mode 100644 index 000000000..55fcee4d1 --- /dev/null +++ b/app/views/advisors/edit.html.erb @@ -0,0 +1,23 @@ +
+

Edit Advisor

+ <%= form_with model: @advisor, local: true do |form| %> +
+ <%= form.label :name, class: "form-label" %> + <%= form.text_field :name, class: "form-control" %> +
+
+ <%= form.label :description, class: "form-label" %> + <%= form.text_area :description, class: "form-control" %> +
+
+ <%= form.label :calendar, class: "form-label" %> + <%= form.text_field :calendar, class: "form-control" %> +
+
+ <%= form.label :active, class: "form-label" %> + <%= form.check_box :active, class: "form-check-input" %> +
+ <%= form.submit "Save Changes", class: "btn btn-primary" %> + <%= link_to "Cancel", manage_advisors_path, class: "btn btn-secondary" %> + <% end %> +
\ No newline at end of file diff --git a/app/views/advisors/new.html.erb b/app/views/advisors/new.html.erb new file mode 100644 index 000000000..7a47a4271 --- /dev/null +++ b/app/views/advisors/new.html.erb @@ -0,0 +1,23 @@ +
+

Add New Advisor

+ <%= form_with model: @advisor, local: true do |form| %> +
+ <%= form.label :name, class: "form-label" %> + <%= form.text_field :name, class: "form-control" %> +
+
+ <%= form.label :description, class: "form-label" %> + <%= form.text_area :description, class: "form-control" %> +
+
+ <%= form.label :calendar, class: "form-label" %> + <%= form.text_field :calendar, class: "form-control" %> +
+
+ <%= form.label :active, class: "form-label" %> + <%= form.check_box :active, class: "form-check-input" %> +
+ <%= form.submit "Create Advisor", class: "btn btn-primary" %> + <%= link_to "Cancel", manage_advisors_path, class: "btn btn-secondary" %> + <% end %> +
\ No newline at end of file diff --git a/app/views/appointments/advisors.html.erb b/app/views/appointments/advisors.html.erb index da9bc554b..a112fbda0 100644 --- a/app/views/appointments/advisors.html.erb +++ b/app/views/appointments/advisors.html.erb @@ -1,18 +1,24 @@ -
-

Create an Appointment with a Counselor



-
-

Amanda

- -

Trinh

- -
-
-
- <%= link_to "Back", root_path, method: :get, class:"btn btn-secondary btn-lg my-4" %> -
+ +
+

+

Make an Appointment with a Counselor

+

+
+ <% @advisors.each do |advisor| %> + <% if advisor.active %> +
+

<%= advisor.name %>

+

<%= advisor.description %>

+ +
+ <% end %> + <% end %>
-
\ No newline at end of file +
+
+ <%= link_to "Back", root_path, method: :get, class: "btn btn-secondary btn-lg my-4", style: "color:#003262" %> +
+
+ \ No newline at end of file diff --git a/app/views/checkin/new.html.erb b/app/views/checkin/new.html.erb index 393b9bafb..23ebd3b62 100644 --- a/app/views/checkin/new.html.erb +++ b/app/views/checkin/new.html.erb @@ -1,15 +1,26 @@ -
+ +
-

Check-in to the Berkeley Reentry
Student Program Study Space

- <%= form_with model: Checkin.new, url: checkin_path do |f| %> +

Check-in to the
Berkeley RSP
Community Space

+ <%= form_with model: @checkin, url: checkin_path, local: true do |f| %>
<%= f.label :reason, "Please select a reason for check-in today ✅".html_safe, class: "form-label" %> - <%= f.select :reason, @reasons, { :include_blank => 'Select One' }, {:required => true, class: "form-select form-control form-select-lg"} %> + <%= select_tag "checkin[reason]", + options_for_select(@reasons, @default_reason), + class: "form-select form-control form-select-lg", + id: "checkin_reason", + required: true, + data: { default: @default_reason } %>
- <%= link_to "Back", root_path, method: :get, class: "btn btn-secondary btn-lg" %> - <%= f.submit "Submit", class: "btn btn-primary btn-lg ms-2" %> - <% end %> +
+
+ <%= link_to "Back", root_path, class: "btn btn-secondary btn-lg", style: "color:#003262" %> + <%= f.submit "Submit", class: "btn btn-primary btn-lg ms-2 berkeley-color" %> +
+
+ <% end %>
+ diff --git a/app/views/courses/index.html.erb b/app/views/courses/index.html.erb new file mode 100644 index 000000000..7b35c0496 --- /dev/null +++ b/app/views/courses/index.html.erb @@ -0,0 +1,45 @@ + +
+

Courses

+ + <% if @courses.empty? %> +
+ No courses are currently available. Please check back later. +
+ <% else %> + <% @courses.each do |course| %> +
+
+

<%= course.title %> (<%= course.code %>)

+
+
+
+
+

<%= course.description %>

+ +
+ Units: <%= course.units %>
+ <% if course.semester.present? %> + Semester: <%= course.semester %>
+ Schedule: <%= course.schedule %> + <% if course.ccn.present? %> (CCN#: <%= course.ccn %>)<% end %>
+ Location: <%= course.location %> + <% end %> +
+
+
+ <% if course.code == "L&S 198" %> + Course information + <% end %> +
+
+
+
+ <% end %> + <% end %> + +
+ <%= link_to "Back", root_path, class: "btn btn-secondary btn-lg", style: "color: white;" %> +
+
+ diff --git a/app/views/events/index.html.erb b/app/views/events/index.html.erb new file mode 100644 index 000000000..8a90f4033 --- /dev/null +++ b/app/views/events/index.html.erb @@ -0,0 +1,51 @@ +
+

Events

+ + <% if @upcoming_events.present? %> +

Upcoming Events

+ + <% @upcoming_events.each do |event| %> +
+
+

<%= event.title %>

+

<%= event.description %>

+ +
+

Details:

+
    +
  • Date: <%= event.formatted_date %>
  • +
  • Time: <%= event.formatted_time %>
  • +
  • Location: <%= event.location %>
  • +
+
+
+
+ <% end %> + <% else %> + + <% end %> + + <% if @past_events.present? %> +

Past Events

+
+
+ <% @past_events.each do |event| %> +

<%= event.title %>

+

<%= event.formatted_date %>, <%= event.formatted_time %>

+

Location: <%= event.location %>

+

<%= event.description %>

+ <% unless event == @past_events.last %> +
+ <% end %> + <% end %> +
+
+ <% end %> + +
+
+ <%= link_to "Back", root_path, class: "btn btn-secondary btn-lg", style: "color:#003262" %> +
+
\ No newline at end of file diff --git a/app/views/layouts/action_text/contents/_content.html.erb b/app/views/layouts/action_text/contents/_content.html.erb new file mode 100644 index 000000000..9e3c0d0df --- /dev/null +++ b/app/views/layouts/action_text/contents/_content.html.erb @@ -0,0 +1,3 @@ +
+ <%= yield -%> +
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index f418df856..6539177cc 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -14,6 +14,7 @@ + <%= render 'shared/navbar' %> <%= yield %> diff --git a/app/views/pages/index.html.erb b/app/views/pages/index.html.erb index fdc81bc0b..74b5fa423 100644 --- a/app/views/pages/index.html.erb +++ b/app/views/pages/index.html.erb @@ -1,31 +1,126 @@ + + + +
-
- <%= render "shared/flash_messages" %> - <%= image_tag("reentry-owl.jpg", width: 130, height: 130, class: "d-block rounded mx-auto mb-4") %> - <% if @logged_out %> -

Welcome to the Berkeley
Reentry Student Program

-
- <%= button_to '/auth/google_oauth2', class: "btn btn-primary btn-lg" do %> - Google sign-in - Login with Google - <% end %> -
- <% else %> -

Welcome to the Berkeley
Reentry Student Program,

-

<%= sanitize @name %>

- <% if @user_type.include? 'Admin' %> +
+ <%= render "shared/flash_messages" %> + <%= image_tag("new_logo.png", width: 300, height: 130, class: "d-block rounded mx-auto mb-4", alt: "re-entry student program logo") %> + <% if @logged_out %> +

Welcome to the Berkeley
Re-entry Student Program

+
+ <%= button_to '/auth/google_oauth2', class: "btn btn-primary btn-lg" do %> + Google sign-in + Login with Google + <% end %> + <%= button_to canvas_login_path, class: "btn btn-primary btn-lg" do %> + Canvas sign-in + Login with Canvas + <% end %> + <% if ENV["MOCK_CANVAS_LOGIN"] == "true" %> + <%= link_to "👑 Mock Admin Login", canvas_callback_path(mock_role: "admin"), class: "btn btn-secondary btn-lg" %> + + <%= link_to "🎓 Mock Student Login", canvas_callback_path(mock_role: "student"), class: "btn btn-secondary btn-lg" %> + <% end %> +
+ <% else %> +

Welcome to the Berkeley
Re-entry Student Program,

+

<%= sanitize @name %>

+ <% if @user_type.include? 'Admin' %> <%= link_to "Go to admin dashboard".html_safe, admins_path, class: "btn btn-primary mt-1 btn-lg px-4 gap-3" %> - <% elsif @user_type.include? 'Staff' %> + <% elsif @user_type.include? 'Staff' %> <%# pending %> - <% else %> + <% else %>
-

Check-in using this application, find and make appointments with staff, find news and updates 💫

- <%= link_to "Check-in to Berkeley
Reentry Student Center".html_safe, checkin_path, class: "btn btn-primary mt-1 btn-lg px-4 gap-3" %> - <%= link_to "Make an Appointment
With a Counselor".html_safe, appointments_path, class: "btn btn-primary mt-1 btn-lg px-4 gap-3" %> +

Check-in using this application, find and make appointments with staff, find news and updates 💫

+ <%= link_to "Check-in to the Berkeley
RSP Community Space".html_safe, checkin_path, class: "btn btn-primary mt-1 btn-lg px-4 gap-3", style:"background-color: #003262; border-color: #003262;" %> + <%= link_to "Make an Appointment
With a Counselor".html_safe, appointments_path, class: "btn btn-primary mt-1 btn-lg px-4 gap-3", style:"background-color: #003262; border-color: #003262;" %> + <%= link_to "Re-entry Scholarships".html_safe, scholarships_path, class: "btn btn-primary mt-1 btn-lg px-4 gap-3", style:"background-color: #003262; border-color: #003262;"%> + <%= link_to "Our Courses".html_safe, courses_path, class: "btn btn-primary mt-1 btn-lg px-4 gap-3", style:"background-color: #003262; border-color: #003262;"%>
- <%= link_to "Edit Profile Information".html_safe, user_profile_edit_path(:edit_profile => true), class: "btn btn-secondary btn-lg px-4 my-4 gap-3" %> - <% end %> - <%= link_to "Logout", logout_path, class: "btn btn-secondary btn-lg my-4" %> - <% end %> -
+ <%= link_to "Edit Profile Information".html_safe, user_profile_edit_path(:edit_profile => true), class: "btn btn-secondary btn-lg px-4 my-4 gap-3", style:"color: #003262;" %> + <% end %> + <%= link_to "Logout", logout_path, class: "btn btn-secondary btn-lg my-4", style:"color: #003262;" %> +
+ <% end %>
+
+
+ +
+ diff --git a/app/views/podcasts/index.html.erb b/app/views/podcasts/index.html.erb new file mode 100644 index 000000000..87ade889a --- /dev/null +++ b/app/views/podcasts/index.html.erb @@ -0,0 +1,46 @@ + +
+
+
+
+ Berkeley Re-Entry Student Program Podcast +
+
+

About the Podcast

+

+ The Berkeley Re-Entry Student Program Podcast is committed to offering helpful insights, guidance, and support specifically for re-entry students at UC Berkeley. Our aim is to build a friendly, informative space where these students can exchange stories, learn from each other, and ultimately succeed in their academic goals. +

+

+ Through conversations with professors, staff, and fellow re-entry students, we cover a diverse range of topics from effective study methods to career growth. Additionally, our podcast emphasizes the unique challenges faced by re-entry students, providing practical solutions and strategies to help them overcome these hurdles. +

+ Listen Now +
+
+
+
+ +
+
+

Latest Episodes

+
+
+
+ Example Episode Title +
+
Example Episode Title
+

Example episode description goes here. This is just a brief overview of the episode's content.

+ Listen Now +
+
+
+
+
+
+ +
+

© <%= Time.now.year %> Berkeley Re-Entry Student Program Podcast. All rights reserved.

+
+ + + + diff --git a/app/views/scholarships/index.html.erb b/app/views/scholarships/index.html.erb new file mode 100644 index 000000000..6a970e089 --- /dev/null +++ b/app/views/scholarships/index.html.erb @@ -0,0 +1,50 @@ + +
+

Scholarships & Awards



+
+

Scholarship Opportunities for Re-entry Students

+

The Re-entry Student Program offers two scholarships: Crankstart for newly admitted students, + and Osher for continuing students.

+

Applications for the 2024 Osher Scholarship opened in May 2024 and closed in July 2024. + We will open the Osher Scholarship for continuing students again in May 2025.

+

Applications for the 2024 Crankstart Scholarship opened in August 2024 and + closed in October 2024. We will open the Crankstart Scholarship for new students again in August 2025.

+
+

+ + <% @scholarships.each do |scholarship| %> +
<%# Add margin-bottom for spacing between scholarships %> +
+

<%= scholarship.name %>

+
+
+ <%= simple_format(scholarship.description) %> + + <%# Display the status text and link %> + <% if scholarship.application_url.present? %> + <%= link_to scholarship.status_text.presence || "Apply Now", + scholarship.application_url, + class: "btn btn-info", + target: "_blank" %> + <% else %> +

<%= scholarship.status_text.presence || "Information not available" %>

+ <% end %> +
+
+
+
+ <% end %> + + <% if @scholarships.empty? %> +
+

There are currently no scholarships listed.

+
+ <% end %> + +
+
+
+ <%= link_to "Back", root_path, method: :get, class:"btn btn-secondary btn-lg my-4", style:"color:#003262" %> +
+
+ diff --git a/app/views/shared/_navbar.html.erb b/app/views/shared/_navbar.html.erb new file mode 100755 index 000000000..db2ef164e --- /dev/null +++ b/app/views/shared/_navbar.html.erb @@ -0,0 +1,27 @@ + diff --git a/app/views/users/_profile_form.html.erb b/app/views/users/_profile_form.html.erb index 878ac45ef..36d2250a9 100644 --- a/app/views/users/_profile_form.html.erb +++ b/app/views/users/_profile_form.html.erb @@ -25,8 +25,8 @@
<% if params[:edit_profile] %> - <%= link_to "Back", root_path, class: "btn btn-secondary btn-lg ms-2 my-4" %> - <%= f.submit "Submit", name: "submit_edit", class: "btn btn-primary btn-lg ms-2 my-4" %> + <%= link_to "Back", root_path, class: "btn btn-secondary btn-lg ms-2 my-4", style:"color:#003262" %> + <%= f.submit "Submit", name: "submit_edit", class: "btn btn-primary btn-lg ms-2 my-4 berkeley-color", style:"border-color:#003262" %> <% else %> <%= f.submit "Skip", name: "skip", class: "btn btn-secondary btn-lg ms-2 my-4" %> <%= f.submit "Submit", name: "submit", class: "btn btn-primary btn-lg ms-2 my-4" %> diff --git a/app/views/users/edit_user_role.html.erb b/app/views/users/edit_user_role.html.erb new file mode 100644 index 000000000..783ed6241 --- /dev/null +++ b/app/views/users/edit_user_role.html.erb @@ -0,0 +1,31 @@ +
+

Edit Advisor

+ <%= form_with model: @user, url: update_user_role_user_path(@user), method: :patch do |form| %> +
+ <%= form.label :sid, class: "form-label" %> + <%= form.text_field :sid, class: "form-control", :readonly => true %> +
+
+ <%= form.label :name, class: "form-label" %> + <%= form.text_field :name, class: "form-control", :readonly => true %> +
+
+ <%= form.label :email, class: "form-label" %> + <%= form.text_field :email, class: "form-control", :readonly => true %> +
+
+ <%= form.label :is_student, class: "form-label" %> + <%= form.check_box :is_student, class: "form-check-input" %> +
+
+ <%= form.label :is_staff, class: "form-label" %> + <%= form.check_box :is_staff, class: "form-check-input" %> +
+
+ <%= form.label :is_admin, class: "form-label" %> + <%= form.check_box :is_admin, class: "form-check-input" %> +
+ <%= form.submit "Update Role", class: "btn btn-primary" %> + <%= link_to "Cancel", manage_user_roles_path, class: "btn btn-secondary" %> + <% end %> +
\ No newline at end of file diff --git a/config.ru b/config.ru index 6dc832180..2e0308469 100644 --- a/config.ru +++ b/config.ru @@ -2,7 +2,7 @@ # This file is used by Rack-based servers to start the application. -require_relative 'config/environment' +require_relative "config/environment" run Rails.application Rails.application.load_server diff --git a/config/application.rb b/config/application.rb index 285296f77..d8760f280 100644 --- a/config/application.rb +++ b/config/application.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -require_relative 'boot' +require_relative "boot" -require 'rails/all' +require "rails/all" # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. @@ -11,7 +11,7 @@ module BerkeleyReentryStudentProgram class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 6.1 + config.load_defaults 7.0 # Configuration for the application, engines, and railties goes here. # diff --git a/config/boot.rb b/config/boot.rb index c04863fa7..aef6d031e 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) -require 'bundler/setup' # Set up gems listed in the Gemfile. -require 'bootsnap/setup' # Speed up boot time by caching expensive operations. +require "bundler/setup" # Set up gems listed in the Gemfile. +require "bootsnap/setup" # Speed up boot time by caching expensive operations. diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc index bc2908f6e..d08ded3af 100644 --- a/config/credentials.yml.enc +++ b/config/credentials.yml.enc @@ -1 +1 @@ -PXAWznRCa759zj22egyT+rQv8XitGnFsqSkkxIfCEMXWeb3pfSSNilEWgzK1gPB/Aoxz02YQ0TXGL76kQpCLoS+bNG6rH1mTY64bQszY8BFTx2v7HOzrelWBkn4J6WWBC9J37g/GpIFVBuxXfeTxMXnEZcmaP0/QeipahxbhLJmSvvfOTYk4iOe1jd6x70MmXxfiQY+Vss9c1gF1fddo7v2SBvZqIZRb0W9JzMmfyRMLFwIEJozW/wRU1tfWAausNYvZrXVp7tCVN+921JfLBZeIFWpR9o3ehox3wrvPsZ87KZzWWg8VOn4FsLNWZwXpsXwoSgbwE44pxX1IQ7tSKX8qrGXogH11PL4xULFFkoiQu+GQxcdU+3xHH4U+DxF05jaoLdVeSMF5hnFNsOGVNlyIbIV17fTmgyXVW44uhHUji3spqEqPw9ksuAM8qiQ+AXzrABMHQrA8ozc1n2g5fhtiENYyyjlMf+HQ9iSEDNUEEFkQKADBLYxZLNlzC1LmY+rD7isO2d5YIiVWLm51XnzBdrYw/mdlz86fXc9wWVXNTUeaHPwkprhEaOGe7F+mrcQMoPiOejjAmexZhvsU3+gwJvVP79KZbrd0tr+qtHDLOUHIGTFVq5Zi6RLDYRqBRHjJGHoPnxo60jcDRi9FdKuCd/eVeQQnMPffKkYy5Y1WhqN33bbp9aqzEWHI+MpEVNpwyR0DS8ngTOW9xmvpWjBUJXI4IblFiPclYVkr1Uew99qyq/AJfLToNNF3dEqqk8ZRIFat73uT4ncoCat3suOUxfBn/kS3--U+JDSZvITr/BRO34--y09R5gi2Z/bR3nVs09H/mg== \ No newline at end of file +BOI5Cc0GTNUs7k9FkoqsFn/IfJijD4p8PN80ZNPmJhGPNbYdAA5GVftm2BJErn2muF5rlfy5r6i7NF4wZCkTDJUvY2L09G3HNYNRxoXGqjz4FbdIoxLWow4H9nOOAlDN2eX1zh6aqzQl0cV0paO5m3+xjC2uI8DHJESjLim/Z+oEymhvHo+9b4CLNChGQAaV8ypx4CIIQlyUhVD4WZ4RgHvvVxPXoiPyeEa433DZza+IQwn/YdBi03ndDM/rmeKuPeoV0swseO8svy47lCybPdSLDvAhl5XLFQ0JNI670U+b0YGpp9ZDub4ftF2E4tzUU4c8w3IFyMxGr91tWOpYPqCyA7SfCDD0gDB4T3Cfc9CAjZon06JoFz/HNpeLgDsRZ8RcDEXtojNz3jLYXBnvfCzEUTv6Tj34thXdDwZHD2t7CgUcrtjC2LYQt34CUUDKy7upAK6tcWamGMCQgL1caw8EeKcfPJv6N9MejXqFum19/EpzLSGsXHH285JTDqxYB80vXmI1WdcqXIc9B3OkKNTjZh5jKXo5otaTFhi7ubcbMmBNMYZuMkiDcnHG76858P7gZdtL22370U8sOyuSOtrVBy85VAz3SMHxFcoCU10EkRXMyWZv7VOd9bdmOW/C9c+ijYD3WaHjJCodT7/RT+oqhJx0rtl1L44T5UkpxD0pPa4xwYpUjaCqPMuXmbVOVWLCxW3owZsKxp9bCPkMJ+wRhLcUgK+r7INN1pGs3Q1gP9ASFp4JTHA1v8tVim2o8dok5rWR/cutX6E0nBnW+g9u5cIRbRjb7dT+mezSnJbuWSXdtqzy0nHt0QCaS7FtrOVGA2V2GfvmIMlHx5ttKgcCNlhEIaaM0exx0lDfThTuOQjz76jTZjALHohuN3HWnkNoq8v10kfdhQtfa9CknM7cFO1/21Ou7CkZAuCOb+SBAz5vV7mMrGoG0Qk/v4mz+hFuzIZvPBrO6AwPQgxacQ4c+lDNHJ8pMQL1V4DOZ5I6Q03S7Nw5d8yeECoSpy4pM9U47oh9XFLshizn8mVk0WwVcR+Dh1an3vjxI7u/pVRdVvqtyQ5IKxoiRhXj6Pvi4NyWqZgyI6uksfOr+7RH1A3YhtxAc+YBPbcG43QpArSQkN27kXXDJDko7m5pVnqGgS6DhTw0ssAbZYGGuDHGOS4=--eVA6oBBGdhujdZ22--ayZX9moTHiscr+0DTA+hTA== \ No newline at end of file diff --git a/config/credentials.yml.enc.bak b/config/credentials.yml.enc.bak new file mode 100644 index 000000000..bc2908f6e --- /dev/null +++ b/config/credentials.yml.enc.bak @@ -0,0 +1 @@ +PXAWznRCa759zj22egyT+rQv8XitGnFsqSkkxIfCEMXWeb3pfSSNilEWgzK1gPB/Aoxz02YQ0TXGL76kQpCLoS+bNG6rH1mTY64bQszY8BFTx2v7HOzrelWBkn4J6WWBC9J37g/GpIFVBuxXfeTxMXnEZcmaP0/QeipahxbhLJmSvvfOTYk4iOe1jd6x70MmXxfiQY+Vss9c1gF1fddo7v2SBvZqIZRb0W9JzMmfyRMLFwIEJozW/wRU1tfWAausNYvZrXVp7tCVN+921JfLBZeIFWpR9o3ehox3wrvPsZ87KZzWWg8VOn4FsLNWZwXpsXwoSgbwE44pxX1IQ7tSKX8qrGXogH11PL4xULFFkoiQu+GQxcdU+3xHH4U+DxF05jaoLdVeSMF5hnFNsOGVNlyIbIV17fTmgyXVW44uhHUji3spqEqPw9ksuAM8qiQ+AXzrABMHQrA8ozc1n2g5fhtiENYyyjlMf+HQ9iSEDNUEEFkQKADBLYxZLNlzC1LmY+rD7isO2d5YIiVWLm51XnzBdrYw/mdlz86fXc9wWVXNTUeaHPwkprhEaOGe7F+mrcQMoPiOejjAmexZhvsU3+gwJvVP79KZbrd0tr+qtHDLOUHIGTFVq5Zi6RLDYRqBRHjJGHoPnxo60jcDRi9FdKuCd/eVeQQnMPffKkYy5Y1WhqN33bbp9aqzEWHI+MpEVNpwyR0DS8ngTOW9xmvpWjBUJXI4IblFiPclYVkr1Uew99qyq/AJfLToNNF3dEqqk8ZRIFat73uT4ncoCat3suOUxfBn/kS3--U+JDSZvITr/BRO34--y09R5gi2Z/bR3nVs09H/mg== \ No newline at end of file diff --git a/config/database.yml b/config/database.yml index 089d4a4cc..97de874fa 100644 --- a/config/database.yml +++ b/config/database.yml @@ -24,6 +24,8 @@ default: &default development: <<: *default database: berkeley_reentry_student_program_development + username: <%= ENV["berkeley_reentry_student_program"] %> + password: <%= ENV["BERKELEY_REENTRY_STUDENT_PROGRAM_DATABASE_PASSWORD"] %> # The specified database role being used to connect to postgres. # To create additional roles in postgres see `$ createuser --help`. diff --git a/config/environment.rb b/config/environment.rb index d5abe5580..7df99e89c 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # Load the Rails application. -require_relative 'application' +require_relative "application" # Initialize the Rails application. Rails.application.initialize! diff --git a/config/environments/development.rb b/config/environments/development.rb index 84a57f401..c8758ceed 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'active_support/core_ext/integer/time' +require "active_support/core_ext/integer/time" Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. @@ -21,13 +21,13 @@ # Enable/disable caching. By default caching is disabled. # Run rails dev:cache to toggle caching. - if Rails.root.join('tmp/caching-dev.txt').exist? + if Rails.root.join("tmp/caching-dev.txt").exist? config.action_controller.perform_caching = true config.action_controller.enable_fragment_cache_logging = true config.cache_store = :memory_store config.public_file_server.headers = { - 'Cache-Control' => "public, max-age=#{2.days.to_i}" + "Cache-Control" => "public, max-age=#{2.days.to_i}" } else config.action_controller.perform_caching = false diff --git a/config/environments/production.rb b/config/environments/production.rb index aa924f921..977a6fe85 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'active_support/core_ext/integer/time' +require "active_support/core_ext/integer/time" Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. @@ -24,7 +24,7 @@ # Disable serving static files from the `/public` folder by default since # Apache or NGINX already handles this. - config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? + config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present? # Compress CSS using a preprocessor. # config.assets.css_compressor = :sass @@ -84,7 +84,7 @@ # require "syslog/logger" # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name") - if ENV['RAILS_LOG_TO_STDOUT'].present? + if ENV["RAILS_LOG_TO_STDOUT"].present? logger = ActiveSupport::Logger.new($stdout) logger.formatter = config.log_formatter config.logger = ActiveSupport::TaggedLogging.new(logger) diff --git a/config/environments/test.rb b/config/environments/test.rb index 9908fbcc0..97f436b35 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'active_support/core_ext/integer/time' +require "active_support/core_ext/integer/time" # The test environment is used exclusively to run your application's # test suite. You never need to work with it otherwise. Remember that @@ -9,8 +9,8 @@ Rails.application.configure do # set testing ENV variables - ENV['ADMINS'] = 'google_admin@berkeley.edu' - ENV['STAFF'] = 'google_staff@berkeley.edu' + Rails.application.credentials[:ADMINS] = "google_admin@berkeley.edu" + Rails.application.credentials[:STAFF] = "google_staff@berkeley.edu" # Settings specified here will take precedence over those in config/application.rb. # Turn false under Spring and add config.action_view.cache_template_loading = true. @@ -19,12 +19,12 @@ # Eager loading loads your whole application. When running a single test locally, # this probably isn't necessary. It's a good idea to do in a continuous integration # system, or in some way before deploying your code. - config.eager_load = ENV['CI'].present? + config.eager_load = ENV["CI"].present? # Configure public file server for tests with Cache-Control for performance. config.public_file_server.enabled = true config.public_file_server.headers = { - 'Cache-Control' => "public, max-age=#{1.hour.to_i}" + "Cache-Control" => "public, max-age=#{1.hour.to_i}" } # Show full error reports and disable caching. diff --git a/config/importmap.rb b/config/importmap.rb index b57e7beb6..48143554c 100644 --- a/config/importmap.rb +++ b/config/importmap.rb @@ -2,8 +2,10 @@ # Pin npm packages by running ./bin/importmap -pin 'application', preload: true -pin '@hotwired/turbo-rails', to: 'turbo.min.js', preload: true -pin '@hotwired/stimulus', to: 'stimulus.min.js', preload: true -pin '@hotwired/stimulus-loading', to: 'stimulus-loading.js', preload: true -pin_all_from 'app/javascript/controllers', under: 'controllers' +pin "application", preload: true +pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true +pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true +pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true +pin_all_from "app/javascript/controllers", under: "controllers" +pin "trix" +pin "@rails/actiontext", to: "actiontext.esm.js" diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index 7f32f226d..8283182b4 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -3,7 +3,7 @@ # Be sure to restart your server when you modify this file. # Version of your assets, change this if you want to expire all your assets. -Rails.application.config.assets.version = '1.0' +Rails.application.config.assets.version = "1.0" # Add additional assets to the asset load path. # Rails.application.config.assets.paths << Emoji.images_path diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb index f37ed8de4..96bd22282 100644 --- a/config/initializers/content_security_policy.rb +++ b/config/initializers/content_security_policy.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + # Be sure to restart your server when you modify this file. # Define an application-wide content security policy diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index 6c78420e7..9e049dcc9 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + # Be sure to restart your server when you modify this file. # Add new inflection rules using the following format. Inflections diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb index c2a9a9b41..8de082720 100644 --- a/config/initializers/omniauth.rb +++ b/config/initializers/omniauth.rb @@ -6,21 +6,44 @@ OmniAuth.config.add_mock( :google_oauth2, { - 'provider' => 'google_oauth2', - 'uid' => '1000000000', - 'info' => { - 'name' => 'Google Test Developer', - 'email' => 'google_test@berkeley.edu', - 'first_name' => 'Google', - 'last_name' => 'Test Developer' + "provider" => "google_oauth2", + "uid" => "1000000000", + "info" => { + "name" => "Google Test Developer", + "email" => "google_test@berkeley.edu", + "first_name" => "Google", + "last_name" => "Test Developer" }, - 'credentials' => { - 'token' => 'credentials_token_1234567', - 'refresh_token' => 'credentials_refresh_token_45678' + "credentials" => { + "token" => "credentials_token_1234567", + "refresh_token" => "credentials_refresh_token_45678" + } + } + ) + OmniAuth.config.add_mock( + :oauth2, + { + "provider" => "oauth2", + "uid" => "1000000000", + "info" => { + "name" => "Canvas Test Developer", + "email" => "canvas_test@berkeley.edu", + "first_name" => "Canvas", + "last_name" => "Test Developer" + }, + "credentials" => { + "token" => "mock_canvas_token_123456", + "refresh_token" => "mock_canvas_refresh_token_45678" } } ) end - provider :google_oauth2, ENV['GOOGLE_CLIENT_ID'], ENV['GOOGLE_CLIENT_SECRET'] + provider :google_oauth2, Rails.application.credentials[:GOOGLE_CLIENT_ID], Rails.application.credentials[:GOOGLE_CLIENT_SECRET] + provider :oauth2, Rails.application.credentials[:CANVAS_CLIENT_ID], Rails.application.credentials[:CANVAS_CLIENT_SECRET], + client_options: { + site: Rails.application.credentials[:CANVAS_URL], + authorize_url: "/login/oauth2/auth", + token_url: "/login/oauth2/token" + } end OmniAuth.config.allowed_request_methods = %i[post] diff --git a/config/initializers/permissions_policy.rb b/config/initializers/permissions_policy.rb index 50bcf4ead..810aadeb9 100644 --- a/config/initializers/permissions_policy.rb +++ b/config/initializers/permissions_policy.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + # Define an application-wide HTTP permissions policy. For further # information see https://developers.google.com/web/updates/2018/06/feature-policy # diff --git a/config/newrelic.yml b/config/newrelic.yml new file mode 100644 index 000000000..d39aa6669 --- /dev/null +++ b/config/newrelic.yml @@ -0,0 +1,66 @@ +# +# This file configures the New Relic Agent. New Relic monitors Ruby, Java, +# .NET, PHP, Python, Node, and Go applications with deep visibility and low +# overhead. For more information, visit www.newrelic.com. +# +# Generated October 28, 2022 +# +# This configuration file is custom generated for NewRelic Administration +# +# For full documentation of agent configuration options, please refer to +# https://docs.newrelic.com/docs/agents/ruby-agent/installation-configuration/ruby-agent-configuration + +common: &default_settings + # Required license key associated with your New Relic account. + license_key: ENV["NEW_RELIC_LICENSE_KEY"] + + # Your application name. Renaming here affects where data displays in New + # Relic. For more details, see https://docs.newrelic.com/docs/apm/new-relic-apm/maintenance/renaming-applications + app_name: 'Berkeley ReEntry' + + distributed_tracing: + enabled: true + + # To disable the agent regardless of other settings, uncomment the following: + + # agent_enabled: false + + # Logging level for log/newrelic_agent.log + log_level: info + + application_logging: + # If `true`, all logging-related features for the agent can be enabled or disabled + # independently. If `false`, all logging-related features are disabled. + enabled: true + forwarding: + # If `true`, the agent captures log records emitted by this application. + enabled: true + # Defines the maximum number of log records to buffer in memory at a time. + max_samples_stored: 10000 + metrics: + # If `true`, the agent captures metrics related to logging for this application. + enabled: true + local_decorating: + # If `true`, the agent decorates logs with metadata to link to entities, hosts, traces, and spans. + # This requires a log forwarder to send your log files to New Relic. + # This should not be used when forwarding is enabled. + enabled: false + +# Environment-specific settings are in this section. +# RAILS_ENV or RACK_ENV (as appropriate) is used to determine the environment. +# If your application has other named environments, configure them here. +development: + <<: *default_settings + app_name: 'Berkeley ReEntry (Development)' + +test: + <<: *default_settings + # It doesn't make sense to report to New Relic from automated test runs. + monitor_mode: false + +staging: + <<: *default_settings + app_name: 'Berkeley ReEntry (Staging)' + +production: + <<: *default_settings diff --git a/config/puma.rb b/config/puma.rb index 1713441e5..de5feec98 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -6,25 +6,25 @@ # the maximum value specified for Puma. Default is set to 5 threads for minimum # and maximum; this matches the default thread size of Active Record. # -max_threads_count = ENV.fetch('RAILS_MAX_THREADS', 5) -min_threads_count = ENV.fetch('RAILS_MIN_THREADS') { max_threads_count } +max_threads_count = ENV.fetch("RAILS_MAX_THREADS", 5) +min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } threads min_threads_count, max_threads_count # Specifies the `worker_timeout` threshold that Puma will use to wait before # terminating a worker in development environments. # -worker_timeout 3600 if ENV.fetch('RAILS_ENV', 'development') == 'development' +worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development" # Specifies the `port` that Puma will listen on to receive requests; default is 3000. # -port ENV.fetch('PORT', 3000) +port ENV.fetch("PORT", 3000) # Specifies the `environment` that Puma will run in. # -environment ENV.fetch('RAILS_ENV', 'development') +environment ENV.fetch("RAILS_ENV", "development") # Specifies the `pidfile` that Puma will use. -pidfile ENV.fetch('PIDFILE', 'tmp/pids/server.pid') +pidfile ENV.fetch("PIDFILE", "tmp/pids/server.pid") # Specifies the number of `workers` to boot in clustered mode. # Workers are forked web server processes. If using threads and workers together diff --git a/config/routes.rb b/config/routes.rb index 9402da70d..a0d240c23 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -4,23 +4,81 @@ # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html # Defines the root path route ("/") - root to: 'pages#index' + root to: "pages#index" # route GET /check-in to login controller and #index action - get "checkin", to: "checkin#new" - post "checkin", to: "checkin#create" - #appointments page - get 'appointments', to: "appointments#advisors" + get "checkin", to: "checkin#new", as: "new_checkin" + post "checkin", to: "checkin#create", as: "checkin" + # appointments page + get "appointments", to: "appointments#advisors" + # scholarships page + get "scholarships/new", to: "admins#new", as: :new_scholarship + get "scholarships/export", to: "admins#export_scholarships", as: "export_scholarships" + get "scholarships", to: "scholarships#index" + get "scholarships/:id", to: "scholarships#show", as: :scholarship + get "manage_scholarships", to: "admins#manage_scholarships", as: "manage_scholarships" + post "scholarships", to: "admins#create", as: :create_scholarship + get "scholarships/:id/edit", to: "admins#edit", as: :edit_scholarship + patch "scholarships/:id", to: "admins#update", as: :update_scholarship + delete "scholarships/batch_delete", to: "admins#batch_delete", as: :batch_delete_scholarships + delete "scholarships/:id", to: "admins#destroy", as: :destroy_scholarship + # podcast page + get "podcasts", to: "podcasts#index" + # courses page + get "courses", to: "courses#index" + # events page + get "events", to: "events#index" # the admin dashboard - get 'admins', to: 'admins#index' - get 'view_checkin_records', to: 'admins#view_checkin_records' + get "admins", to: "admins#index" + get "view_checkin_records", to: "admins#view_checkin_records" + + # event routes + namespace :admin do + resources :events do + collection do + get :export_events, defaults: { format: :csv }, as: :export + end + end + end + + # advisor routes + get "manage_advisors", to: "admins#manage_advisors", as: "manage_advisors" + get "/advisors/new", to: "advisors#new" + post "/advisors", to: "advisors#create" + get "/advisors/:id/edit", to: "advisors#edit", as: "edit_advisor" + patch "/advisors/:id", to: "advisors#update", as: "advisor" + delete "/advisors/:id", to: "advisors#destroy", as: "delete_advisor" + get "/advisors/:id", to: "advisors#destroy" + + # Course management routes + get "manage_courses", to: "admins#manage_courses", as: "manage_courses" + get "courses/new", to: "admins#new_course", as: "new_course" + post "courses", to: "admins#create_course", as: "create_course" + get "courses/:id/edit", to: "admins#edit_course", as: "edit_course" + patch "courses/:id", to: "admins#update_course", as: "update_course" + delete "courses/:id", to: "admins#destroy_course", as: "destroy_course" + get "courses/export", to: "admins#export_courses", as: "export_courses" + # user routes - patch 'user', to: 'users#update' - get 'user/profile/new', to: 'users#profile_new', as: 'user_profile_new' - patch 'user/profile/update', to: 'users#profile_update', as: 'user_profile_update' - get 'user/profile/edit', to: 'users#profile_edit', as: 'user_profile_edit' + patch "user", to: "users#update" + get "user/profile/new", to: "users#profile_new", as: "user_profile_new" + patch "user/profile/update", to: "users#profile_update", as: "user_profile_update" + get "user/profile/edit", to: "users#profile_edit", as: "user_profile_edit" + get "manage_user_roles", to: "admins#manage_user_roles", as: "manage_user_roles" + get "user/:id/role_edit", to: "users#edit_user_role", as: "edit_user_role" + + resources :users do + member do + patch :update_user_role + end + end + # Routes for Google authentication - get 'auth/google_oauth2/callback', to: 'sessions#google_auth', as: 'google_login' - get 'auth/failure', to: redirect('/') - get 'logout', to: 'sessions#google_auth_logout' - get 'login/confirm', to: 'login#confirm' + get "auth/google_oauth2/callback", to: "sessions#google_auth", as: "google_login" + get "auth/failure", to: redirect("/") + get "logout", to: "sessions#google_auth_logout" + get "login/confirm", to: "login#confirm" + + # Routes for Canvas authentication + post "login/canvas", to: "login#canvas_login", as: "canvas_login" + get "auth/canvas/callback", to: "sessions#canvas_callback", as: "canvas_callback" end diff --git a/db/migrate/20250403215428_create_events.rb b/db/migrate/20250403215428_create_events.rb new file mode 100644 index 000000000..7c2edf89b --- /dev/null +++ b/db/migrate/20250403215428_create_events.rb @@ -0,0 +1,14 @@ +class CreateEvents < ActiveRecord::Migration[6.1] + def change + create_table :events do |t| + t.string :title + t.date :date + t.time :start_time + t.time :end_time + t.string :location + t.text :description + + t.timestamps + end + end +end diff --git a/db/migrate/20250406052745_create_scholarships.rb b/db/migrate/20250406052745_create_scholarships.rb new file mode 100644 index 000000000..6dc191c89 --- /dev/null +++ b/db/migrate/20250406052745_create_scholarships.rb @@ -0,0 +1,11 @@ +class CreateScholarships < ActiveRecord::Migration[6.1] + def change + create_table :scholarships do |t| + t.string :name + t.text :description + t.string :status_text + t.string :application_url + t.timestamps + end + end +end \ No newline at end of file diff --git a/db/migrate/20250409195822_create_advisors.rb b/db/migrate/20250409195822_create_advisors.rb new file mode 100644 index 000000000..265cf729a --- /dev/null +++ b/db/migrate/20250409195822_create_advisors.rb @@ -0,0 +1,12 @@ +class CreateAdvisors < ActiveRecord::Migration[6.1] + def change + create_table :advisors do |t| + t.string :name + t.text :description + t.string :calendar + t.boolean :active + + t.timestamps + end + end +end diff --git a/db/migrate/20250411044224_create_active_storage_tables.active_storage.rb b/db/migrate/20250411044224_create_active_storage_tables.active_storage.rb new file mode 100644 index 000000000..e4706aa21 --- /dev/null +++ b/db/migrate/20250411044224_create_active_storage_tables.active_storage.rb @@ -0,0 +1,57 @@ +# This migration comes from active_storage (originally 20170806125915) +class CreateActiveStorageTables < ActiveRecord::Migration[7.0] + def change + # Use Active Record's configured type for primary and foreign keys + primary_key_type, foreign_key_type = primary_and_foreign_key_types + + create_table :active_storage_blobs, id: primary_key_type do |t| + t.string :key, null: false + t.string :filename, null: false + t.string :content_type + t.text :metadata + t.string :service_name, null: false + t.bigint :byte_size, null: false + t.string :checksum + + if connection.supports_datetime_with_precision? + t.datetime :created_at, precision: 6, null: false + else + t.datetime :created_at, null: false + end + + t.index [ :key ], unique: true + end + + create_table :active_storage_attachments, id: primary_key_type do |t| + t.string :name, null: false + t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type + t.references :blob, null: false, type: foreign_key_type + + if connection.supports_datetime_with_precision? + t.datetime :created_at, precision: 6, null: false + else + t.datetime :created_at, null: false + end + + t.index [ :record_type, :record_id, :name, :blob_id ], name: :index_active_storage_attachments_uniqueness, unique: true + t.foreign_key :active_storage_blobs, column: :blob_id + end + + create_table :active_storage_variant_records, id: primary_key_type do |t| + t.belongs_to :blob, null: false, index: false, type: foreign_key_type + t.string :variation_digest, null: false + + t.index [ :blob_id, :variation_digest ], name: :index_active_storage_variant_records_uniqueness, unique: true + t.foreign_key :active_storage_blobs, column: :blob_id + end + end + + private + def primary_and_foreign_key_types + config = Rails.configuration.generators + setting = config.options[config.orm][:primary_key_type] + primary_key_type = setting || :primary_key + foreign_key_type = setting || :bigint + [primary_key_type, foreign_key_type] + end +end diff --git a/db/migrate/20250411044225_create_action_text_tables.action_text.rb b/db/migrate/20250411044225_create_action_text_tables.action_text.rb new file mode 100644 index 000000000..1be48d70b --- /dev/null +++ b/db/migrate/20250411044225_create_action_text_tables.action_text.rb @@ -0,0 +1,26 @@ +# This migration comes from action_text (originally 20180528164100) +class CreateActionTextTables < ActiveRecord::Migration[6.0] + def change + # Use Active Record's configured type for primary and foreign keys + primary_key_type, foreign_key_type = primary_and_foreign_key_types + + create_table :action_text_rich_texts, id: primary_key_type do |t| + t.string :name, null: false + t.text :body, size: :long + t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type + + t.timestamps + + t.index [ :record_type, :record_id, :name ], name: "index_action_text_rich_texts_uniqueness", unique: true + end + end + + private + def primary_and_foreign_key_types + config = Rails.configuration.generators + setting = config.options[config.orm][:primary_key_type] + primary_key_type = setting || :primary_key + foreign_key_type = setting || :bigint + [primary_key_type, foreign_key_type] + end +end diff --git a/db/migrate/20250418103445_create_courses.rb b/db/migrate/20250418103445_create_courses.rb new file mode 100644 index 000000000..f580e8afa --- /dev/null +++ b/db/migrate/20250418103445_create_courses.rb @@ -0,0 +1,17 @@ +class CreateCourses < ActiveRecord::Migration[7.1] + def change + create_table :courses do |t| + t.string :code + t.string :title + t.text :description + t.string :units + t.string :semester + t.string :schedule + t.string :ccn + t.string :location + t.boolean :available + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 5397e35af..836215f6e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1,5 +1,3 @@ -# frozen_string_literal: true - # This file is auto-generated from the current state of the database. Instead # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. @@ -12,63 +10,154 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2022_04_20_211533) do - +ActiveRecord::Schema[7.1].define(version: 2025_04_18_103445) do # These are extensions that must be enabled in order to support this database - enable_extension 'plpgsql' + enable_extension "plpgsql" + + create_table "action_text_rich_texts", force: :cascade do |t| + t.string "name", null: false + t.text "body" + t.string "record_type", null: false + t.bigint "record_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["record_type", "record_id", "name"], name: "index_action_text_rich_texts_uniqueness", unique: true + end + + create_table "active_storage_attachments", force: :cascade do |t| + t.string "name", null: false + t.string "record_type", null: false + t.bigint "record_id", null: false + t.bigint "blob_id", null: false + t.datetime "created_at", null: false + t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" + t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true + end + + create_table "active_storage_blobs", force: :cascade do |t| + t.string "key", null: false + t.string "filename", null: false + t.string "content_type" + t.text "metadata" + t.string "service_name", null: false + t.bigint "byte_size", null: false + t.string "checksum" + t.datetime "created_at", null: false + t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true + end + + create_table "active_storage_variant_records", force: :cascade do |t| + t.bigint "blob_id", null: false + t.string "variation_digest", null: false + t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true + end + + create_table "advisors", force: :cascade do |t| + t.string "name" + t.text "description" + t.string "calendar" + t.boolean "active" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "announcements", force: :cascade do |t| + t.string "title" + t.text "content" + t.date "issued_date" + t.bigint "admin_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["admin_id"], name: "index_announcements_on_admin_id" + end + + create_table "appointments", force: :cascade do |t| + t.datetime "time", precision: nil + t.string "location" + t.bigint "staff_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.bigint "student_id" + t.index ["staff_id"], name: "index_appointments_on_staff_id" + t.index ["student_id"], name: "index_appointments_on_student_id" + end + + create_table "checkins", force: :cascade do |t| + t.datetime "time", precision: nil + t.bigint "student_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "reason" + t.index ["student_id"], name: "index_checkins_on_student_id" + end + + create_table "counselors", force: :cascade do |t| + t.string "name" + t.text "description" + t.string "calendar" + t.boolean "active" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end - create_table 'announcements', force: :cascade do |t| - t.string 'title' - t.text 'content' - t.date 'issued_date' - t.bigint 'admin_id', null: false - t.datetime 'created_at', precision: 6, null: false - t.datetime 'updated_at', precision: 6, null: false - t.index ['admin_id'], name: 'index_announcements_on_admin_id' + create_table "courses", force: :cascade do |t| + t.string "code" + t.string "title" + t.text "description" + t.string "units" + t.string "semester" + t.string "schedule" + t.string "ccn" + t.string "location" + t.boolean "available" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end - create_table 'appointments', force: :cascade do |t| - t.datetime 'time' - t.string 'location' - t.bigint 'staff_id', null: false - t.datetime 'created_at', precision: 6, null: false - t.datetime 'updated_at', precision: 6, null: false - t.bigint 'student_id' - t.index ['staff_id'], name: 'index_appointments_on_staff_id' - t.index ['student_id'], name: 'index_appointments_on_student_id' + create_table "events", force: :cascade do |t| + t.string "title" + t.date "date" + t.time "start_time" + t.time "end_time" + t.string "location" + t.text "description" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end - create_table 'checkins', force: :cascade do |t| - t.datetime 'time' - t.bigint 'student_id', null: false - t.datetime 'created_at', precision: 6, null: false - t.datetime 'updated_at', precision: 6, null: false - t.string 'reason' - t.index ['student_id'], name: 'index_checkins_on_student_id' + create_table "scholarships", force: :cascade do |t| + t.string "name" + t.text "description" + t.string "status_text" + t.string "application_url" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end - create_table 'users', force: :cascade do |t| - t.bigint 'sid' - t.string 'first_name' - t.string 'last_name' - t.string 'email' - t.boolean 'is_student' - t.boolean 'is_admin' - t.boolean 'is_staff' - t.datetime 'created_at', precision: 6, null: false - t.datetime 'updated_at', precision: 6, null: false - t.string 'google_token' - t.string 'google_refresh_token' - t.string 'major' - t.string 'identities' - t.string 'pronouns' - t.datetime 'grad_year' - t.index ['email'], name: 'index_users_on_email', unique: true - t.index ['sid'], name: 'index_users_on_sid', unique: true + create_table "users", force: :cascade do |t| + t.bigint "sid" + t.string "first_name" + t.string "last_name" + t.string "email" + t.boolean "is_student" + t.boolean "is_admin" + t.boolean "is_staff" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "google_token" + t.string "google_refresh_token" + t.string "major" + t.string "identities" + t.string "pronouns" + t.datetime "grad_year", precision: nil + t.index ["email"], name: "index_users_on_email", unique: true + t.index ["sid"], name: "index_users_on_sid", unique: true end - add_foreign_key 'announcements', 'users', column: 'admin_id' - add_foreign_key 'appointments', 'users', column: 'staff_id' - add_foreign_key 'appointments', 'users', column: 'student_id' - add_foreign_key 'checkins', 'users', column: 'student_id' + add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" + add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" + add_foreign_key "announcements", "users", column: "admin_id" + add_foreign_key "appointments", "users", column: "staff_id" + add_foreign_key "appointments", "users", column: "student_id" + add_foreign_key "checkins", "users", column: "student_id" end diff --git a/db/seeds.rb b/db/seeds.rb index 7d0d09bbf..1140ed1e1 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -11,7 +11,7 @@ r = Random.new students = [] staffs = [] -admin = FactoryBot.create(:admin) +FactoryBot.create(:admin) 10.times do students.push FactoryBot.create(:student) @@ -22,7 +22,7 @@ staffs.push FactoryBot.create(:staff) end -checkin_reasons = ['Peer Support', 'Counseling Appointment', 'Studying', 'OWLs Meeting', 'Other'] +checkin_reasons = ["Peer Support", "Counseling Appointment", "Studying", "OWLs Meeting", "Other"] 40.times do FactoryBot.create(:checkin, reason: checkin_reasons.sample, student: students.sample, time: Time.current + r.rand(-240.hours..0.hours)) @@ -36,3 +36,80 @@ 10.times do FactoryBot.create(:appointment, staff: staffs.sample, student: nil, time: Time.current + r.rand(1.hours..240.hours)) end + +puts "Seeding Scholarships..." + +# Clear existing scholarships if needed (be careful with this in production) +# Scholarship.destroy_all + +Scholarship.find_or_create_by!(name: "Crankstart Re-entry Scholarship") do |s| + s.description = <<-DESC.strip_heredoc +

The Crankstart Re-entry Scholarship provides up to $5,000 awards to ten first-semester#{' '} + (if you began taking courses during summer, you may still apply) undergraduate re-entry students#{' '} + who demonstrate the potential and capacity to positively contribute to society beyond Berkeley.#{' '} + The Crankstart Re-entry Scholarship has closed for the 2024-2025 cycle. +

+

Applicants must meet the following criteria: +

+

+

Your application must include:#{' '} +

    +
  1. A personal essay no more than three pages double-spaced (with one inch margins in Times New Roman font)#{' '} + that describes what it means to be a re-entry student. In your essay please: share your interest and values,#{' '} + highlight your personal achievements through your work, school, family or community service in spite of obstacles,#{' '} + explain how your leadership and perseverance made a difference, address the interruption to your college academics,#{' '} + and share your educational and career goals.
  2. +
  3. Copies of transcripts from all colleges attended (unofficial accepted)
  4. +
  5. No more than 2 letters of recommendation (can be from, for example, a former instructor,#{' '} + supervisor, or mentor who can speak to your ability to apply your Berkeley education to future#{' '} + contributions to society)
  6. +
+

#{' '} + DESC + s.status_text = "The 2024 Crankstart Re-entry Scholarship has closed for the 2024-2025 cycle." + # You might want to leave the URL blank initially or set it to a placeholder if the real form isn't ready + s.application_url = "https://docs.google.com/forms/d/e/1FAIpQLSfQlLgXMMbFPkaicu1szFYGFedH6EyDGY7xMnAKZnI_pdecow/closedform" # Example URL +end + +Scholarship.find_or_create_by!(name: "Osher Re-entry Scholarship") do |s| + s.description = <<-DESC.strip_heredoc +

The Osher Re-entry Scholarship Program awards several continuing (completed at least one fall or spring semester at Berkeley)#{' '} + undergraduate re-entry students who demonstrate the potential and capacity to positively contribute to society#{' '} + beyond Berkeley with awards up to $5,000 for the academic year. The Osher Re-entry Scholarship has closed for the 2024-2025 cycle. +

+

Applicants must meet the following criteria: +

+

+

Your application must include:#{' '} +

    +
  1. A personal essay no more than 3 pages double-spaced (with one inch margins in Times New Roman font) that describes#{' '} + what it means to be a re-entry student. In your essay please: share your interest and values, highlight your personal#{' '} + achievements through your work, school, family or community service in spite of obstacles, explain how your leadership and#{' '} + perseverance made a difference, address the interruption to your college academics, and share your educational#{' '} + and career goals.
  2. +
  3. Copies of transcripts from all colleges attended (unofficial accepted)
  4. +
  5. No more than 2 letters of recommendation* (can be from, for example, a former instructor, supervisor or mentor#{' '} + who can speak to your ability to apply your Berkeley education to future contributions to society)
  6. +
+

+ DESC + s.status_text = "The 2024 Osher Re-entry Scholarship has closed for the 2024-2025 cycle." + s.application_url = "https://forms.gle/xakufqTN2Gsu5vur9" # Example URL +end + +puts "Finished seeding Scholarships." diff --git a/features/accessibility.feature b/features/accessibility.feature new file mode 100644 index 000000000..a14a5c9d5 --- /dev/null +++ b/features/accessibility.feature @@ -0,0 +1,53 @@ +@selenium +Feature: Accessibility Testing + As a visitor of the website + I want all pages I visit to be WCAG 2.0 AA compliant + So that the website is accessible to everyone, including those with disabilities + + Scenario: Check landing page accessibility + Given I am on the landing page + Then the page should be axe clean + + Scenario: Check checkin page accessibility + Given I am on the checkin page + Then the page should be axe clean + + Scenario: Check appointments page accessibility + Given I am on the appointments page + Then the page should be axe clean + + Scenario: Check scholarships page accessibility + Given I am on the scholarships page + Then the page should be axe clean + + Scenario: Check courses page accessibility + Given I am on the courses page + Then the page should be axe clean + + Scenario: Check events page accessibility + Given I am on the events page + Then the page should be axe clean + + Scenario: Check create new user profile page accessibility + Given I am on the user_profile_new page + Then the page should be axe clean + + Scenario: Check user profile edit page accessibility + Given I am on the user_profile_edit page + Then the page should be axe clean + + Scenario: Check admins page accessibility + Given I am on the admins page + Then the page should be axe clean + + Scenario: Check view checkin records page accessibility + Given I am on the view_checkin_records page + Then the page should be axe clean + + Scenario: Check admin events page accessibility + Given I am on the admin_events page + Then the page should be axe clean + + Scenario: Check admin new event page accessibility + Given I am on the new_admin_event page + Then the page should be axe clean diff --git a/features/admin_event_management.feature b/features/admin_event_management.feature new file mode 100644 index 000000000..6310d7f58 --- /dev/null +++ b/features/admin_event_management.feature @@ -0,0 +1,9 @@ +Feature: Admin Event Management + As a logged-in admin + I want to be able to add, edit, and delete events + So that I can directly manipulate what events students see + + Background: + Given I logged in as a "Admin" + And I am on the admins page + \ No newline at end of file diff --git a/features/autofill.feature b/features/autofill.feature new file mode 100644 index 000000000..50fc386c1 --- /dev/null +++ b/features/autofill.feature @@ -0,0 +1,17 @@ +Feature: Autofill check-in reason + As a student using the check-in system + I want my check-in reason to be pre-filled + So that I can check in more quickly + + Background: logged in student + Given I logged in as a "Student" + And I am on the landing page + + Scenario: New user sees default reason + When I click "Check-in" + Then the reason dropdown should show "Peer Support" + + Scenario: Returning user sees last used reason + Given I have previously checked in for "Studying" + When I click "Check-in" + Then the reason dropdown should show "Studying" \ No newline at end of file diff --git a/features/checkin.feature b/features/checkin.feature index 630898aab..940867a2d 100644 --- a/features/checkin.feature +++ b/features/checkin.feature @@ -1,32 +1,32 @@ Feature: check in as a student + As a logged-in student, I want to be able to checkin on the app, + so that I can access the study space. - As a logged-in student, I want to be able to checkin on the app, - so that I can access the study space. - -Background: logged in student, on landing page + Background: logged in student, on landing page Given I logged in as a "Student" And I am on the landing page -Scenario: student should be able to check in + Scenario: student should be able to check in Then I should got "Check-in" -Scenario: student should be redirect to the check-in page after clicking "Check-in" + Scenario: student should be redirect to the check-in page after clicking "Check-in" When I click "Check-in" Then I should see "Please select a reason for check-in today" -Scenario: student should be able to fill in a reason and check-in (good path) + Scenario: student should be able to fill in a reason and check-in (good path) When I click "Check-in" And I select "Peer Support" from "checkin_reason" And I press "Submit" Then I should be on the landing page And I should see "Success!" -Scenario: student should not be able to check-in without filling in a reason (sad path) - When I click "Check-in" - And I click "Submit" in checkin - Then no checkin record should be created - -Scenario: student should be able to go back to landing page from checkin page + Scenario: student should be able to go back to landing page from checkin page When I click "Check-in" And I click "Back" Then I should be on the landing page + + Scenario: student should be able to check-in with default reason + When I click "Check-in" + And I click "Submit" in checkin + Then I should see "Success!" + And I should be on the landing page diff --git a/features/course.feature b/features/course.feature new file mode 100644 index 000000000..701054bdc --- /dev/null +++ b/features/course.feature @@ -0,0 +1,19 @@ +Feature: view course information + As a logged-in student, I want to be able to go to the courses page, + so that I can view course information + +Background: logged in student, on landing page + Given I logged in as a "Student" + And I am on the landing page + +Scenario: student should be able to see a link to scholarship information + Then I should got "Our Courses" + +Scenario: student should be redirected to the courses page after clicking "Our Courses" + When I click "Our Courses" + Then I should be on the courses page + +Scenario: student should be able to go back to the landing page from courses page + When I click "Our Courses" + And I click "Back" + Then I should be on the landing page \ No newline at end of file diff --git a/features/events.feature b/features/events.feature new file mode 100755 index 000000000..c2229376a --- /dev/null +++ b/features/events.feature @@ -0,0 +1,15 @@ +Feature: view events + + As a logged-in student, I want to be able to go to the events page, + so that I can view information on upcoming events + +Background: logged in student, on landing page + Given I logged in as a "Student" + And I am on the landing page + +Scenario: student should be able to see a link to events + Then I should got "Events" + +Scenario: student should be redirected to the events page after clicking "Events" + When I click "Events" + Then I should be on the events page diff --git a/features/podcast.feature b/features/podcast.feature new file mode 100644 index 000000000..c9f1611a9 --- /dev/null +++ b/features/podcast.feature @@ -0,0 +1,11 @@ +Feature: view podcast information + + As a logged-in student, I want to be able to go to the scholarships page, + so that I can view scholarship information + +Background: logged in student + Given I logged in as a "Student" + +Scenario: View the podcast page + Given I am on the podcast page + Then I should see "Listen Now" \ No newline at end of file diff --git a/features/scholarship.feature b/features/scholarship.feature new file mode 100644 index 000000000..67270abcb --- /dev/null +++ b/features/scholarship.feature @@ -0,0 +1,20 @@ +Feature: view scholarship information + + As a logged-in student, I want to be able to go to the scholarships page, + so that I can view scholarship information + +Background: logged in student, on landing page + Given I logged in as a "Student" + And I am on the landing page + +Scenario: student should be able to see a link to scholarship information + Then I should got "Re-entry Scholarships" + +Scenario: student should be redirected to the scholarships page after clicking "Re-entry Scholarships" + When I click "Re-entry Scholarships" + Then I should be on the scholarships page + +Scenario: student should be able to go back to the landing page from scholarships page + When I click "Re-entry Scholarships" + And I click "Back" + Then I should be on the landing page \ No newline at end of file diff --git a/features/step_definitions/checkin_steps.rb b/features/step_definitions/checkin_steps.rb index 8ebf1c4f0..fcac57571 100644 --- a/features/step_definitions/checkin_steps.rb +++ b/features/step_definitions/checkin_steps.rb @@ -1,11 +1,24 @@ # frozen_string_literal: true -When(/^(?:|I )click "Submit" in checkin$/) do - @n_checkin_before = Checkin.all.size - click_link_or_button('Submit') +When(/^I click "Submit" in checkin$/) do + @checkins_before = Checkin.count + click_button "Submit" end -Then(/^no checkin record should be created$/) do - n_checkin_after = Checkin.all.size - expect(n_checkin_after).to eq @n_checkin_before +Then(/^the checkin should be successful$/) do + expect(Checkin.count).to eq(@checkins_before + 1) + expect(page).to have_content("Success!") +end + +Given(/^I have previously checked in for "([^"]*)"$/) do |reason| + # Make sure we're using the same user from login + @user = Student.find(Capybara.current_session.driver.request.session["current_user_id"]) + @user.checkins.create!( + reason: reason, + time: Time.current + ) +end + +Then(/^the reason dropdown should show "([^"]*)"$/) do |expected_reason| + expect(page).to have_select("checkin_reason", selected: expected_reason) end diff --git a/features/step_definitions/login_steps.rb b/features/step_definitions/login_steps.rb index 17b893349..428a053a4 100644 --- a/features/step_definitions/login_steps.rb +++ b/features/step_definitions/login_steps.rb @@ -1,32 +1,32 @@ # frozen_string_literal: true ADMIN_CREDENTIALS = { - 'provider' => 'google_oauth2', - 'uid' => '1000000000', - 'info' => { - 'name' => 'Google Admin Developer', - 'email' => 'google_admin@berkeley.edu', - 'first_name' => 'Google', - 'last_name' => 'Admin Developer' + "provider" => "google_oauth2", + "uid" => "1000000000", + "info" => { + "name" => "Google Admin Developer", + "email" => "google_admin@berkeley.edu", + "first_name" => "Google", + "last_name" => "Admin Developer" }, - 'credentials' => { - 'token' => 'credentials_token_1234567', - 'refresh_token' => 'credentials_refresh_token_45678' + "credentials" => { + "token" => "credentials_token_1234567", + "refresh_token" => "credentials_refresh_token_45678" } }.freeze STUDENT_CREDENTIALS = { - 'provider' => 'google_oauth2', - 'uid' => '1000000000', - 'info' => { - 'name' => 'Google Test Developer', - 'email' => 'google_test@berkeley.edu', - 'first_name' => 'Google', - 'last_name' => 'Test Developer' + "provider" => "google_oauth2", + "uid" => "1000000000", + "info" => { + "name" => "Google Test Developer", + "email" => "google_test@berkeley.edu", + "first_name" => "Google", + "last_name" => "Test Developer" }, - 'credentials' => { - 'token' => 'credentials_token_1234567', - 'refresh_token' => 'credentials_refresh_token_45678' + "credentials" => { + "token" => "credentials_token_1234567", + "refresh_token" => "credentials_refresh_token_45678" } }.freeze @@ -40,23 +40,23 @@ When(/^(?:|I )am a "([^"]*)"$/) do |user_type| case user_type - when 'Student' + when "Student" OmniAuth.config.add_mock( :google_oauth2, STUDENT_CREDENTIALS ) - u = User.new(first_name: 'Google', last_name: 'Test Developer', email: 'google_test@berkeley.edu') + u = User.new(first_name: "Google", last_name: "Test Developer", email: "google_test@berkeley.edu") u.is_student = true - when 'Admin' + when "Admin" OmniAuth.config.add_mock( :google_oauth2, ADMIN_CREDENTIALS ) - u = User.new(first_name: 'Google', last_name: 'Admin Developer', email: 'google_admin@berkeley.edu') + u = User.new(first_name: "Google", last_name: "Admin Developer", email: "google_admin@berkeley.edu") u.is_admin = true - when 'Staff' + when "Staff" # pending - u = User.new(first_name: 'Google', last_name: 'Test Developer', email: 'google_staff@berkeley.edu') + u = User.new(first_name: "Google", last_name: "Test Developer", email: "google_staff@berkeley.edu") u.is_staff = true end u.save @@ -71,12 +71,12 @@ And(/^I am a logged-out "([^"]*)"$/) do |user_type| case user_type - when 'Student' + when "Student" OmniAuth.config.add_mock( :google_oauth2, STUDENT_CREDENTIALS ) - when 'Admin' + when "Admin" OmniAuth.config.add_mock( :google_oauth2, ADMIN_CREDENTIALS diff --git a/features/step_definitions/web_steps.rb b/features/step_definitions/web_steps.rb index 50783ed6f..8c3cb4048 100644 --- a/features/step_definitions/web_steps.rb +++ b/features/step_definitions/web_steps.rb @@ -20,10 +20,10 @@ # * http://elabs.se/blog/15-you-re-cuking-it-wrong # -require 'uri' -require 'cgi' -require File.expand_path(File.join(File.dirname(__FILE__), '..', 'support', 'paths')) -require File.expand_path(File.join(File.dirname(__FILE__), '..', 'support', 'selectors')) +require "uri" +require "cgi" +require File.expand_path(File.join(File.dirname(__FILE__), "..", "support", "paths")) +require File.expand_path(File.join(File.dirname(__FILE__), "..", "support", "selectors")) module WithinHelpers def with_scope(locator, &block) @@ -115,9 +115,9 @@ def with_scope(locator, &block) regexp = Regexp.new(regexp) if page.respond_to? :should - page.should have_xpath('//*', text: regexp) + page.should have_xpath("//*", text: regexp) else - assert page.has_xpath?('//*', text: regexp) + assert page.has_xpath?("//*", text: regexp) end end @@ -133,16 +133,16 @@ def with_scope(locator, &block) regexp = Regexp.new(regexp) if page.respond_to? :should - page.should have_no_xpath('//*', text: regexp) + page.should have_no_xpath("//*", text: regexp) else - assert page.has_no_xpath?('//*', text: regexp) + assert page.has_no_xpath?("//*", text: regexp) end end Then(/^the "([^"]*)" field(?: within (.*))? should contain "([^"]*)"$/) do |field, parent, value| with_scope(parent) do field = find_field(field) - field_value = field.tag_name == 'textarea' ? field.text : field.value + field_value = field.tag_name == "textarea" ? field.text : field.value if field_value.respond_to? :should field_value.should =~ /#{value}/ else @@ -154,7 +154,7 @@ def with_scope(locator, &block) Then(/^the "([^"]*)" field(?: within (.*))? should not contain "([^"]*)"$/) do |field, parent, value| with_scope(parent) do field = find_field(field) - field_value = field.tag_name == 'textarea' ? field.text : field.value + field_value = field.tag_name == "textarea" ? field.text : field.value if field_value.respond_to? :should_not field_value.should_not =~ /#{value}/ else @@ -165,11 +165,11 @@ def with_scope(locator, &block) Then(/^the "([^"]*)" field should have the error "([^"]*)"$/) do |field, error_message| element = find_field(field) - classes = element.find(:xpath, '..')[:class].split(' ') + classes = element.find(:xpath, "..")[:class].split(" ") - form_for_input = element.find(:xpath, 'ancestor::form[1]') - using_formtastic = form_for_input[:class].include?('formtastic') - error_class = using_formtastic ? 'error' : 'field_with_errors' + form_for_input = element.find(:xpath, "ancestor::form[1]") + using_formtastic = form_for_input[:class].include?("formtastic") + error_class = using_formtastic ? "error" : "field_with_errors" if classes.respond_to? :should classes.should include(error_class) @@ -194,19 +194,19 @@ def with_scope(locator, &block) Then(/^the "([^"]*)" field should have no error$/) do |field| element = find_field(field) - classes = element.find(:xpath, '..')[:class].split(' ') + classes = element.find(:xpath, "..")[:class].split(" ") if classes.respond_to? :should - classes.should_not include('field_with_errors') - classes.should_not include('error') + classes.should_not include("field_with_errors") + classes.should_not include("error") else - assert !classes.include?('field_with_errors') - assert !classes.include?('error') + assert !classes.include?("field_with_errors") + assert !classes.include?("error") end end Then(/^the "([^"]*)" checkbox(?: within (.*))? should be checked$/) do |label, parent| with_scope(parent) do - field_checked = find_field(label)['checked'] + field_checked = find_field(label)["checked"] if field_checked.respond_to? :should field_checked.should be_true else @@ -217,7 +217,7 @@ def with_scope(locator, &block) Then(/^the "([^"]*)" checkbox(?: within (.*))? should not be checked$/) do |label, parent| with_scope(parent) do - field_checked = find_field(label)['checked'] + field_checked = find_field(label)["checked"] if field_checked.respond_to? :should field_checked.should be_false else @@ -239,7 +239,7 @@ def with_scope(locator, &block) query = URI.parse(current_url).query actual_params = query ? CGI.parse(query) : {} expected_params = {} - expected_pairs.rows_hash.each_pair { |k, v| expected_params[k] = v.split(',') } + expected_pairs.rows_hash.each_pair { |k, v| expected_params[k] = v.split(",") } if actual_params.respond_to? :should actual_params.should == expected_params diff --git a/features/support/axe_cucumber.rb b/features/support/axe_cucumber.rb new file mode 100644 index 000000000..dfadd396a --- /dev/null +++ b/features/support/axe_cucumber.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +require "axe-cucumber-steps" diff --git a/features/support/capybara.rb b/features/support/capybara.rb new file mode 100644 index 000000000..c6bf53585 --- /dev/null +++ b/features/support/capybara.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require "capybara" +require "selenium-webdriver" + +Capybara.register_driver :selenium_chrome do |app| + options = Selenium::WebDriver::Chrome::Options.new + options.add_argument("--headless") # Runs Chrome in headless mode (no UI) + options.add_argument("--disable-gpu") # Optional: to prevent GPU acceleration issues + options.add_argument("--window-size=1280x1024") # Optional: specify window size to avoid rendering issues + Capybara::Selenium::Driver.new(app, browser: :chrome, options: options) +end + +Before("@selenium") do + Capybara.current_driver = :selenium_chrome + WebMock.disable! +end + +After("@selenium") do + Capybara.use_default_driver + WebMock.enable! +end diff --git a/features/support/env.rb b/features/support/env.rb index 8441816d9..474812b2d 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -1,8 +1,11 @@ # frozen_string_literal: true -require 'simplecov' -require 'simplecov_json_formatter' -SimpleCov.start 'rails' +require "webmock/cucumber" + +WebMock.disable_net_connect!(allow_localhost: true, allow: "127.0.0.1:9515") +require "simplecov" +require "simplecov_json_formatter" +SimpleCov.start "rails" SimpleCov.formatters = SimpleCov::Formatter::MultiFormatter.new([ SimpleCov::Formatter::JSONFormatter, SimpleCov::Formatter::HTMLFormatter @@ -14,7 +17,7 @@ # newer version of cucumber-rails. Consider adding your own code to a new file # instead of editing this one. Cucumber will automatically load all features/**/*.rb # files. -require 'cucumber/rails' +require "cucumber/rails" # Capybara defaults to CSS3 selectors rather than XPath. # If you'd prefer to use XPath, just uncomment this line and adjust any @@ -43,7 +46,7 @@ begin DatabaseCleaner.strategy = :transaction rescue NameError - raise 'You need to add database_cleaner to your Gemfile (in the :test group) if you wish to use it.' + raise "You need to add database_cleaner to your Gemfile (in the :test group) if you wish to use it." end # You may also want to configure DatabaseCleaner to use different strategies for certain features and scenarios. # See the DatabaseCleaner documentation for details. Example: @@ -66,3 +69,11 @@ Cucumber::Rails::Database.javascript_strategy = :truncation World(FactoryBot::Syntax::Methods) + +Before do + stub_request(:get, %r{#{ENV['CANVAS_URL']}/login/oauth2/auth}) + .to_return( + status: 302, + headers: { "Location" => "/auth/canvas/callback?code=test_code" } + ) +end diff --git a/features/support/paths.rb b/features/support/paths.rb index 20963fa84..3946c8064 100644 --- a/features/support/paths.rb +++ b/features/support/paths.rb @@ -20,6 +20,9 @@ def path_to(page_name) when /^the checkin page$/ then checkin_path when /^the confirm page$/ then login_confirm_path when /^the admin dashboard$/ then admins_path + when /^the scholarships page$/ then scholarships_path + when /^the courses page$/ then courses_path + when /^the podcast page$/ then podcasts_path # Add more mappings here. # Here is an example that pulls values out of the Regexp: @@ -31,7 +34,7 @@ def path_to(page_name) begin page_name =~ /^the (.*) page$/ path_components = Regexp.last_match(1).split(/\s+/) - send(path_components.push('path').join('_').to_sym) + send(path_components.push("path").join("_").to_sym) rescue NoMethodError, ArgumentError raise "Can't find mapping from \"#{page_name}\" to a path.\n" \ "Now, go and add a mapping in #{__FILE__}" diff --git a/features/support/selectors.rb b/features/support/selectors.rb index 4b25c26d0..80022e898 100644 --- a/features/support/selectors.rb +++ b/features/support/selectors.rb @@ -15,8 +15,8 @@ module HtmlSelectorsHelpers def selector_for(locator) case locator - when 'the page' - 'html > body' + when "the page" + "html > body" # Add more mappings here. # Here is an example that pulls values out of the Regexp: diff --git a/info.yml b/info.yml deleted file mode 100644 index 74d0c1e9d..000000000 --- a/info.yml +++ /dev/null @@ -1,34 +0,0 @@ -project: - name: 'berkeley-reentry-student-program' - owner: 'Reentry Student Program' - teamId: 'berkeley-reentry-student-program' - identities: - pivotal: 'https://www.pivotaltracker.com/n/projects/2553425' - heroku: 'https://berkeley-reentry.herokuapp.com/' - codeclimate: 'https://codeclimate.com/github/ryan-garay89/berkeley-reentry-student-program' - members: - member1: - name: 'Ryan' - surname: 'Garay' - githubUsername: 'ryan-garay89' - member2: - name: 'Elizabeth' - surname: 'Herrmann' - githubUsername: 'eherrmann2023' - member3: - name: 'Erin' - surname: 'Kraemer' - githubUsername: 'erinkraemer' - member4: - name: 'Michelle' - surname: 'Kroll' - githubUsername: 'michellekroll' - member5: - name: 'Kin Long' - surname: 'Lo' - githubUsername: 'KLongLo' - member6: - name: 'Haolan' - surname: 'Mo' - githubUsername: 'HaolanMo' - diff --git a/lib/tasks/cucumber.rake b/lib/tasks/cucumber.rake index 2adc2f001..f94020abf 100644 --- a/lib/tasks/cucumber.rake +++ b/lib/tasks/cucumber.rake @@ -12,48 +12,48 @@ unless ARGV.any? { |a| a =~ /^gems/ } # Don't load anything when running the gem $LOAD_PATH.unshift("#{File.dirname(vendored_cucumber_bin)}/../lib") unless vendored_cucumber_bin.nil? begin - require 'cucumber/rake/task' + require "cucumber/rake/task" namespace :cucumber do - Cucumber::Rake::Task.new({ ok: 'test:prepare' }, 'Run features that should pass') do |t| + Cucumber::Rake::Task.new({ ok: "test:prepare" }, "Run features that should pass") do |t| t.binary = vendored_cucumber_bin # If nil, the gem's binary is used. t.fork = true # You may get faster startup if you set this to false - t.profile = 'default' + t.profile = "default" end - Cucumber::Rake::Task.new({ wip: 'test:prepare' }, 'Run features that are being worked on') do |t| + Cucumber::Rake::Task.new({ wip: "test:prepare" }, "Run features that are being worked on") do |t| t.binary = vendored_cucumber_bin t.fork = true # You may get faster startup if you set this to false - t.profile = 'wip' + t.profile = "wip" end - Cucumber::Rake::Task.new({ rerun: 'test:prepare' }, - 'Record failing features and run only them if any exist') do |t| + Cucumber::Rake::Task.new({ rerun: "test:prepare" }, + "Record failing features and run only them if any exist") do |t| t.binary = vendored_cucumber_bin t.fork = true # You may get faster startup if you set this to false - t.profile = 'rerun' + t.profile = "rerun" end - desc 'Run all features' + desc "Run all features" task all: %i[ok wip] task :statsetup do - require 'rails/code_statistics' - ::STATS_DIRECTORIES << ['Cucumber features', 'features'] if File.exist?('features') - ::CodeStatistics::TEST_TYPES << 'Cucumber features' if File.exist?('features') + require "rails/code_statistics" + ::STATS_DIRECTORIES << ["Cucumber features", "features"] if File.exist?("features") + ::CodeStatistics::TEST_TYPES << "Cucumber features" if File.exist?("features") end task :annotations_setup do Rails.application.configure do if config.respond_to?(:annotations) - config.annotations.directories << 'features' - config.annotations.register_extensions('feature') { |tag| /#\s*(#{tag}):?\s*(.*)$/ } + config.annotations.directories << "features" + config.annotations.register_extensions("feature") { |tag| /#\s*(#{tag}):?\s*(.*)$/ } end end end end - desc 'Alias for cucumber:ok' - task cucumber: 'cucumber:ok' + desc "Alias for cucumber:ok" + task cucumber: "cucumber:ok" task default: :cucumber @@ -62,16 +62,16 @@ unless ARGV.any? { |a| a =~ /^gems/ } # Don't load anything when running the gem end # In case we don't have the generic Rails test:prepare hook, append a no-op task that we can depend upon. - task 'test:prepare' do + task "test:prepare" do end - task stats: 'cucumber:statsetup' + task stats: "cucumber:statsetup" - task notes: 'cucumber:annotations_setup' + task notes: "cucumber:annotations_setup" rescue LoadError - desc 'cucumber rake task not available (cucumber not installed)' + desc "cucumber rake task not available (cucumber not installed)" task :cucumber do - abort 'Cucumber rake task is not available. Be sure to install cucumber as a gem or plugin' + abort "Cucumber rake task is not available. Be sure to install cucumber as a gem or plugin" end end diff --git a/script/cucumber b/script/cucumber index eb5e962e8..3d7910bdf 100755 --- a/script/cucumber +++ b/script/cucumber @@ -5,7 +5,7 @@ vendored_cucumber_bin = Dir["#{File.dirname(__FILE__)}/../vendor/{gems,plugins}/ if vendored_cucumber_bin load File.expand_path(vendored_cucumber_bin) else - require 'rubygems' unless ENV['NO_RUBYGEMS'] - require 'cucumber' + require "rubygems" unless ENV["NO_RUBYGEMS"] + require "cucumber" load Cucumber::BINARY end diff --git a/spec/controllers/admins_controller_spec.rb b/spec/controllers/admins_controller_spec.rb deleted file mode 100644 index 5d93e5a48..000000000 --- a/spec/controllers/admins_controller_spec.rb +++ /dev/null @@ -1,50 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe AdminsController do - describe 'This following user types should be blocked from accessing admin dashboard: ' do - it 'Student' do - @stu = FactoryBot.create :student - session[:current_user_id] = @stu.id - get :index - expect(response).to redirect_to root_path - end - - it 'Staff' do - @staff = FactoryBot.create :staff - session[:current_user_id] = @staff.id - get :index - expect(response).to redirect_to root_path - end - - it 'Logged out user' do - session[:current_user_id] = nil - get :index - expect(response).to redirect_to root_path - end - end - - describe 'view checkin records' do - before do - admin = FactoryBot.create :admin - session[:current_user_id] = admin.id - 50.times do - FactoryBot.create :checkin - end - end - - it "redirect to itself with params[:page] == 1 - if it doesn't have params[:page] or params[:page] is invalid" do - controller.params[:page] = nil - get :view_checkin_records - expect(response).to redirect_to view_checkin_records_path(page: 1) - end - - it 'set has_next_page to false current page is the last checkin records' do - controller.params[:page] = Checkin.all.size / 20 + 1 - get :view_checkin_records - expect(@has_next_page).to be_falsey - end - end -end diff --git a/spec/controllers/checkin_controller_spec.rb b/spec/controllers/checkin_controller_spec.rb deleted file mode 100644 index 4b1ac7874..000000000 --- a/spec/controllers/checkin_controller_spec.rb +++ /dev/null @@ -1,57 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe CheckinController do - before do - # set precondition: sessions, datetime, checkin records - @stu = FactoryBot.create :student - @expected_time = DateTime.parse('2022-03-08T12:00:00-08:00') - Timecop.freeze(@expected_time) - controller.session[:current_user_id] = @stu.id - @n_checkin_before = Checkin.all.size - end - - describe 'POST create' do - describe ': happy path: ' do - before do - controller.session[:current_user_id] = @stu.id - post :create, params: { checkin: { reason: 'Studying' } } - @new_checkin = Checkin.order(id: :desc).first - end - - it 'should add 1 new checkin record to the table' do - expect(Checkin.all.size).to eq(@n_checkin_before + 1) - end - - it 'should create a checkin record with correct time' do - expect(@new_checkin.time).to eq(@expected_time) - end - - it 'the new checkin record should belongs to the student in session' do - expect(@new_checkin.student).to eq(@stu) - end - - it 'should clear flash when creating a new checkin' do - flash[:alert] = 'This is an alert' - get :new - expect(flash).to be_empty - end - end - - describe ': sad path: ' do - it "should redirect user to login if the user haven't log in" do - controller.session[:current_user_id] = nil - post :create, params: { checkin: { reason: 'Studying' } } - expect(response).to redirect_to root_path - end - - it 'should redirect user to landing page if record is not valid' do - controller.session[:current_user_id] = @stu.id - post :create, params: { checkin: { reason: nil } } - expect(response).to redirect_to root_path - expect(flash[:error]).to match(/Something went wrong, please try again/) - end - end - end -end diff --git a/spec/controllers/pages_controller_spec.rb b/spec/controllers/pages_controller_spec.rb deleted file mode 100644 index cdfbc0feb..000000000 --- a/spec/controllers/pages_controller_spec.rb +++ /dev/null @@ -1,61 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe PagesController do - describe 'logged out user' do - it 'should not have any user_type' do - controller.session[:current_user_id] = nil - get :index - expect(assigns(:user_type).empty?).to be_truthy - end - end - - describe 'logged in student' do - before do - student = FactoryBot.create :student - controller.session[:current_user_id] = student.id - get :index - end - - it 'should identified as Student' do - expect(assigns(:user_type)).to include 'Student' - end - - it 'name should be saved' do - expect(assigns(:name)).to be_truthy - end - end - - describe 'logged in staff' do - before do - staff = FactoryBot.create :staff - controller.session[:current_user_id] = staff.id - get :index - end - - it 'should identified as Staff' do - expect(assigns(:user_type)).to include 'Staff' - end - - it 'name should be saved' do - expect(assigns(:name)).to be_truthy - end - end - - describe 'logged in Admin' do - before do - admin = FactoryBot.create :admin - controller.session[:current_user_id] = admin.id - get :index - end - - it 'should identified as Admin' do - expect(assigns(:user_type)).to include 'Admin' - end - - it 'name should be saved' do - expect(assigns(:name)).to be_truthy - end - end -end diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb deleted file mode 100644 index 4faed8e21..000000000 --- a/spec/controllers/sessions_controller_spec.rb +++ /dev/null @@ -1,62 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -ADMIN_CREDENTIALS = { - "provider" => 'google_oauth2', - "uid" => '1000000000', - "info" => { - "name" => 'Google Admin Developer', - "email" => 'google_admin@berkeley.edu', - "first_name" => 'Google', - "last_name" => 'Admin Developer' - }, - "credentials" => { - "token" => "credentials_token_1234567", - "refresh_token" => "credentials_refresh_token_45678" - } -} - -describe SessionsController do - before(:each) do - stub_const('ENV', {'ADMINS' => 'google_admin@berkeley.edu', 'STAFF' => 'google_staff@berkeley.edu'}) - end - describe "google authentication" do - it "should create a new user" do - user_len = User.all.size - request.env['omniauth.auth'] = OmniAuth.config.mock_auth[:google_oauth2] - get :google_auth - expect(User.all.size).to be user_len + 1 - end - end - describe 'logout' do - it 'should delete session key' do - user = FactoryBot.create(:user) - controller.session[:current_user_id] = user.id - get :google_auth_logout - expect(controller.session[:current_user_id]).to be_nil - end - end - describe "admin login" do - it "should create an admin" do - admin_len = Admin.all.size - OmniAuth.config.add_mock( - :google_oauth2, - ADMIN_CREDENTIALS - ) - request.env["omniauth.auth"] = OmniAuth.config.mock_auth[:google_oauth2] - get :google_auth - expect(Admin.all.size).to be admin_len + 1 - end - it "should recover on empty admins" do - stub_const('ENV', {'ADMINS' => '', 'STAFF' => 'google_staff@berkeley.edu'}) - OmniAuth.config.add_mock( - :google_oauth2, - ADMIN_CREDENTIALS - ) - request.env["omniauth.auth"] = OmniAuth.config.mock_auth[:google_oauth2] - get :google_auth - expect(flash[:error]).to match(/Something went wrong, please try again later./) - end - end -end diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb deleted file mode 100644 index f870d74e8..000000000 --- a/spec/controllers/users_controller_spec.rb +++ /dev/null @@ -1,53 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe UsersController do - describe 'update profile' do - before do - @student = FactoryBot.create :student - controller.session[:current_user_id] = @student.id - end - - it 'should update the user record' do - patch :profile_update, - params: { profile: { major: 'computer science', grad_year: DateTime.new(2022), pronouns: 'He/Him/His', - identities: 'something' } } - @student.reload - expect(@student.major).to eq 'computer science' - expect(@student.grad_year).to eq DateTime.new(2022, 0o1, 0o1) - expect(@student.pronouns).to eq 'He/Him/His' - expect(@student.identities).to eq 'something' - end - - it 'should not update user on skip' do - @student.update(major: '', grad_year: nil, pronouns: '', identities: '') - @student.reload - patch :profile_update, - params: { profile: { major: 'computer science', grad_year: DateTime.new(2022), pronouns: 'He/Him/His', - identities: 'something', skip: 'Skip' } } - expect(@student.major).to eq '' - expect(@student.grad_year).to eq nil - expect(@student.pronouns).to eq '' - expect(@student.identities).to eq '' - end - - it 'should redirect on logged-out user' do - controller.session[:current_user_id] = '' - patch :profile_update, - params: { profile: { major: 'computer science', grad_year: DateTime.new(2022), pronouns: 'He/Him/His', - identities: 'something', skip: 'Skip' } } - expect(response).to redirect_to root_path - end - - it 'should not say logged-in on profile edit' do - controller.session[:current_user_id] = @student.id - patch :profile_update, - params: { - profile: { major: 'computer science', grad_year: DateTime.new(2022), pronouns: 'He/Him/His', - identities: 'something' }, submit_edit: 'submit_edit' - } - expect(flash[:success]).to match(/Success! Your profile has been updated./) - end - end -end diff --git a/spec/factories/advisors.rb b/spec/factories/advisors.rb new file mode 100644 index 000000000..8b987db0e --- /dev/null +++ b/spec/factories/advisors.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :advisor do + name { "MyString" } + description { "MyText" } + calendar { "MyString" } + active { false } + end +end diff --git a/spec/factories/announcements.rb b/spec/factories/announcements.rb index 95659d125..bd5185c92 100644 --- a/spec/factories/announcements.rb +++ b/spec/factories/announcements.rb @@ -2,7 +2,7 @@ FactoryBot.define do factory :announcement do - Time.zone = 'Pacific Time (US & Canada)' + Time.zone = "Pacific Time (US & Canada)" sequence(:title) { |n| "Announcement #{n} Title" } sequence(:content) { |n| "Announcement #{n} content" } issued_date { DateTime.now.to_date } diff --git a/spec/factories/appointments.rb b/spec/factories/appointments.rb index 030a60413..5acbcb63d 100644 --- a/spec/factories/appointments.rb +++ b/spec/factories/appointments.rb @@ -2,9 +2,9 @@ FactoryBot.define do factory :appointment do - Time.zone = 'Pacific Time (US & Canada)' + Time.zone = "Pacific Time (US & Canada)" time { DateTime.now + 1.day } - location { 'ESS' } + location { "ESS" } association :staff, factory: :staff student { nil } end diff --git a/spec/factories/checkins.rb b/spec/factories/checkins.rb index c50d4f279..289a42597 100644 --- a/spec/factories/checkins.rb +++ b/spec/factories/checkins.rb @@ -2,9 +2,9 @@ FactoryBot.define do factory :checkin do - Time.zone = 'Pacific Time (US & Canada)' + Time.zone = "Pacific Time (US & Canada)" time { Time.now } - reason { 'Studying' } + reason { "Studying" } association :student, factory: :student end end diff --git a/spec/factories/events.rb b/spec/factories/events.rb new file mode 100644 index 000000000..df4d6bd09 --- /dev/null +++ b/spec/factories/events.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :event do + title { "MyString" } + date { "2025-04-03" } + start_time { "2025-04-03 14:54:29" } + end_time { "2025-04-03 14:54:29" } + location { "MyString" } + description { "MyText" } + end +end diff --git a/spec/factories/scholarships.rb b/spec/factories/scholarships.rb new file mode 100644 index 000000000..b3cf1fda2 --- /dev/null +++ b/spec/factories/scholarships.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :scholarship do + name { "MyString" } + description { "MyText" } + status_text { "MyString" } + application_url { "MyString" } + end +end diff --git a/spec/features/courses_spec.rb b/spec/features/courses_spec.rb new file mode 100644 index 000000000..b8428e675 --- /dev/null +++ b/spec/features/courses_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +# spec/features/courses_spec.rb + +require "rails_helper" + +STUDENT_CREDENTIALS = { + "provider" => "google_oauth2", + "uid" => "1000000000", + "info" => { + "name" => "Google Test Developer", + "email" => "google_student@berkeley.edu", + "first_name" => "Google", + "last_name" => "Test Developer" + }, + "credentials" => { + "token" => "credentials_token_1234567", + "refresh_token" => "credentials_refresh_token_45678" + } +} + +RSpec.feature "Courses", type: :feature do + before do + OmniAuth.config.add_mock( + :google_oauth2, + STUDENT_CREDENTIALS + ) + @student = FactoryBot.create :student, email: "google_student@berkeley.edu" + end + + scenario "user can view courses index page" do + visit root_path + click_button "Login with Google" + expect(page).to have_link("Our Courses", href: courses_path) + click_link "Our Courses" + + # This checks that we've reached the courses page by its title + expect(page).to have_content("Courses") + + # This checks that we either see courses or the "no courses" message + expect(page).to satisfy do |p| + p.has_css?(".card-header") || + p.has_content?("No courses are currently available") + end + + expect(current_path).to eq(courses_path) + end +end diff --git a/spec/features/scholarships_spec.rb b/spec/features/scholarships_spec.rb new file mode 100644 index 000000000..4c830904a --- /dev/null +++ b/spec/features/scholarships_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +# spec/features/scholarships_spec.rb + +require "rails_helper" + +STUDENT_CREDENTIALS = { + "provider" => "google_oauth2", + "uid" => "1000000000", + "info" => { + "name" => "Google Test Developer", + "email" => "google_student@berkeley.edu", + "first_name" => "Google", + "last_name" => "Test Developer" + }, + "credentials" => { + "token" => "credentials_token_1234567", + "refresh_token" => "credentials_refresh_token_45678" + } +} + +RSpec.feature "Scholarships", type: :feature do + before do + OmniAuth.config.add_mock( + :google_oauth2, + STUDENT_CREDENTIALS + ) + @student = FactoryBot.create :student, email: "google_student@berkeley.edu" + end + scenario "user can view scholsarships index page" do + visit root_path + click_button "Login with Google" + expect(page).to have_link("Re-entry Scholarships", href: scholarships_path) + click_link "Re-entry Scholarships" + expect(page).to have_content("Awards") + expect(current_path).to eq(scholarships_path) + end +end diff --git a/spec/models/admin_spec.rb b/spec/models/admin_spec.rb index 6bd093fd3..ab6e73d4e 100644 --- a/spec/models/admin_spec.rb +++ b/spec/models/admin_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'rails_helper' +require "rails_helper" RSpec.describe Admin, type: :model do pending "add some examples to (or delete) #{__FILE__}" diff --git a/spec/models/advisor_spec.rb b/spec/models/advisor_spec.rb new file mode 100644 index 000000000..952c13bc1 --- /dev/null +++ b/spec/models/advisor_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Advisor, type: :model do + it "is valid with valid attributes" do + advisor = Advisor.new(name: "John Doe", description: "Test description", calendar: "https://example.com", active: true) + expect(advisor).to be_valid + end + + it "is invalid without a name" do + advisor = Advisor.new(description: "Test description", calendar: "https://example.com", active: true) + expect(advisor).not_to be_valid + end + + it "is invalid with an invalid calendar link" do + advisor = Advisor.new(name: "John Doe", description: "Test description", calendar: "invalid-url", active: true) + expect(advisor).not_to be_valid + end +end diff --git a/spec/models/announcement_spec.rb b/spec/models/announcement_spec.rb index bd7e089ca..2e1b98403 100644 --- a/spec/models/announcement_spec.rb +++ b/spec/models/announcement_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'rails_helper' +require "rails_helper" RSpec.describe Announcement, type: :model do pending "add some examples to (or delete) #{__FILE__}" diff --git a/spec/models/appointment_spec.rb b/spec/models/appointment_spec.rb index 6c4457105..ebbbc086f 100644 --- a/spec/models/appointment_spec.rb +++ b/spec/models/appointment_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'rails_helper' +require "rails_helper" RSpec.describe Appointment, type: :model do pending "add some examples to (or delete) #{__FILE__}" diff --git a/spec/models/checkin_spec.rb b/spec/models/checkin_spec.rb index f093b70e2..e47f8ba93 100644 --- a/spec/models/checkin_spec.rb +++ b/spec/models/checkin_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'rails_helper' +require "rails_helper" RSpec.describe Checkin, type: :model do before do @@ -10,8 +10,8 @@ @all_sorted_records = Checkin.all.order(time: :desc) end - describe 'get_20_checkin_records' do - it 'n less than 1 should get the first 20 records in Checkin' do + describe "get_20_checkin_records" do + it "n less than 1 should get the first 20 records in Checkin" do n = -1 records_from_method = Checkin.get_20_checkin_records(n) expect(records_from_method).to eq @all_sorted_records[0, 20] diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb new file mode 100644 index 000000000..dc42c148b --- /dev/null +++ b/spec/models/event_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Event, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/scholarship_spec.rb b/spec/models/scholarship_spec.rb new file mode 100644 index 000000000..eec2d259c --- /dev/null +++ b/spec/models/scholarship_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Scholarship, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/staff_spec.rb b/spec/models/staff_spec.rb index d20bdcf7d..cf2d7681a 100644 --- a/spec/models/staff_spec.rb +++ b/spec/models/staff_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'rails_helper' +require "rails_helper" RSpec.describe Staff, type: :model do pending "add some examples to (or delete) #{__FILE__}" diff --git a/spec/models/student_spec.rb b/spec/models/student_spec.rb index 9efff46ae..998242d73 100644 --- a/spec/models/student_spec.rb +++ b/spec/models/student_spec.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true -require 'rails_helper' +require "rails_helper" RSpec.describe Student, type: :model do - it 'checks if student have sid before saving' do + it "checks if student have sid before saving" do n_student = FactoryBot.build :student n_student.sid = nil expect { n_student.save! }.to raise_error @@ -12,6 +12,6 @@ it "doesn't allows non student user to be saved as student" do n_student = FactoryBot.build :student n_student.is_student = false - expect { n_student.save! }.to raise_error(Exception, 'This user must be a student!!') + expect { n_student.save! }.to raise_error(Exception, "This user must be a student!!") end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 7da47d132..c8a8e7b37 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1,7 +1,78 @@ # frozen_string_literal: true -require 'rails_helper' +require "rails_helper" RSpec.describe User, type: :model do - pending "add some examples to (or delete) #{__FILE__}" + describe ".from_canvas" do + let(:user_data) do + { + "first_name" => "John", + "last_name" => "Doe", + "email" => "john.doe@berkeley.edu", + "sis_user_id" => 12345 + } + end + + context "when user does not exist" do + it "creates a new user with canvas data" do + expect { + user = User.from_canvas(user_data) + user.save + }.to change(User, :count).by(1) + + user = User.last + expect(user.first_name).to eq(user_data["first_name"]) + expect(user.last_name).to eq(user_data["last_name"]) + expect(user.email).to eq(user_data["email"]) + expect(user.sid).to eq(user_data["sis_user_id"]) + end + end + + context "when user already exists" do + let!(:existing_user) do + User.create!( + first_name: "John", + last_name: "Doe", + email: "john.doe@berkeley.edu" + ) + end + + it "returns the existing user without creating a new record" do + expect { + user = User.from_canvas(user_data) + user.save + }.not_to change(User, :count) + + expect(User.last).to eq(existing_user) + end + end + + context "with invalid data" do + let(:invalid_data) do + { + "first_name" => nil, + "last_name" => nil, + "email" => "invalid-email", + "sis_user_id" => 12345 + } + end + + it "returns an invalid user object" do + user = User.from_canvas(invalid_data) + expect(user).not_to be_valid + expect(user.errors[:first_name]).to include("can't be blank") + expect(user.errors[:last_name]).to include("can't be blank") + end + end + + context "with missing data" do + let(:missing_data) { {} } + + it "handles missing data gracefully" do + user = User.from_canvas(missing_data) + expect(user).not_to be_valid + expect(user.errors[:email]).to include("can't be blank") + end + end + end end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 09e19be5c..7b42f56f4 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -1,14 +1,15 @@ # frozen_string_literal: true +require "webmock/rspec" # require 'simplecov' # SimpleCov.start # This file is copied to spec/ when you run 'rails generate rspec:install' -require 'spec_helper' -ENV['RAILS_ENV'] ||= 'test' -require File.expand_path('../config/environment', __dir__) +require "spec_helper" +ENV["RAILS_ENV"] ||= "test" +require File.expand_path("../config/environment", __dir__) # Prevent database truncation if the environment is production -abort('The Rails environment is running in production mode!') if Rails.env.production? -require 'rspec/rails' +abort("The Rails environment is running in production mode!") if Rails.env.production? +require "rspec/rails" # Add additional requires below this line. Rails is not loaded until this point! # Requires supporting ruby files with custom matchers and macros, etc, in # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are @@ -23,7 +24,7 @@ # directory. Alternatively, in the individual `*_spec.rb` files, manually # require only the support files necessary. # -Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f } +Dir[Rails.root.join("spec", "support", "**", "*.rb")].sort.each { |f| require f } # Checks for pending migrations and applies them before tests are run. # If you are not using ActiveRecord, you can remove these lines. @@ -67,4 +68,7 @@ # setup for factory bot config.include FactoryBot::Syntax::Methods + config.before(:each, type: :controller) do + @request.env["HTTPS"] = "on" + end end diff --git a/spec/requests/admins_spec.rb b/spec/requests/admins_spec.rb new file mode 100755 index 000000000..35b00fb9a --- /dev/null +++ b/spec/requests/admins_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +# spec/requests/admins_spec.rb +require "rails_helper" + +RSpec.describe "Admins", type: :request do + describe "Access control for admin dashboard" do + it "blocks a student from accessing the admin dashboard" do + student = FactoryBot.create(:student) + sign_in_as(student) + get admins_path + expect(response).to redirect_to(root_path) + end + + it "blocks a staff member from accessing the admin dashboard" do + staff = FactoryBot.create(:staff) + sign_in_as(staff) + get admins_path + expect(response).to redirect_to(root_path) + end + + it "blocks a logged out user from accessing the admin dashboard" do + get admins_path + expect(response).to redirect_to(root_path) + end + end + + describe "View checkin records" do + before do + admin = FactoryBot.create(:admin) + sign_in_as(admin) + 50.times { FactoryBot.create(:checkin) } + end + + it "redirects to itself with params[:page] == 1 if params[:page] is nil or invalid" do + get view_checkin_records_path + expect(response).to redirect_to(view_checkin_records_path(page: 1)) + end + + it "sets has_next_page to false if the current page is the last page of checkin records" do + get view_checkin_records_path(page: Checkin.count / 20 + 1) + expect(assigns(:has_next_page)).to be_falsey + end + end +end diff --git a/spec/requests/checkin_spec.rb b/spec/requests/checkin_spec.rb new file mode 100755 index 000000000..2b9747f1f --- /dev/null +++ b/spec/requests/checkin_spec.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +# spec/requests/checkin_spec.rb + +require "rails_helper" + +RSpec.describe "Checkin", type: :request do + let(:student) { FactoryBot.create(:student) } + let(:expected_time) { DateTime.parse("2022-03-08T12:00:00-08:00") } + + before do + Timecop.freeze(expected_time) + end + + after do + Timecop.return + end + + describe "POST /checkin" do + context "when user is logged in" do + before do + sign_in_as(student) + @n_checkin_before = Checkin.count + end + + context "happy path" do + before do + post checkin_path, params: { checkin: { reason: "Studying" } } + @new_checkin = Checkin.order(id: :desc).first + end + + it "adds 1 new checkin record" do + expect(Checkin.count).to eq(@n_checkin_before + 1) + end + + it "creates a checkin record with the correct time" do + expect(@new_checkin.time).to eq(expected_time) + end + + it "associates the new checkin with the logged-in student" do + expect(@new_checkin.student).to eq(student) + end + + it "clears the flash when creating a new checkin" do + get new_checkin_path + expect(flash).to be_empty + end + end + + context "sad path" do + it "redirects to root if the record is invalid" do + post checkin_path, params: { checkin: { reason: nil } } + expect(response).to redirect_to(root_path) + expect(flash[:error]).to match(/Something went wrong, please try again/) + end + end + end + + context "when user is not logged in" do + it "redirects to the root path" do + post checkin_path, params: { checkin: { reason: "Studying" } } + expect(response).to redirect_to(root_path) + end + end + end + + describe "GET /checkin/new (autofill functionality)" do + context "when user has no previous check-ins" do + before do + sign_in_as(student) + get new_checkin_path + end + + it "sets default_reason to 'Peer Support'" do + expect(assigns(:default_reason)).to eq("Peer Support") + end + end + + context "when user has previous check-ins" do + before do + sign_in_as(student) + Checkin.create!(student: student, reason: "Studying", time: Time.current) + get new_checkin_path + end + + it "sets default_reason to the last used reason" do + expect(assigns(:default_reason)).to eq("Studying") + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 4f61f4d58..d0a3badf8 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require 'simplecov' -require 'simplecov_json_formatter' +require "simplecov" +require "simplecov_json_formatter" # SimpleCov.start # This file was generated by the `rails generate rspec:install` command. Conventionally, all # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. @@ -16,11 +16,11 @@ # a separate helper file that requires the additional dependencies and performs # the additional setup, and require it from the spec files that actually need # it. -SimpleCov.start 'rails' do +SimpleCov.start "rails" do # default rails files - add_filter '/app/mailers/' - add_filter '/app/jobs/' - add_filter '/app/channels/' + add_filter "/app/mailers/" + add_filter "/app/jobs/" + add_filter "/app/channels/" end SimpleCov.formatters = SimpleCov::Formatter::MultiFormatter.new([ SimpleCov::Formatter::JSONFormatter, diff --git a/spec/support/factory_bot.rb b/spec/support/factory_bot.rb old mode 100644 new mode 100755 diff --git a/spec/support/session_helpers.rb b/spec/support/session_helpers.rb new file mode 100755 index 000000000..46065f708 --- /dev/null +++ b/spec/support/session_helpers.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# spec/support/session_helpers.rb + +module SessionHelpers + def sign_in_as(user) + post "/__test_login", params: { id: user.id } + end +end + +RSpec.configure do |config| + config.include SessionHelpers, type: :request +end diff --git a/spec/support/test_routes.rb b/spec/support/test_routes.rb new file mode 100755 index 000000000..062ac8b68 --- /dev/null +++ b/spec/support/test_routes.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +# spec/support/test_routes.rb + +if Rails.env.test? && !Rails.application.routes.named_routes.key?(:__test_login) + Rails.application.routes.append do + post "__test_login", to: ->(env) { + req = ActionDispatch::Request.new(env) + req.session[:current_user_id] = req.params["id"] + [200, { "Content-Type" => "text/plain" }, ["Logged in"]] + } + end + + Rails.application.reload_routes! +end diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb index 652febbd6..c05709aff 100644 --- a/test/application_system_test_case.rb +++ b/test/application_system_test_case.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'test_helper' +require "test_helper" class ApplicationSystemTestCase < ActionDispatch::SystemTestCase driven_by :selenium, using: :chrome, screen_size: [1400, 1400] diff --git a/test/channels/application_cable/connection_test.rb b/test/channels/application_cable/connection_test.rb index 4aee9b335..baa2e3fbe 100644 --- a/test/channels/application_cable/connection_test.rb +++ b/test/channels/application_cable/connection_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'test_helper' +require "test_helper" module ApplicationCable class ConnectionTest < ActionCable::Connection::TestCase diff --git a/test/models/event_test.rb b/test/models/event_test.rb new file mode 100644 index 000000000..23ab7a388 --- /dev/null +++ b/test/models/event_test.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +# test/models/event_test.rb +require "test_helper" + +class EventTest < ActiveSupport::TestCase + test "should validate presence of required fields" do + event = Event.new + assert_not event.valid? + assert_includes event.errors[:title], "can't be blank" + assert_includes event.errors[:date], "can't be blank" + assert_includes event.errors[:start_time], "can't be blank" + assert_includes event.errors[:location], "can't be blank" + assert_includes event.errors[:description], "can't be blank" + end + + test "should be valid with all required fields" do + event = Event.new( + title: "Career Workshop", + date: Date.today, + start_time: Time.now, + location: "MLK Student Union", + description: "Learn resume skills" + ) + assert event.valid? + end + + test "formatted_date should return readable date" do + event = Event.new(date: Date.new(2025, 4, 15)) + assert_equal "Tuesday, April 15, 2025", event.formatted_date + end + + test "formatted_time should handle missing end_time" do + event = Event.new(start_time: Time.new(2025, 1, 1, 14, 0, 0)) + assert_equal "2:00 PM", event.formatted_time + end + + test "formatted_time should include range when end_time present" do + event = Event.new( + start_time: Time.new(2025, 1, 1, 14, 0, 0), + end_time: Time.new(2025, 1, 1, 16, 0, 0) + ) + assert_equal "2:00 PM - 4:00 PM", event.formatted_time + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 0c92e8e88..a64c49d49 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -ENV['RAILS_ENV'] ||= 'test' -require_relative '../config/environment' -require 'rails/test_help' +ENV["RAILS_ENV"] ||= "test" +require_relative "../config/environment" +require "rails/test_help" module ActiveSupport class TestCase