├── .dockerignore ├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .ruby-version ├── Dockerfile ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── Rakefile ├── app ├── assets │ ├── builds │ │ └── .keep │ ├── config │ │ └── manifest.js │ ├── images │ │ ├── .keep │ │ ├── flag-orpheus-top.png │ │ └── flag-standalone.png │ ├── stylesheets │ │ ├── simple_form.css │ │ └── theme.css │ └── tailwind │ │ └── application.css ├── channels │ └── application_cable │ │ ├── channel.rb │ │ └── connection.rb ├── controllers │ ├── admin │ │ ├── base_controller.rb │ │ ├── hackathons │ │ │ ├── addresses_controller.rb │ │ │ ├── expected_attendees_controller.rb │ │ │ ├── holds_controller.rb │ │ │ ├── names_controller.rb │ │ │ ├── submissions │ │ │ │ └── notifications_controller.rb │ │ │ ├── subscriptions_controller.rb │ │ │ ├── times_controller.rb │ │ │ └── websites_controller.rb │ │ ├── hackathons_controller.rb │ │ ├── users │ │ │ ├── email_addresses_controller.rb │ │ │ ├── names_controller.rb │ │ │ └── promotions_controller.rb │ │ └── users_controller.rb │ ├── api │ │ ├── base_controller.rb │ │ ├── hackathons │ │ │ └── subscriptions_controller.rb │ │ ├── hackathons_controller.rb │ │ └── stats │ │ │ ├── hackathons │ │ │ └── subscriptions_controller.rb │ │ │ └── hackathons_controller.rb │ ├── application_controller.rb │ ├── concerns │ │ ├── .keep │ │ ├── api │ │ │ └── errors.rb │ │ ├── audit1984.rb │ │ ├── authenticate.rb │ │ ├── hackathon_scoped.rb │ │ ├── read_only_mode.rb │ │ ├── set_current_request_details.rb │ │ ├── stream_flashes.rb │ │ └── user_scoped.rb │ ├── database_dumps_controller.rb │ ├── hackathons │ │ ├── submissions_controller.rb │ │ ├── subscriptions │ │ │ └── bulk_controller.rb │ │ └── subscriptions_controller.rb │ ├── hackathons_controller.rb │ └── users │ │ ├── authentications_controller.rb │ │ └── sessions_controller.rb ├── errors │ ├── api │ │ ├── bad_request_error.rb │ │ ├── error.rb │ │ ├── invalid_api_version_error.rb │ │ ├── not_found_error.rb │ │ └── read_only_mode_error.rb │ └── read_only_mode_error.rb ├── helpers │ ├── api_helper.rb │ ├── application_helper.rb │ └── mailer_helper.rb ├── javascript │ ├── application.js │ ├── appsignal.js │ ├── controllers │ │ ├── application.js │ │ ├── element_removal_controller.js │ │ ├── form │ │ │ ├── required_checkboxes_controller.js │ │ │ └── visibility_controller.js │ │ ├── form_controller.js │ │ ├── index.js │ │ └── test_readiness_controller.js │ └── custom_turbo_stream_actions.js ├── jobs │ ├── application_job.rb │ ├── database_dump_job.rb │ ├── hackathons │ │ ├── digests_delivery_job.rb │ │ ├── swag_request_delivery_job.rb │ │ ├── website_archival_job.rb │ │ ├── website_archivals_job.rb │ │ ├── website_status_refresh_job.rb │ │ └── website_statuses_refresh_job.rb │ └── loops_synchronization_job.rb ├── mailers │ ├── application_mailer.rb │ ├── hackathon_mailer.rb │ ├── hackathons │ │ ├── digest_mailer.rb │ │ └── submission_mailer.rb │ └── user_mailer.rb ├── models │ ├── application_record.rb │ ├── concerns │ │ ├── .keep │ │ ├── broadcasting.rb │ │ ├── delivered.rb │ │ ├── eventable.rb │ │ ├── rate_limitable.rb │ │ ├── suppressible.rb │ │ └── taggable.rb │ ├── current.rb │ ├── database_dump.rb │ ├── database_dump │ │ └── processed.rb │ ├── event.rb │ ├── event │ │ ├── request.rb │ │ └── requested.rb │ ├── hackathon.rb │ ├── hackathon │ │ ├── applicant.rb │ │ ├── branded.rb │ │ ├── digest.rb │ │ ├── digest │ │ │ ├── listing.rb │ │ │ ├── listings.rb │ │ │ └── listings │ │ │ │ ├── by_location.rb │ │ │ │ └── minimizing_previously_listed_hackathons.rb │ │ ├── financially_assisting.rb │ │ ├── gathering.rb │ │ ├── named.rb │ │ ├── notifying.rb │ │ ├── regional.rb │ │ ├── reviewable.rb │ │ ├── scheduled.rb │ │ ├── status.rb │ │ ├── subscription.rb │ │ ├── subscription │ │ │ ├── regional.rb │ │ │ └── status.rb │ │ ├── swag.rb │ │ ├── swag_request.rb │ │ ├── swag_request │ │ │ └── delivered.rb │ │ ├── website.rb │ │ └── website │ │ │ └── archivable.rb │ ├── location.rb │ ├── lock.rb │ ├── mailing_address.rb │ ├── tag.rb │ ├── tagging.rb │ ├── user.rb │ └── user │ │ ├── authenticatable.rb │ │ ├── authentication.rb │ │ ├── identifiable.rb │ │ ├── informed.rb │ │ ├── named.rb │ │ ├── privileged.rb │ │ ├── session.rb │ │ ├── settings.rb │ │ └── subscriber.rb └── views │ ├── admin │ ├── _header.html.erb │ ├── hackathons │ │ ├── _snippet.html.erb │ │ ├── addresses │ │ │ └── edit.html.erb │ │ ├── expected_attendees │ │ │ └── edit.html.erb │ │ ├── index.html.erb │ │ ├── names │ │ │ └── edit.html.erb │ │ ├── show.html.erb │ │ ├── times │ │ │ └── edit.html.erb │ │ └── websites │ │ │ └── edit.html.erb │ ├── header │ │ ├── _heading.html.erb │ │ ├── _nav.html.erb │ │ └── nav │ │ │ └── _link.html.erb │ └── users │ │ ├── email_addresses │ │ └── edit.html.erb │ │ ├── index.html.erb │ │ ├── names │ │ └── edit.html.erb │ │ └── show.html.erb │ ├── api │ ├── errors │ │ └── _error.json.jbuilder │ ├── hackathons │ │ ├── _hackathon.json.v1.jbuilder │ │ ├── index.json.v1.jbuilder │ │ ├── show.json.v1.jbuilder │ │ └── subscriptions │ │ │ └── _subscription.json.jbuilder │ ├── stats │ │ └── hackathons │ │ │ ├── index.json.v1.jbuilder │ │ │ └── subscriptions │ │ │ └── index.json.v1.jbuilder │ └── users │ │ └── _user.json.jbuilder │ ├── database_dumps │ ├── edit.html.erb │ └── index.html.erb │ ├── events │ └── _timeline.html.erb │ ├── hackathon_mailer │ └── swag_request.text.erb │ ├── hackathons │ ├── digest_mailer │ │ ├── _hackathon.html.erb │ │ ├── _hackathon.text.erb │ │ ├── _subscription.html.erb │ │ ├── _subscription.text.erb │ │ ├── admin_summary.html.erb │ │ ├── digest.html.erb │ │ └── digest.text.erb │ ├── index.html.erb │ ├── submission_mailer │ │ ├── admin_notification.html.erb │ │ ├── admin_notification.text.erb │ │ ├── approval.html.erb │ │ ├── approval.text.erb │ │ ├── confirmation.html.erb │ │ └── confirmation.text.erb │ ├── submissions │ │ ├── _form.html.erb │ │ ├── index.html.erb │ │ ├── new.html.erb │ │ └── show.html.erb │ └── subscriptions │ │ ├── _manage.html.erb │ │ ├── index.html.erb │ │ └── unsubscribe_all.html.erb │ ├── layouts │ ├── admin.html.erb │ ├── application.html.erb │ ├── mailer.html.erb │ └── mailer.text.erb │ ├── mailing_addresses │ └── _fields.html.erb │ ├── shared │ ├── _flash.html.erb │ ├── _footer.html.erb │ └── _full_page_card.html.erb │ ├── user_mailer │ ├── authentication.html.erb │ └── authentication.text.erb │ └── users │ └── authentications │ └── new.html.erb ├── bin ├── bundle ├── docker-entrypoint ├── importmap ├── rails ├── rake └── setup ├── config.ru ├── config ├── application.rb ├── appsignal.yml ├── boot.rb ├── cable.yml ├── credentials.yml.enc ├── credentials │ ├── development.key │ ├── development.yml.enc │ ├── test.key │ └── test.yml.enc ├── database.yml ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── importmap.rb ├── initializers │ ├── action_mailer_concurrency.rb │ ├── assets.rb │ ├── console1984.rb │ ├── constants.rb │ ├── content_security_policy.rb │ ├── cors.rb │ ├── filter_parameter_logging.rb │ ├── geocoder.rb │ ├── hashid.rb │ ├── inflections.rb │ ├── permissions_policy.rb │ ├── read_only_mode.rb │ ├── simple_form.rb │ └── versioncake.rb ├── locales │ ├── en.yml │ └── simple_form.en.yml ├── puma.rb ├── queue.yml ├── recurring.yml ├── routes.rb ├── schedule.yml └── storage.yml ├── db ├── migrate │ ├── 20230710190106_create_users.rb │ ├── 20230711133014_create_events.rb │ ├── 20230711134548_create_hackathons.rb │ ├── 20230714152817_create_tags.rb │ ├── 20230717112942_make_hackathon_dates_not_null.rb │ ├── 20230717171620_add_street_to_hackathons.rb │ ├── 20230718091913_add_timestamps_to_hackathons.rb │ ├── 20230718223502_create_user_sessions.rb │ ├── 20230718223503_create_event_requests.rb │ ├── 20230718235719_add_applicant_to_hackathons.rb │ ├── 20230719004620_add_fields_to_hackathons.rb │ ├── 20230719015234_create_active_storage_tables.active_storage.rb │ ├── 20230719234350_add_default_true_to_high_school_led_on_hackathons.rb │ ├── 20230720000103_create_mailing_addresses.rb │ ├── 20230720054548_create_hackathon_subscriptions.rb │ ├── 20230720205641_create_hackathon_digests.rb │ ├── 20230726064503_add_belongs_to_subscription_on_hackathon_digest_lisings.rb │ ├── 20230726225359_drop_financial_assistance_on_hackathons.rb │ ├── 20230728125411_allow_null_on_hackathon_expected_attendees.rb │ ├── 20230811051252_make_website_optional_on_hackathons.rb │ ├── 20230814030407_add_apac_to_hackathon.rb │ ├── 20230815050917_add_airtable_id_to_hackathons.rb │ ├── 20230815052624_add_airtable_id_to_hackathon_subscriptions.rb │ ├── 20230815053636_make_auth_tokens_unique.rb │ ├── 20230815191654_create_console1984_tables.console1984.rb │ ├── 20230815205623_create_auditing_tables.audits1984.rb │ ├── 20230818183712_index_created_at_for_hackathon_digests.rb │ ├── 20231204215503_change_jfif_files_to_jpeg.rb │ ├── 20231223215503_add_settings_to_users.rb │ ├── 20240107231455_create_database_dumps.rb │ ├── 20240206140353_create_swag_requests.rb │ ├── 20240426144308_add_contacts_to_loops.rb │ ├── 20240704165824_jsonb_to_json.rb │ ├── 20241125161900_create_solid_queue_tables.rb │ ├── 20250103164545_create_locks.rb │ ├── 20250128210851_remove_unique_constraint_on_locks.rb │ └── 20250220140522_alpha3_country_codes_to_alpha2.rb ├── schema.rb └── seeds.rb ├── docker-compose.production.yml ├── docker-compose.yml ├── lib ├── assets │ └── .keep ├── constraints │ └── admin.rb ├── http_url_validator.rb ├── internet_archive.rb ├── internet_archive │ └── capture.rb ├── loops.rb ├── loops │ ├── contact.rb │ └── resource.rb ├── puma │ └── plugin │ │ └── dartsass.rb ├── tasks │ ├── hackathons.rake │ └── to_stdout.rake └── templates │ └── erb │ └── scaffold │ └── _form.html.erb ├── log └── .keep ├── public ├── 404.html ├── 422.html ├── 500.html ├── apple-touch-icon-precomposed.png ├── apple-touch-icon.png ├── favicon.ico └── robots.txt ├── storage └── .keep ├── test ├── application_system_test_case.rb ├── fixtures │ ├── active_storage │ │ ├── attachments.yml │ │ └── blobs.yml │ ├── files │ │ ├── assemble.jpg │ │ ├── assemble_logo.jpg │ │ └── hack_club_logo.jpg │ ├── hackathon │ │ ├── digest │ │ │ └── listings.yml │ │ ├── digests.yml │ │ └── subscriptions.yml │ ├── hackathons.yml │ ├── mailing_addresses.yml │ └── users.yml ├── mailers │ ├── hackathons │ │ ├── digest_mailer_test.rb │ │ └── submission_mailer_test.rb │ └── previews │ │ └── hackathon │ │ └── digest_mailer_preview.rb ├── models │ ├── database_dump_test.rb │ ├── hackathon │ │ ├── digest │ │ │ └── listings │ │ │ │ └── listings_test.rb │ │ ├── regional_test.rb │ │ └── subscription_test.rb │ ├── hackathon_test.rb │ ├── location_test.rb │ ├── mailing_address_test.rb │ ├── tag_test.rb │ ├── tagging_test.rb │ ├── user │ │ ├── informed_test.rb │ │ └── privilege_test.rb │ └── user_test.rb ├── system │ ├── admin │ │ └── users_test.rb │ ├── authentication_test.rb │ ├── hackathon_submission_test.rb │ └── read_only_mode_test.rb └── test_helper.rb ├── tmp ├── .keep ├── pids │ └── .keep └── storage │ └── .keep └── vendor ├── .keep └── javascript └── .keep /.dockerignore: -------------------------------------------------------------------------------- 1 | # See https://docs.docker.com/engine/reference/builder/#dockerignore-file for more about ignoring files. 2 | 3 | # Ignore bundler config. 4 | /.bundle 5 | /vendor/bundle 6 | 7 | # Ignore all logfiles and tempfiles. 8 | /log/* 9 | /tmp/* 10 | !/log/.keep 11 | !/tmp/.keep 12 | 13 | # Ignore pidfiles, but keep the directory. 14 | /tmp/pids/* 15 | !/tmp/pids/ 16 | !/tmp/pids/.keep 17 | 18 | # Ignore uploaded files in development. 19 | /storage/* 20 | !/storage/.keep 21 | /tmp/storage/* 22 | !/tmp/storage/ 23 | !/tmp/storage/.keep 24 | 25 | /public/assets 26 | 27 | # Ignore master key for decrypting credentials and more. 28 | /config/master.key 29 | 30 | # Environment variables 31 | .env* 32 | !.env*example 33 | 34 | # IDEs 35 | .idea 36 | .vscode 37 | 38 | # OS 39 | .DS_Store 40 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | db/schema.rb linguist-generated 2 | 3 | vendor/* linguist-vendored 4 | 5 | config/credentials/*.yml.enc diff=rails_credentials 6 | config/credentials.yml.enc diff=rails_credentials 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | time: "10:00" 8 | timezone: "America/New_York" 9 | 10 | - package-ecosystem: "bundler" 11 | directory: "/" 12 | schedule: 13 | interval: "monthly" 14 | time: "10:00" 15 | timezone: "America/New_York" 16 | open-pull-requests-limit: 10 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches-ignore: [main] 5 | permissions: 6 | packages: write 7 | jobs: 8 | standard: 9 | name: Standard 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | - name: Setup Ruby 15 | uses: ruby/setup-ruby@v1 16 | with: 17 | bundler-cache: true 18 | - name: Run Standard 19 | run: bundle exec standardrb 20 | test: 21 | name: Test 22 | runs-on: ubuntu-latest 23 | timeout-minutes: 15 24 | steps: 25 | - name: Install dependencies 26 | run: | 27 | sudo apt-get install postgresql-common 28 | sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y 29 | sudo apt-get update 30 | sudo apt-get install libvips postgresql-client 31 | - name: Checkout code 32 | uses: actions/checkout@v4 33 | - name: Setup Ruby 34 | uses: ruby/setup-ruby@v1 35 | with: 36 | bundler-cache: true 37 | - name: Setup Postgres 38 | run: docker compose up --detach 39 | - name: Run tests 40 | run: bin/rails test; bin/rails test:system 41 | - name: Keep screenshots from failed system tests 42 | uses: actions/upload-artifact@v4 43 | if: failure() 44 | with: 45 | name: screenshots 46 | path: ${{ github.workspace }}/tmp/screenshots 47 | if-no-files-found: ignore 48 | build: 49 | name: Build 50 | runs-on: ubuntu-latest 51 | steps: 52 | - name: Checkout code 53 | uses: actions/checkout@v4 54 | - name: Setup Docker 55 | uses: docker/setup-buildx-action@v3 56 | - name: Login to GitHub Packages 57 | uses: docker/login-action@v3 58 | with: 59 | registry: ghcr.io 60 | username: ${{ github.actor }} 61 | password: ${{ github.token }} 62 | - name: Build and push Docker image 63 | uses: docker/build-push-action@v6 64 | with: 65 | cache-from: type=gha 66 | cache-to: type=gha,mode=max 67 | push: true 68 | tags: ghcr.io/${{ github.repository }}:${{ github.sha }} 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | /vendor/bundle 10 | 11 | # Ignore all logfiles and tempfiles. 12 | /log/* 13 | /tmp/* 14 | !/log/.keep 15 | !/tmp/.keep 16 | 17 | # Ignore pidfiles, but keep the directory. 18 | /tmp/pids/* 19 | !/tmp/pids/ 20 | !/tmp/pids/.keep 21 | 22 | # Ignore uploaded files in development. 23 | /storage/* 24 | !/storage/.keep 25 | /tmp/storage/* 26 | !/tmp/storage/ 27 | !/tmp/storage/.keep 28 | 29 | /app/assets/builds/* 30 | !/app/assets/builds/.keep 31 | /public/assets 32 | 33 | # Ignore master key for decrypting credentials and more. 34 | /config/master.key 35 | 36 | # Environment variables 37 | .env* 38 | !.env*example 39 | 40 | # IDEs 41 | .idea 42 | .vscode 43 | 44 | # OS 45 | .DS_Store 46 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.4.2 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:3.4.2-slim AS base 2 | 3 | WORKDIR /hackathons 4 | 5 | ENV RAILS_ENV="production" \ 6 | BUNDLE_DEPLOYMENT="1" \ 7 | BUNDLE_PATH="/usr/local/bundle" \ 8 | BUNDLE_WITHOUT="development" 9 | 10 | 11 | FROM base AS build 12 | 13 | RUN apt-get update -qq && \ 14 | apt-get install --no-install-recommends -y build-essential git pkg-config libyaml-dev libpq-dev libvips 15 | 16 | COPY .ruby-version Gemfile Gemfile.lock ./ 17 | RUN bundle install && \ 18 | rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \ 19 | bundle exec bootsnap precompile --gemfile 20 | 21 | 22 | COPY . . 23 | 24 | RUN bundle exec bootsnap precompile app/ lib/ 25 | 26 | RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile 27 | 28 | 29 | FROM base 30 | 31 | RUN apt-get update -qq && \ 32 | apt-get install --no-install-recommends -y postgresql-common libvips curl ca-certificates lsb-release libjemalloc2 && \ 33 | install -d /usr/share/postgresql-common/pgdg && curl -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc --fail https://www.postgresql.org/media/keys/ACCC4CF8.asc && \ 34 | sh -c 'echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' && \ 35 | apt-get update -qq && apt-get install --no-install-recommends -y postgresql-client && \ 36 | rm -rf /var/lib/apt/lists /var/cache/apt/archives 37 | 38 | ENV LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2 39 | 40 | COPY --from=build /usr/local/bundle /usr/local/bundle 41 | COPY --from=build /hackathons /hackathons 42 | 43 | RUN useradd hackathons --create-home --shell /bin/bash && \ 44 | chown -R hackathons:hackathons db log storage tmp 45 | USER hackathons:hackathons 46 | 47 | ENTRYPOINT ["/hackathons/bin/docker-entrypoint"] 48 | 49 | EXPOSE 3000 50 | CMD ["./bin/rails", "server"] 51 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 3 | 4 | ruby file: ".ruby-version" 5 | 6 | gem "rails", github: "rails/rails" 7 | 8 | gem "dotenv-rails", require: "dotenv/load" 9 | 10 | # Drivers 11 | gem "pg" 12 | gem "puma" 13 | 14 | # Assets 15 | gem "sprockets-rails" 16 | gem "tailwindcss-rails" 17 | gem "importmap-rails" 18 | gem "turbo-rails" 19 | gem "stimulus-rails" 20 | gem "local_time" 21 | gem "premailer-rails" # Inline CSS for emails 22 | 23 | # Active Storage 24 | gem "aws-sdk-s3", require: false 25 | gem "image_processing", ">= 1.2" 26 | gem "active_storage_validations" 27 | 28 | # Background jobs 29 | gem "solid_queue" 30 | gem "mission_control-jobs" 31 | 32 | # API 33 | gem "jbuilder" # JSON templating 34 | gem "versioncake" # API versioning 35 | gem "rack-cors" # Cross-origin resource sharing 36 | gem "faraday" # GET / POST requests 37 | gem "faraday-follow_redirects" 38 | 39 | gem "geared_pagination" 40 | 41 | # User interface 42 | gem "simple_form" 43 | gem "country_select" 44 | 45 | # Geography 46 | gem "geocoder" 47 | gem "aws-sdk-locationservice" 48 | gem "countries" 49 | 50 | gem "hashid-rails" # Non-sequential IDs 51 | 52 | gem "tzinfo-data", platforms: %i[mingw mswin x64_mingw jruby] # Windows doesn't include zoneinfo files 53 | gem "bootsnap", require: false # reduces boot times through caching; required in config/boot.rb 54 | 55 | gem "appsignal" 56 | gem "lograge" 57 | 58 | gem "console1984" 59 | gem "audits1984" 60 | 61 | group :development, :test do 62 | gem "debug", platforms: %i[mri mingw x64_mingw] 63 | 64 | # Code Critics 65 | gem "standard" 66 | end 67 | 68 | group :development do 69 | gem "web-console" 70 | gem "rack-mini-profiler" 71 | gem "letter_opener_web" 72 | gem "better_errors" 73 | gem "binding_of_caller" # used by better_errors 74 | end 75 | 76 | group :test do 77 | gem "webmock" 78 | 79 | gem "capybara" 80 | gem "cuprite" 81 | end 82 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) The Hack Foundation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative "config/application" 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /app/assets/builds/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackathons-backend/92a9670ac669d9e3ec3237801f1d8949f6e7e552/app/assets/builds/.keep -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_tree ../builds 3 | //= link_tree ../../javascript .js 4 | //= link_tree ../../../vendor/javascript .js 5 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackathons-backend/92a9670ac669d9e3ec3237801f1d8949f6e7e552/app/assets/images/.keep -------------------------------------------------------------------------------- /app/assets/images/flag-orpheus-top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackathons-backend/92a9670ac669d9e3ec3237801f1d8949f6e7e552/app/assets/images/flag-orpheus-top.png -------------------------------------------------------------------------------- /app/assets/images/flag-standalone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackathons-backend/92a9670ac669d9e3ec3237801f1d8949f6e7e552/app/assets/images/flag-standalone.png -------------------------------------------------------------------------------- /app/assets/stylesheets/simple_form.css: -------------------------------------------------------------------------------- 1 | form.simple_form { 2 | input:not([type="file"]):not([type="checkbox"]), textarea, select { 3 | border: solid 2px var(--sunken); 4 | min-width: 200px; 5 | } 6 | 7 | label { 8 | flex-direction: row; 9 | row-gap: var(--spacing-2); 10 | font-weight: 800; 11 | } 12 | 13 | /* Simple form's asterisk (for required fields) */ 14 | abbr[title] { 15 | text-decoration: none; 16 | margin-left: 2px; 17 | color: var(--red); 18 | opacity: 60%; 19 | } 20 | 21 | div.input { 22 | width: 100%; 23 | 24 | display: flex; 25 | flex-direction: column; 26 | align-items: flex-start; 27 | } 28 | 29 | .form__inputs, .form__inputs--nested { 30 | display: flex; 31 | flex-direction: column; 32 | row-gap: var(--spacing-4); 33 | 34 | input[type=email], input[type=text], input[type=url] textarea { 35 | width: 100%; 36 | } 37 | 38 | .hint { 39 | margin-bottom: var(--spacing-2); 40 | } 41 | } 42 | 43 | .field_with_errors { 44 | label { 45 | color: var(--red); 46 | } 47 | } 48 | 49 | .error { 50 | color: var(--red); 51 | } 52 | 53 | .form__inputs--nested { 54 | @media screen and (min-width: 32em) { 55 | border-left: dotted 2px var(--sunken); 56 | padding-left: var(--spacing-3); 57 | margin-top: calc(var(--spacing-4) / -2); 58 | } 59 | } 60 | 61 | .form__inputs--nested > div > label { 62 | font-size: 18px; 63 | margin-bottom: var(--spacing-2); 64 | } 65 | 66 | .form__inputs--nested > .lead { 67 | font-size: var(--font-2)!important; 68 | } 69 | 70 | .form__actions { 71 | margin-top: var(--spacing-4); 72 | } 73 | 74 | .input.file { 75 | padding-left: 0px; 76 | } 77 | 78 | input::file-selector-button { 79 | font-family: "Phantom Sans"; 80 | padding: 4px; 81 | text-align: left; 82 | background: var(--sunken); 83 | border: 1px solid var(--border); 84 | border-radius: var(--radii-small); 85 | } 86 | 87 | h1 > a { 88 | text-decoration: none; 89 | } 90 | 91 | h1 { 92 | word-wrap: break-word; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app/assets/tailwind/application.css: -------------------------------------------------------------------------------- 1 | @layer tailwind-theme, base, components, utilities; 2 | 3 | @import "tailwindcss/theme.css" layer(tailwind-theme); 4 | @import "tailwindcss/utilities.css" layer(utilities); 5 | 6 | @import "../stylesheets/simple_form"; 7 | 8 | body { 9 | overflow-y: scroll; 10 | scrollbar-gutter: stable; 11 | } 12 | 13 | /* redefining .container here so Hack Club's theme doesn't conflict with Tailwind */ 14 | .container { 15 | width: 100%; 16 | margin: auto; 17 | padding-left: var(--spacing-3); 18 | padding-right: var(--spacing-3); 19 | 20 | @media screen and (max-width: 32em) { 21 | padding: inherit var(--spacing-3); 22 | } 23 | 24 | .full { 25 | max-width: 100%; 26 | padding: 0; 27 | margin: 0; 28 | } 29 | } 30 | @media screen { 31 | @media (min-width: 32em) { 32 | .container { 33 | max-width: var(--size-layout); 34 | } 35 | .container.copy { 36 | max-width: var(--size-copy); 37 | } 38 | .container.narrow { 39 | max-width: var(--size-narrow); 40 | } 41 | } 42 | 43 | @media (min-width: 64em) { 44 | .container { 45 | max-width: var(--size-layout-plus); 46 | } 47 | .container.wide { 48 | max-width: var(--size-wide); 49 | } 50 | .container.copy { 51 | max-width: var(--size-copy-plus); 52 | } 53 | .container.narrow { 54 | max-width: var(--size-narrow-plus); 55 | } 56 | } 57 | } 58 | 59 | @theme { 60 | @keyframes appear-then-fade { 61 | 0%,100% { 62 | opacity: 0 63 | } 64 | 65 | 6%,66% { 66 | opacity: 1 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Channel < ActionCable::Channel::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/admin/base_controller.rb: -------------------------------------------------------------------------------- 1 | class Admin::BaseController < ApplicationController 2 | layout "admin" 3 | end 4 | -------------------------------------------------------------------------------- /app/controllers/admin/hackathons/addresses_controller.rb: -------------------------------------------------------------------------------- 1 | class Admin::Hackathons::AddressesController < Admin::BaseController 2 | include HackathonScoped 3 | 4 | def edit 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/controllers/admin/hackathons/expected_attendees_controller.rb: -------------------------------------------------------------------------------- 1 | class Admin::Hackathons::ExpectedAttendeesController < Admin::BaseController 2 | include HackathonScoped 3 | 4 | def edit 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/controllers/admin/hackathons/holds_controller.rb: -------------------------------------------------------------------------------- 1 | class Admin::Hackathons::HoldsController < Admin::BaseController 2 | include HackathonScoped 3 | 4 | def create 5 | @hackathon.hold 6 | redirect_to admin_hackathon_path(@hackathon) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/controllers/admin/hackathons/names_controller.rb: -------------------------------------------------------------------------------- 1 | class Admin::Hackathons::NamesController < Admin::BaseController 2 | include HackathonScoped 3 | 4 | def edit 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/controllers/admin/hackathons/submissions/notifications_controller.rb: -------------------------------------------------------------------------------- 1 | class Admin::Hackathons::Submissions::NotificationsController < Admin::BaseController 2 | def index 3 | Current.user.update! new_hackathon_submission_notifications: !Current.user.new_hackathon_submission_notifications 4 | action = Current.user.new_hackathon_submission_notifications ? "enabled" : "disabled" 5 | redirect_to admin_hackathons_path, notice: "Notifications #{action} for new submissions" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/controllers/admin/hackathons/subscriptions_controller.rb: -------------------------------------------------------------------------------- 1 | class Admin::Hackathons::SubscriptionsController < Admin::BaseController 2 | before_action :set_subscription 3 | 4 | def destroy 5 | @subscription.unsubscribe 6 | redirect_to admin_user_path(@subscription.subscriber) 7 | end 8 | 9 | private 10 | 11 | def set_subscription 12 | @subscription = Hackathon::Subscription.find(params[:id]) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/controllers/admin/hackathons/times_controller.rb: -------------------------------------------------------------------------------- 1 | class Admin::Hackathons::TimesController < Admin::BaseController 2 | include HackathonScoped 3 | 4 | def edit 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/controllers/admin/hackathons/websites_controller.rb: -------------------------------------------------------------------------------- 1 | class Admin::Hackathons::WebsitesController < Admin::BaseController 2 | include HackathonScoped 3 | 4 | def edit 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/controllers/admin/hackathons_controller.rb: -------------------------------------------------------------------------------- 1 | class Admin::HackathonsController < Admin::BaseController 2 | before_action :set_hackathon, except: :index 3 | 4 | def index 5 | set_page_and_extract_portion_from Hackathon.all.with_attached_logo, ordered_by: {id: :desc} 6 | end 7 | 8 | def show 9 | end 10 | 11 | def edit 12 | end 13 | 14 | def update 15 | if @hackathon.update(hackathon_params) 16 | redirect_to admin_hackathon_path(@hackathon) 17 | else 18 | stream_flash_notice @hackathon.errors.full_messages.to_sentence 19 | end 20 | end 21 | 22 | def destroy 23 | if @hackathon.destroy 24 | redirect_to admin_hackathons_path, notice: "#{@hackathon.name} has been deleted." 25 | else 26 | render :show, status: :unprocessable_entity 27 | end 28 | end 29 | 30 | private 31 | 32 | def hackathon_params 33 | params.require(:hackathon).permit( 34 | :name, 35 | :status, 36 | :website, 37 | :logo, 38 | :banner, 39 | :starts_at, 40 | :ends_at, 41 | :modality, 42 | :address, 43 | :expected_attendees, 44 | :offers_financial_assistance, 45 | :requested_swag, 46 | swag_mailing_address_attributes: [ 47 | :line1, 48 | :line2, 49 | :city, 50 | :province, 51 | :postal_code, 52 | :country_code 53 | ], 54 | applicant: [ 55 | :name, 56 | :email_address 57 | ] 58 | ) 59 | end 60 | 61 | def set_hackathon 62 | @hackathon = Hackathon.find(params[:id]) 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /app/controllers/admin/users/email_addresses_controller.rb: -------------------------------------------------------------------------------- 1 | class Admin::Users::EmailAddressesController < Admin::BaseController 2 | include UserScoped 3 | 4 | def edit 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/controllers/admin/users/names_controller.rb: -------------------------------------------------------------------------------- 1 | class Admin::Users::NamesController < Admin::BaseController 2 | include UserScoped 3 | 4 | def edit 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/controllers/admin/users/promotions_controller.rb: -------------------------------------------------------------------------------- 1 | class Admin::Users::PromotionsController < Admin::BaseController 2 | include UserScoped 3 | 4 | def create 5 | @user.update! admin: true 6 | 7 | redirect_to admin_user_path(@user) 8 | end 9 | 10 | def destroy 11 | @user.update! admin: false 12 | 13 | redirect_to admin_user_path(@user) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/controllers/admin/users_controller.rb: -------------------------------------------------------------------------------- 1 | class Admin::UsersController < Admin::BaseController 2 | before_action :set_user, except: :index 3 | 4 | def index 5 | @email_address = params[:email_address] 6 | return unless @email_address.present? 7 | 8 | if (user = User.find_by_email_address @email_address.downcase) 9 | redirect_to admin_user_path(user) 10 | else 11 | stream_flash_notice "User not found." 12 | end 13 | end 14 | 15 | def show 16 | end 17 | 18 | def update 19 | if @user.update(user_params) 20 | redirect_to admin_user_path(@user) 21 | else 22 | stream_flash_notice @user.errors.full_messages.first 23 | end 24 | end 25 | 26 | private 27 | 28 | def set_user 29 | @user = User.find(params[:id]) 30 | end 31 | 32 | def user_params 33 | params.require(:user).permit(:name, :email_address, *User::SETTINGS) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /app/controllers/api/base_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::BaseController < ActionController::Base 2 | skip_before_action :verify_authenticity_token # CSRF 3 | 4 | include Api::Errors 5 | 6 | before_action :set_request_version 7 | 8 | private 9 | 10 | def set_request_version 11 | @request_version = request_version 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/controllers/api/hackathons/subscriptions_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::Hackathons::SubscriptionsController < Api::BaseController 2 | def create 3 | ActiveRecord::Base.transaction do 4 | user = User.find_or_create_by!(email_address: params.require(:email)) 5 | @subscription = Hackathon::Subscription.create!( 6 | location_input: params.require(:location), 7 | subscriber: user 8 | ) 9 | end 10 | 11 | render partial: "api/hackathons/subscriptions/subscription", locals: {subscription: @subscription} 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/controllers/api/hackathons_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::HackathonsController < Api::BaseController 2 | def index 3 | set_page_and_extract_portion_from( 4 | Hackathon.approved 5 | .includes(:tags, :events) 6 | .with_attached_logo.with_attached_banner, 7 | ordered_by: {id: :desc}, per_page: 50 8 | ) 9 | end 10 | 11 | def show 12 | @hackathon = Hackathon.approved.find_by_hashid!(params[:id]) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/controllers/api/stats/hackathons/subscriptions_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::Stats::Hackathons::SubscriptionsController < Api::BaseController 2 | def index 3 | @subscriptions = Hackathon::Subscription.active 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/controllers/api/stats/hackathons_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::Stats::HackathonsController < Api::BaseController 2 | def index 3 | @hackathons = Hackathon 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | include Audit1984 3 | 4 | include SetCurrentRequestDetails 5 | include Authenticate 6 | include StreamFlashes 7 | include ReadOnlyMode 8 | end 9 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackathons-backend/92a9670ac669d9e3ec3237801f1d8949f6e7e552/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /app/controllers/concerns/audit1984.rb: -------------------------------------------------------------------------------- 1 | module Audit1984 2 | private 3 | 4 | def find_current_auditor 5 | Current.user if Current.user&.admin? 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/controllers/concerns/authenticate.rb: -------------------------------------------------------------------------------- 1 | module Authenticate 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | before_action :authenticate 6 | before_action :redirect_if_unauthenticated 7 | end 8 | 9 | class_methods do 10 | def allow_unauthenticated_access(**) 11 | skip_before_action :redirect_if_unauthenticated, ** 12 | end 13 | end 14 | 15 | private 16 | 17 | def authenticate 18 | if (session = User::Session.find_by(token: cookies.signed[:session_token])) 19 | Current.session = session.access 20 | 21 | # handle cookies that weren't initially set as HTTP-only / Secure 22 | cookies.permanent.signed[:session_token] = {value: cookies.signed[:session_token], httponly: true, secure: Rails.env.production?} 23 | end 24 | end 25 | 26 | def redirect_if_unauthenticated 27 | redirect_to sign_in_path unless Current.user 28 | end 29 | 30 | def redirect_if_authenticated 31 | if Current.user&.admin? 32 | redirect_to admin_hackathons_path 33 | elsif Current.user 34 | redirect_to hackathons_submissions_path 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /app/controllers/concerns/hackathon_scoped.rb: -------------------------------------------------------------------------------- 1 | module HackathonScoped 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | before_action :set_hackathon 6 | end 7 | 8 | private 9 | 10 | def set_hackathon 11 | @hackathon = Hackathon.find(params[:hackathon_id]) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/controllers/concerns/read_only_mode.rb: -------------------------------------------------------------------------------- 1 | module ReadOnlyMode 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | rescue_from ReadOnlyModeError do 6 | stream_flash_notice \ 7 | "The app is in read only mode for maintenance. Try again in a few minutes." 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/controllers/concerns/set_current_request_details.rb: -------------------------------------------------------------------------------- 1 | module SetCurrentRequestDetails 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | before_action do 6 | Current.request_id = request.uuid 7 | Current.user_agent = request.user_agent 8 | Current.ip_address = request.remote_ip 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/controllers/concerns/stream_flashes.rb: -------------------------------------------------------------------------------- 1 | module StreamFlashes 2 | private 3 | 4 | def stream_flash_notice(notice = flash.now[:notice]) 5 | flash.now[:notice] = notice 6 | render turbo_stream: turbo_stream.replace("flash", partial: "shared/flash") 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/controllers/concerns/user_scoped.rb: -------------------------------------------------------------------------------- 1 | module UserScoped 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | before_action :set_user 6 | end 7 | 8 | private 9 | 10 | def set_user 11 | @user = User.find(params[:user_id]) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/controllers/database_dumps_controller.rb: -------------------------------------------------------------------------------- 1 | class DatabaseDumpsController < ApplicationController 2 | before_action :set_database_dump, except: %i[index create] 3 | 4 | def index 5 | @database_dumps = DatabaseDump.all 6 | end 7 | 8 | def create 9 | @database_dump = DatabaseDump.create! if Current.user&.admin? 10 | 11 | redirect_to database_dumps_path 12 | end 13 | 14 | def edit 15 | end 16 | 17 | def update 18 | @database_dump.update!(database_dump_params) if Current.user&.admin? 19 | 20 | redirect_to database_dumps_path 21 | end 22 | 23 | def destroy 24 | @database_dump.destroy! if Current.user&.admin? 25 | 26 | redirect_to database_dumps_path 27 | end 28 | 29 | private 30 | 31 | def set_database_dump 32 | @database_dump = DatabaseDump.find(params[:id]) 33 | end 34 | 35 | def database_dump_params 36 | params.require(:database_dump).permit(:name) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /app/controllers/hackathons/submissions_controller.rb: -------------------------------------------------------------------------------- 1 | class Hackathons::SubmissionsController < ApplicationController 2 | allow_unauthenticated_access only: %i[new create] 3 | 4 | def index 5 | @hackathons = Hackathon.not_approved.where applicant: Current.user 6 | 7 | redirect_to new_hackathons_submission_path if @hackathons.none? 8 | end 9 | 10 | def new 11 | @hackathon = Hackathon.new 12 | @hackathon.build_swag_request.build_mailing_address 13 | end 14 | 15 | def create 16 | requested_swag = params[:requested_swag] == "1" 17 | offers_financial_assistance = params[:hackathon][:offers_financial_assistance] == "true" 18 | 19 | @hackathon = Hackathon.new(hackathon_params) 20 | 21 | @hackathon.swag_request = nil unless requested_swag 22 | @hackathon.tag_with! "Offers Financial Assistance" if offers_financial_assistance 23 | 24 | @hackathon.applicant = User.find_or_initialize_by(email_address: applicant_params[:email_address]) do |user| 25 | user.attributes = applicant_params 26 | end 27 | 28 | if @hackathon.save context: [:create, :submit] 29 | redirect_to new_hackathons_submission_path, notice: "Your hackathon has been submitted for approval!" 30 | else 31 | render :new, status: :unprocessable_entity 32 | end 33 | end 34 | 35 | def show 36 | @hackathon = Hackathon.not_approved.where(applicant: Current.user).find(params[:id]) 37 | end 38 | 39 | private 40 | 41 | def hackathon_params 42 | params.require(:hackathon).permit( 43 | :name, 44 | :website, 45 | :logo, 46 | :banner, 47 | :starts_at, 48 | :ends_at, 49 | :modality, 50 | :street, 51 | :city, 52 | :province, 53 | :postal_code, 54 | :country_code, 55 | :expected_attendees, 56 | :high_school_led, 57 | swag_request_attributes: [ 58 | mailing_address_attributes: [ 59 | :line1, 60 | :line2, 61 | :city, 62 | :province, 63 | :postal_code, 64 | :country_code 65 | ] 66 | ] 67 | ) 68 | end 69 | 70 | def applicant_params 71 | params.require(:hackathon).require(:applicant).permit( 72 | :name, 73 | :email_address 74 | ) 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /app/controllers/hackathons/subscriptions/bulk_controller.rb: -------------------------------------------------------------------------------- 1 | class Hackathons::Subscriptions::BulkController < ApplicationController 2 | allow_unauthenticated_access 3 | 4 | before_action :set_user 5 | before_action :set_subscriptions 6 | 7 | def update 8 | @count = @subscriptions.map(&:resubscribe).count(true) 9 | 10 | redirect_to Hackathon::Subscription.manage_subscriptions_url_for(@user), 11 | notice: "Resubscribed to #{@count} #{"locations".pluralize(@count)}." 12 | end 13 | 14 | def destroy 15 | @count = @subscriptions.map(&:unsubscribe).count(true) 16 | 17 | redirect_to Hackathon::Subscription.manage_subscriptions_url_for(@user), 18 | notice: "Unsubscribed from #{@count} #{"locations".pluralize(@count)}." 19 | end 20 | 21 | private 22 | 23 | def set_user 24 | @user = User.find_signed!(params[:user_id], purpose: :manage_subscriptions) 25 | end 26 | 27 | def set_subscriptions 28 | @subscriptions = @user.subscriptions.where(id: params[:ids]) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /app/controllers/hackathons/subscriptions_controller.rb: -------------------------------------------------------------------------------- 1 | class Hackathons::SubscriptionsController < ApplicationController 2 | allow_unauthenticated_access 3 | before_action :set_user 4 | 5 | def index 6 | return if @expired 7 | 8 | @subscriptions = @user.subscriptions.active 9 | end 10 | 11 | def unsubscribe_all 12 | return if @expired 13 | 14 | @subscriptions = @user.subscriptions.active 15 | 16 | @unsubscribe_count = @subscriptions.map(&:unsubscribe).count(true) 17 | end 18 | 19 | private 20 | 21 | def set_user 22 | @user = User.find_signed(params[:user_id], purpose: :manage_subscriptions) 23 | @expired = @user.nil? 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/controllers/hackathons_controller.rb: -------------------------------------------------------------------------------- 1 | class HackathonsController < ApplicationController 2 | allow_unauthenticated_access 3 | 4 | def index 5 | if Current.user&.admin? 6 | redirect_to admin_hackathons_path 7 | else 8 | redirect_to Hackathons::WEBSITE, allow_other_host: true 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/controllers/users/authentications_controller.rb: -------------------------------------------------------------------------------- 1 | class Users::AuthenticationsController < ApplicationController 2 | allow_unauthenticated_access 3 | before_action :redirect_if_authenticated 4 | 5 | def new 6 | end 7 | 8 | def create 9 | @user = User.find_or_create_by(email_address: params[:email_address]) 10 | @user&.authentications&.create! if @user.persisted? 11 | 12 | render :new, status: :unprocessable_entity 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/controllers/users/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class Users::SessionsController < ApplicationController 2 | allow_unauthenticated_access except: :destroy 3 | before_action :redirect_if_authenticated, except: :destroy 4 | 5 | def new 6 | if (auth = User.authenticate(params[:auth_token])) 7 | cookies.permanent.signed[:session_token] = {value: auth.token, httponly: true, secure: Rails.env.production?} 8 | redirect_to hackathons_submissions_path 9 | else 10 | redirect_to sign_in_path, notice: "Invalid or expired, try again!" 11 | end 12 | end 13 | 14 | def destroy 15 | Current.session.destroy! 16 | cookies.delete :session_token 17 | redirect_to root_path 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/errors/api/bad_request_error.rb: -------------------------------------------------------------------------------- 1 | class Api::BadRequestError < Api::Error 2 | def initialize( 3 | title: "Bad Request", 4 | detail: "you've done a whoopsie!", 5 | status: :bad_request, 6 | type: :bad_request_error, 7 | backtrace: nil 8 | ) 9 | super 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/errors/api/error.rb: -------------------------------------------------------------------------------- 1 | class Api::Error < StandardError 2 | attr_reader :title, :detail, :status, :type 3 | 4 | def initialize(title: nil, detail: nil, status: nil, type: nil, backtrace: nil) 5 | @title = title 6 | @detail = detail 7 | @status = status 8 | @type = type 9 | super(title) 10 | 11 | set_backtrace(backtrace) if backtrace 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/errors/api/invalid_api_version_error.rb: -------------------------------------------------------------------------------- 1 | class Api::InvalidApiVersionError < Api::Error 2 | def initialize( 3 | title: "Unsupported API version", 4 | detail: "API version you requested is not supported.", 5 | status: :bad_request, 6 | type: :invalid_api_version_error, 7 | backtrace: nil 8 | ) 9 | super 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/errors/api/not_found_error.rb: -------------------------------------------------------------------------------- 1 | class Api::NotFoundError < Api::Error 2 | def initialize( 3 | title: "Not Found", 4 | detail: "The resource you requested could not be found.", 5 | status: :not_found, 6 | type: :not_found_error, 7 | backtrace: nil 8 | ) 9 | super 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/errors/api/read_only_mode_error.rb: -------------------------------------------------------------------------------- 1 | class Api::ReadOnlyModeError < Api::Error 2 | def initialize( 3 | title: "Read Only Mode", 4 | detail: "The app is in read only mode for maintenance. Try again in a few minutes.", 5 | status: :service_unavailable, 6 | type: :read_only_mode, 7 | backtrace: nil 8 | ) 9 | super 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/errors/read_only_mode_error.rb: -------------------------------------------------------------------------------- 1 | class ReadOnlyModeError < StandardError 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/api_helper.rb: -------------------------------------------------------------------------------- 1 | module ApiHelper 2 | def paginated(json) 3 | json.data do 4 | yield 5 | end 6 | 7 | unless @page.last? 8 | json.links do 9 | json.next url_for(page: @page.next_param, only_path: false) 10 | end 11 | end 12 | 13 | json.meta do 14 | json.total_count @page.recordset.records_count 15 | json.total_pages @page.recordset.page_count 16 | end 17 | end 18 | 19 | # Adds standard attributes to JSON for a record. 20 | def shape_for(record, json) 21 | json.id record.hashid 22 | json.type record.class.try(:api_type) || record.class.name.underscore.parameterize 23 | 24 | yield if block_given? 25 | 26 | json.created_at record.created_at if record.respond_to?(:created_at) 27 | end 28 | 29 | # API URL for an record. By default, the api_version of the generated URL is 30 | # the same as the current request's version. 31 | def api_url_for(record, **) 32 | polymorphic_url([:api, record], api_version: @request_version, **) 33 | rescue NoMethodError 34 | nil 35 | end 36 | 37 | # Permanent URL for an attached file or variant 38 | def file_url_for(attachable, variant = nil) 39 | return nil if attachable.is_a?(ActiveStorage::Attached) && !attachable.attached? 40 | 41 | attachable = attachable.variant(variant) if variant && attachable.variable? 42 | polymorphic_url(attachable) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /app/helpers/mailer_helper.rb: -------------------------------------------------------------------------------- 1 | module MailerHelper 2 | def recipient_name 3 | recipient_emails = 4 | if @_message&.to&.is_a?(String) 5 | # Comma-separated list of email addresses 6 | @_message.to.split(",") 7 | else 8 | # Array of email addresses 9 | @_message&.to || [] 10 | end 11 | 12 | return if recipient_emails.many? # Abort if multiple recipients 13 | 14 | recipient = @recipient || @user || User.find_by_email_address(recipient_emails.first) 15 | recipient&.first_name 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/javascript/application.js: -------------------------------------------------------------------------------- 1 | // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails 2 | import "@hotwired/turbo-rails" 3 | import "controllers" 4 | import "custom_turbo_stream_actions" 5 | 6 | import LocalTime from "local-time" 7 | LocalTime.start() 8 | 9 | document.addEventListener("turbo:morph", () => LocalTime.run()) 10 | -------------------------------------------------------------------------------- /app/javascript/appsignal.js: -------------------------------------------------------------------------------- 1 | import Appsignal from "@appsignal/javascript"; 2 | import {plugin as breadcrumbsConsole} from "@appsignal/plugin-breadcrumbs-console"; 3 | import {plugin as breadcrumbsNetwork} from "@appsignal/plugin-breadcrumbs-network"; 4 | import {plugin as pathDecorator} from "@appsignal/plugin-path-decorator" 5 | 6 | export const appsignal = new Appsignal({ 7 | key: Hackathons.appsignal.key, 8 | }); 9 | 10 | appsignal.use(breadcrumbsConsole()); 11 | appsignal.use(breadcrumbsNetwork()); 12 | appsignal.use(pathDecorator()); 13 | -------------------------------------------------------------------------------- /app/javascript/controllers/application.js: -------------------------------------------------------------------------------- 1 | import { Application } from "@hotwired/stimulus" 2 | 3 | import { appsignal } from "appsignal"; 4 | import { installErrorHandler } from "@appsignal/stimulus"; 5 | 6 | const application = Application.start() 7 | 8 | // Configure Stimulus development experience 9 | application.debug = false 10 | window.Stimulus = application 11 | 12 | installErrorHandler(appsignal, application); 13 | 14 | export { application } 15 | -------------------------------------------------------------------------------- /app/javascript/controllers/element_removal_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | remove() { 5 | this.element.remove() 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/javascript/controllers/form/required_checkboxes_controller.js: -------------------------------------------------------------------------------- 1 | import {Controller} from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | /* Prevent form submission unless required checkboxes are checked. 5 | 6 | This controller may require all or some depending on the `require` value. 7 | - `every`: All checkboxes must be checked 8 | - `some`: At least one checkbox must be checked 9 | 10 | USAGE: 11 | 1. Add the `data-controller` attribute to the form and set the `require` 12 | value to either `every` or `some`. 13 | 2. Place the `button` target on the submit button that should be disabled. 14 | 3. And place the `input` target and `changed` action on every checkbox 15 | that should be checked. 16 | */ 17 | static targets = ["button", "input"] 18 | static values = {require: String} 19 | 20 | connect() { 21 | this.changed() 22 | } 23 | 24 | changed() { 25 | const message = { 26 | every: "Please select all options", 27 | some: "Please select at least one option", 28 | }[this.requireValue] 29 | 30 | let metRequirements = this.inputTargets[this.requireValue]((input) => input.checked) 31 | 32 | this.buttonTarget.disabled = !metRequirements 33 | this.buttonTarget.title = metRequirements ? null : message 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/javascript/controllers/form/visibility_controller.js: -------------------------------------------------------------------------------- 1 | import {Controller} from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | static targets = ["switch", "element"] 5 | static values = {on: Array} 6 | 7 | connect() { 8 | this.toggle() 9 | } 10 | 11 | toggle() { 12 | const elementType = this.switchTarget.type; 13 | 14 | let visible; 15 | switch (elementType) { 16 | case "checkbox": 17 | visible = this.switchTarget.checked; 18 | break; 19 | 20 | default: 21 | visible = this.onValue.includes(this.switchTarget.value); 22 | } 23 | 24 | const update = () => { 25 | this.elementTargets.forEach((element) => element.style.display = visible ? "block" : "none"); 26 | }; 27 | 28 | // Fallback for browsers that don't support View Transitions 29 | if (!document.startViewTransition) { 30 | update(); 31 | return; 32 | } 33 | 34 | document.startViewTransition(() => update()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/javascript/controllers/form_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | connect() { 5 | let inputs = this.enabledVisibleFields(); 6 | if (inputs.length === 1 && inputs[0].type === "text") { 7 | this.focusAtEndOfText(inputs[0]); 8 | } 9 | } 10 | 11 | submitOnClickOutside(event) { 12 | let clickedOutside = true; 13 | for (let input of this.enabledVisibleFields()) { 14 | if (input.contains(event.target)) { 15 | clickedOutside = false; 16 | break; 17 | } 18 | } 19 | 20 | if (clickedOutside) { 21 | this.element.requestSubmit(); 22 | } 23 | } 24 | 25 | submitOnEnter(event) { 26 | if (event.key === "enter") { 27 | event.preventDefault(); 28 | this.element.requestSubmit(); 29 | } 30 | } 31 | 32 | submit(event) { 33 | event.preventDefault(); 34 | this.element.requestSubmit(); 35 | } 36 | 37 | // private 38 | 39 | enabledVisibleFields() { 40 | return this.element.querySelectorAll( 41 | "input:enabled:not([type=hidden]), select:enabled:not([type=hidden])" 42 | ); 43 | } 44 | 45 | focusAtEndOfText(input) { 46 | input.focus(); 47 | input.setSelectionRange(input.value.length, input.value.length); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/javascript/controllers/index.js: -------------------------------------------------------------------------------- 1 | // Import and register all your controllers from the importmap under controllers/* 2 | 3 | import { application } from "controllers/application" 4 | 5 | // Eager load all controllers defined in the import map under controllers/**/*_controller 6 | import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading" 7 | eagerLoadControllersFrom("controllers", application) 8 | 9 | // Lazy load controllers as they appear in the DOM (remember not to preload controllers in import map!) 10 | // import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading" 11 | // lazyLoadControllersFrom("controllers", application) 12 | -------------------------------------------------------------------------------- /app/javascript/controllers/test_readiness_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | connect() { 5 | this.element.setAttribute("data-stimulus-ready", "true") 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/javascript/custom_turbo_stream_actions.js: -------------------------------------------------------------------------------- 1 | window.Turbo.StreamActions.after_unless_duplicate = function () { 2 | if (!this.firstChild?.id || !document.getElementById(this.firstChild.id)) { 3 | const stream = this.cloneNode(true) 4 | stream.setAttribute("action", "after") 5 | this.after(stream) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | discard_on ActiveJob::DeserializationError # most likely a record that's not there anymore 3 | 4 | include RateLimitable 5 | end 6 | -------------------------------------------------------------------------------- /app/jobs/database_dump_job.rb: -------------------------------------------------------------------------------- 1 | class DatabaseDumpJob < ApplicationJob 2 | queue_as :low 3 | 4 | def perform(database_dump) 5 | database_dump.process 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/jobs/hackathons/digests_delivery_job.rb: -------------------------------------------------------------------------------- 1 | class Hackathons::DigestsDeliveryJob < ApplicationJob 2 | def perform 3 | sent_digest_ids = [] 4 | current_subscribers.find_each do |subscriber| 5 | next unless new_digest_pertinent?(subscriber) 6 | 7 | digest = subscriber.digests.new 8 | 9 | digest.save! unless digest.invalid? && digest.listings.none? 10 | sent_digest_ids << digest.id if digest.persisted? 11 | end 12 | ensure 13 | Hackathons::DigestMailer.admin_summary(sent_digest_ids).deliver_later if sent_digest_ids.any? 14 | end 15 | 16 | private 17 | 18 | def current_subscribers 19 | User.includes(:subscriptions).where(subscriptions: {status: :active}).includes(:digests) 20 | end 21 | 22 | def new_digest_pertinent?(subscriber) 23 | subscriber.digests.none? || subscriber.digests.last.created_at.before?(6.days.ago) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/jobs/hackathons/swag_request_delivery_job.rb: -------------------------------------------------------------------------------- 1 | class Hackathons::SwagRequestDeliveryJob < ApplicationJob 2 | def perform(swag_request) 3 | swag_request.deliver_if_pertinent 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/jobs/hackathons/website_archival_job.rb: -------------------------------------------------------------------------------- 1 | class Hackathons::WebsiteArchivalJob < ApplicationJob 2 | rate_limit "Wayback Machine", to: 15, within: 1.minute 3 | queue_as :low 4 | 5 | def perform(hackathon) 6 | if hackathon.eligible_for_archive? 7 | hackathon.archive_website 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/jobs/hackathons/website_archivals_job.rb: -------------------------------------------------------------------------------- 1 | class Hackathons::WebsiteArchivalsJob < ApplicationJob 2 | queue_as :low 3 | 4 | def perform 5 | upcoming_or_recent_hackathons.find_each do |hackathon| 6 | Hackathons::WebsiteArchivalJob.perform_later(hackathon) 7 | end 8 | end 9 | 10 | private 11 | 12 | def upcoming_or_recent_hackathons 13 | Hackathon.approved.where("ends_at >= ?", 1.month.ago) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/jobs/hackathons/website_status_refresh_job.rb: -------------------------------------------------------------------------------- 1 | class Hackathons::WebsiteStatusRefreshJob < ApplicationJob 2 | queue_as :low 3 | 4 | def perform(hackathon) 5 | hackathon.refresh_website_status 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/jobs/hackathons/website_statuses_refresh_job.rb: -------------------------------------------------------------------------------- 1 | class Hackathons::WebsiteStatusesRefreshJob < ApplicationJob 2 | queue_as :low 3 | 4 | def perform 5 | past_hackathons.find_each do |hackathon| 6 | ::Hackathons::WebsiteStatusRefreshJob.perform_later(hackathon) 7 | end 8 | end 9 | 10 | private 11 | 12 | def past_hackathons 13 | Hackathon.approved.where("ends_at < ?", Time.now) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/jobs/loops_synchronization_job.rb: -------------------------------------------------------------------------------- 1 | class LoopsSynchronizationJob < ApplicationJob 2 | rate_limit to: 3, within: 1.second 3 | queue_as :low 4 | 5 | def perform(user) 6 | user.sync_with_loops 7 | rescue Faraday::BadRequestError 8 | Rails.logger.warn "Ignoring a 400 error from Loops when syncing User##{user.id}, most likely because of an invalid email address." 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: "hackathons@hackclub.com" 3 | layout "mailer" 4 | 5 | helper :mailer, :application 6 | 7 | after_action :set_unsubscribe_header 8 | 9 | private 10 | 11 | def set_unsubscribe_urls_for(user) 12 | @unsubscribe_url = Hackathon::Subscription.unsubscribe_all_url_for user 13 | @email_preferences_url = Hackathon::Subscription.manage_subscriptions_url_for user 14 | end 15 | 16 | def set_unsubscribe_header 17 | headers["List-Unsubscribe"] = "<#{@unsubscribe_url}>" if @unsubscribe_url 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/mailers/hackathon_mailer.rb: -------------------------------------------------------------------------------- 1 | class HackathonMailer < ApplicationMailer 2 | before_action :set_hackathon 3 | layout false, only: :swag_request 4 | 5 | def swag_request 6 | @mailing_address = @hackathon.swag_request.mailing_address 7 | mail(to: Rails.application.credentials.swag_requests_email_address || "swag@hackathons.test") 8 | end 9 | 10 | private 11 | 12 | def set_hackathon 13 | @hackathon = params[:hackathon] 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/mailers/hackathons/digest_mailer.rb: -------------------------------------------------------------------------------- 1 | class Hackathons::DigestMailer < ApplicationMailer 2 | def digest(digest) 3 | @digest = digest 4 | @recipient = digest.recipient 5 | 6 | @listings_by_subscription = digest.listings 7 | .includes(:subscription, hackathon: {logo_attachment: :blob}) 8 | .group_by(&:subscription) 9 | 10 | set_unsubscribe_urls_for @recipient 11 | mail to: @recipient.email_address, subject: "Hackathons near you" 12 | end 13 | 14 | def admin_summary(sent_digest_ids) 15 | @sent_digests = Hackathon::Digest.where(id: sent_digest_ids) 16 | 17 | @sent_digests_by_hackathons = @sent_digests 18 | .includes(listings: {hackathon: {logo_attachment: :blob}}) 19 | .flat_map(&:listings).group_by(&:hackathon) 20 | .transform_values { |listings| listings.map(&:digest).uniq } 21 | 22 | @listed_hackathons = @sent_digests_by_hackathons.keys 23 | .sort_by { |hackathon| @sent_digests_by_hackathons[hackathon].count }.reverse! 24 | 25 | subject = <<~SUBJECT.squish 26 | 📬 Hackathons: #{@sent_digests.count} #{"email".pluralize(@sent_digests.count)} 27 | sent for #{@listed_hackathons.count} #{"hackathon".pluralize(@listed_hackathons.count)} 28 | SUBJECT 29 | 30 | mail to: Hackathons::SUPPORT_EMAIL, cc: User.admins.collect(&:email_address), subject: 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /app/mailers/hackathons/submission_mailer.rb: -------------------------------------------------------------------------------- 1 | class Hackathons::SubmissionMailer < ApplicationMailer 2 | before_action :set_hackathon 3 | 4 | def confirmation 5 | mail to: @hackathon.applicant.email_address, subject: "Your hackathon was submitted!" 6 | end 7 | 8 | def admin_notification 9 | email_addresses = User.admins.with_setting_enabled(:new_hackathon_submission_notifications).pluck :email_address 10 | mail to: email_addresses, subject: "A new hackathon named \"#{@hackathon.name}\" was submitted!" 11 | end 12 | 13 | def approval 14 | mail to: @hackathon.applicant.email_address, subject: "Your hackathon submission was approved!" 15 | end 16 | 17 | private 18 | 19 | def set_hackathon 20 | @hackathon = params[:hackathon] 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/mailers/user_mailer.rb: -------------------------------------------------------------------------------- 1 | class UserMailer < ApplicationMailer 2 | self.deliver_later_queue_name = :critical 3 | 4 | before_action :set_user 5 | 6 | def authentication(authentication) 7 | @authentication = authentication 8 | 9 | mail to: @authentication.user.email_address, subject: "Sign in to hackathons.hackclub.com" 10 | end 11 | 12 | private 13 | 14 | def set_user 15 | @user = params&.[](:user) || Current.user 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | primary_abstract_class 3 | 4 | include Hashid::Rails 5 | end 6 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackathons-backend/92a9670ac669d9e3ec3237801f1d8949f6e7e552/app/models/concerns/.keep -------------------------------------------------------------------------------- /app/models/concerns/broadcasting.rb: -------------------------------------------------------------------------------- 1 | module Broadcasting 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | broadcasts_refreshes 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/models/concerns/delivered.rb: -------------------------------------------------------------------------------- 1 | module Delivered 2 | extend ActiveSupport::Concern 3 | extend Suppressible 4 | 5 | class_methods do 6 | def delivered(timing, **) 7 | timing = timing.to_sym 8 | raise ArgumentError, "Invalid timing: #{timing}" unless %i[now later].include?(timing) 9 | self.deliverable_options = {timing: timing, **} 10 | end 11 | end 12 | 13 | included do 14 | class_attribute :deliverable_options, 15 | instance_accessor: false, instance_predicate: false, 16 | default: { 17 | timing: :later 18 | } 19 | 20 | after_create_commit do 21 | case self.class.deliverable_options[:timing] 22 | when :now 23 | delivery.deliver_now 24 | else 25 | delivery.deliver_later 26 | end 27 | end 28 | end 29 | 30 | private 31 | 32 | def deliver_after_creation? 33 | !Delivered.suppressed? 34 | end 35 | 36 | def delivery 37 | raise NotImplementedError 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /app/models/concerns/eventable.rb: -------------------------------------------------------------------------------- 1 | module Eventable 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | has_many :events, as: :eventable, dependent: :destroy 6 | 7 | after_create { record :created } 8 | end 9 | 10 | private 11 | 12 | def record(action, target = nil, by: Current.user, **details) 13 | events.create!(action:, target:, details:, creator: by) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/models/concerns/rate_limitable.rb: -------------------------------------------------------------------------------- 1 | module RateLimitable 2 | extend ActiveSupport::Concern 3 | 4 | class Limit < StandardError 5 | attr_reader :duration 6 | def initialize(duration: nil) 7 | @duration = duration 8 | super 9 | end 10 | end 11 | 12 | class_methods do 13 | def rate_limit(key = name, to:, within:) 14 | around_perform do |job, block| 15 | unless Lock.acquire(key, limit: to, duration: within) { block.call } 16 | raise Limit.new(duration: within) 17 | end 18 | end 19 | end 20 | end 21 | 22 | included do 23 | rescue_from RateLimitable::Limit do |limit| 24 | retry_job wait: limit.duration 25 | logger.info "#{self.class.name} #{job_id} was rate limited for at least #{limit.duration.inspect}" 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /app/models/concerns/suppressible.rb: -------------------------------------------------------------------------------- 1 | module Suppressible 2 | def self.registry 3 | ActiveSupport::IsolatedExecutionState[:suppressible_registry] ||= {} 4 | end 5 | 6 | def suppress 7 | previous_state = Suppressible.registry[name] 8 | Suppressible.registry[name] = true 9 | yield 10 | ensure 11 | Suppressible.registry[name] = previous_state 12 | end 13 | 14 | def suppressed? 15 | !!Suppressible.registry[name] 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/models/concerns/taggable.rb: -------------------------------------------------------------------------------- 1 | module Taggable 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | has_many :taggings, as: :taggable, dependent: :destroy 6 | has_many :tags, through: :taggings 7 | 8 | scope :tagged_with, ->(tags_or_names) { 9 | tags, names = Array(tags_or_names).partition { |tag| tag.is_a? Tag } 10 | tags.concat Tag.where(name: names) 11 | 12 | joins(:taggings).where(taggings: {tag: tags}) 13 | } 14 | end 15 | 16 | def tagged_with?(tags_or_names) 17 | return false unless tags_or_names.present? 18 | Array(tags_or_names).all? do |tag| 19 | if tags.loaded? && tag.is_a?(String) 20 | tags.any? { |t| t.name == tag } 21 | elsif tag.is_a? Tag 22 | tags.include? tag 23 | else 24 | tags.exists? name: tag 25 | end 26 | end 27 | end 28 | 29 | def tag_with(tags_or_names) 30 | new_taggings = Array(tags_or_names).map do |tag| 31 | tag = Tag.find_by(name: tag) unless tag.is_a? Tag 32 | next unless tag 33 | 34 | if new_record? 35 | taggings.find_or_initialize_by tag: tag 36 | else 37 | taggings.find_or_create_by tag: tag 38 | end 39 | end 40 | 41 | return false if new_taggings.include? nil # At least one tag didn't exist 42 | new_taggings 43 | end 44 | 45 | def tag_with!(tags_or_names) 46 | tags = Array(tags_or_names).map do |tag| 47 | next tag if tag.is_a? Tag 48 | Tag.find_or_initialize_by(name: tag) 49 | end 50 | 51 | tag_with tags 52 | end 53 | 54 | def untag(tags_or_names) 55 | Array(tags_or_names).each do |tag| 56 | tag = Tag.find_by name: tag unless tag.is_a? Tag 57 | taggings.destroy_by tag: tag 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /app/models/current.rb: -------------------------------------------------------------------------------- 1 | class Current < ActiveSupport::CurrentAttributes 2 | attribute :request_id, :user_agent, :ip_address 3 | attribute :user, :session 4 | 5 | def session=(session) 6 | super 7 | self.user = session&.user 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/models/database_dump.rb: -------------------------------------------------------------------------------- 1 | class DatabaseDump < ApplicationRecord 2 | TABLES = %w[hackathons] 3 | 4 | include Broadcasting 5 | include Eventable 6 | 7 | include Processed 8 | 9 | def name 10 | time = created_at || Time.now 11 | super.presence || time.strftime("%B %-d, %Y") 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/models/database_dump/processed.rb: -------------------------------------------------------------------------------- 1 | module DatabaseDump::Processed 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | has_one_attached :file 6 | 7 | after_create_commit { DatabaseDumpJob.perform_later(self) } 8 | end 9 | 10 | def processed? 11 | file.attached? 12 | end 13 | 14 | def process 15 | return if processed? 16 | 17 | raise "pg_dump not found" unless `which pg_dump`.present? 18 | 19 | transaction do 20 | Tempfile.create do |io| 21 | dump DatabaseDump::TABLES, to: io.path 22 | 23 | file.attach io: File.open(io), filename: "#{name.delete(",").tr(" ", "-")}.sql" 24 | record :processed 25 | end 26 | end 27 | end 28 | 29 | private 30 | 31 | def dump(tables, to:) 32 | system postgres_env, "pg_dump --table '#{tables.join("|")}' --file #{to}", exception: true 33 | end 34 | 35 | def postgres_env 36 | connection = ApplicationRecord.connection_db_config.configuration_hash 37 | 38 | {}.tap do |env| 39 | env["PGHOST"] = connection[:host] 40 | env["PGPORT"] = connection[:port] 41 | env["PGUSER"] = connection[:username] 42 | env["PGPASSWORD"] = connection[:password] 43 | env["PGDATABASE"] = connection[:database] 44 | end.transform_values!(&:to_s) 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /app/models/event.rb: -------------------------------------------------------------------------------- 1 | class Event < ApplicationRecord 2 | include Requested 3 | 4 | belongs_to :eventable, polymorphic: true, touch: true 5 | 6 | belongs_to :creator, class_name: "User", optional: true 7 | belongs_to :target, class_name: "User", optional: true 8 | 9 | validates :action, presence: true 10 | 11 | def description 12 | [creator&.name, action.humanize(capitalize: false), target&.name].compact.join(" ") 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/models/event/request.rb: -------------------------------------------------------------------------------- 1 | class Event::Request < ApplicationRecord 2 | belongs_to :event 3 | 4 | validates :uuid, :user_agent, :ip_address, presence: true 5 | 6 | before_validation :set_from_current, on: :create 7 | 8 | private 9 | 10 | def set_from_current 11 | self.uuid = Current.request_id 12 | self.user_agent = Current.user_agent 13 | self.ip_address = Current.ip_address 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/models/event/requested.rb: -------------------------------------------------------------------------------- 1 | module Event::Requested 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | has_one :request, dependent: :destroy 6 | 7 | before_create :build_request 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/models/hackathon.rb: -------------------------------------------------------------------------------- 1 | class Hackathon < ApplicationRecord 2 | include Broadcasting 3 | include Eventable 4 | include Taggable 5 | 6 | include Status 7 | 8 | include Applicant 9 | include Branded 10 | include FinanciallyAssisting # depends on Taggable 11 | include Gathering 12 | include Named 13 | include Notifying 14 | include Regional 15 | include Reviewable # depends on Eventable and Status 16 | include Scheduled 17 | include Swag 18 | include Website 19 | end 20 | -------------------------------------------------------------------------------- /app/models/hackathon/applicant.rb: -------------------------------------------------------------------------------- 1 | module Hackathon::Applicant 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | belongs_to :applicant, class_name: "User", default: -> { Current.user } 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/models/hackathon/branded.rb: -------------------------------------------------------------------------------- 1 | module Hackathon::Branded 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | has_one_attached :logo do |logo| 6 | logo.variant :small, resize_to_limit: [128, 128] 7 | end 8 | has_one_attached :banner do |banner| 9 | banner.variant :small, resize_to_limit: [228, 128] 10 | banner.variant :large, resize_to_limit: [1920, 1080] 11 | end 12 | 13 | validates :logo, :banner, processable_file: true, 14 | content_type: {in: ActiveStorage.variable_content_types, message: "must be a valid image format (png, jpeg, webp, etc.)"} 15 | 16 | validates :logo, :banner, on: :submit, attached: true, 17 | size: {less_than: 25.megabytes, message: "is too powerful (max 25 MB)"} 18 | 19 | before_validation :jfif_to_jpeg, if: -> { attachment_changes.any? }, on: :create 20 | end 21 | 22 | private 23 | 24 | def jfif_to_jpeg 25 | logo&.blob&.filename = logo&.blob&.filename&.to_s&.gsub(/\.jfif\z/i, ".jpeg") 26 | banner&.blob&.filename = banner&.blob&.filename&.to_s&.gsub(/\.jfif\z/i, ".jpeg") 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /app/models/hackathon/digest.rb: -------------------------------------------------------------------------------- 1 | class Hackathon::Digest < ApplicationRecord 2 | include Delivered 3 | include Listings 4 | 5 | belongs_to :recipient, class_name: "User" 6 | 7 | private 8 | 9 | def delivery 10 | Hackathons::DigestMailer.digest(self) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/models/hackathon/digest/listing.rb: -------------------------------------------------------------------------------- 1 | class Hackathon::Digest::Listing < ApplicationRecord 2 | belongs_to :digest, class_name: "Hackathon::Digest" 3 | belongs_to :hackathon 4 | belongs_to :subscription 5 | end 6 | -------------------------------------------------------------------------------- /app/models/hackathon/digest/listings.rb: -------------------------------------------------------------------------------- 1 | module Hackathon::Digest::Listings 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | has_many :listings, dependent: :destroy 6 | has_many :listed_hackathons, through: :listings, source: :hackathon 7 | has_many :listed_subscriptions, through: :listings, source: :subscription 8 | 9 | before_validation :build_listings, on: :create, if: -> { listings.none? } 10 | validates_presence_of :listings, on: :create 11 | end 12 | 13 | private 14 | 15 | MAX_LISTINGS = 8 16 | 17 | def build_listings 18 | candidates 19 | .sort_by { |candidate| candidate.hackathon.starts_at } 20 | .first(MAX_LISTINGS).each do |listing| 21 | listings << listing 22 | end 23 | end 24 | 25 | def candidates 26 | super || [] 27 | end 28 | 29 | include ByLocation 30 | include MinimizingPreviouslyListedHackathons 31 | end 32 | -------------------------------------------------------------------------------- /app/models/hackathon/digest/listings/minimizing_previously_listed_hackathons.rb: -------------------------------------------------------------------------------- 1 | module Hackathon::Digest::Listings::MinimizingPreviouslyListedHackathons 2 | private 3 | 4 | def candidates 5 | super.reject do |candidate| 6 | candidate.hackathon.starts_at > 1.month.from_now and 7 | Hackathon::Digest::Listing.exists?(hackathon: candidate.hackathon, digest: recent_digests) 8 | end 9 | end 10 | 11 | def recent_digests 12 | Hackathon::Digest.where(recipient:, created_at: 3.months.ago..) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/models/hackathon/financially_assisting.rb: -------------------------------------------------------------------------------- 1 | module Hackathon::FinanciallyAssisting 2 | def offers_financial_assistance? 3 | tagged_with? "Offers Financial Assistance" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/models/hackathon/gathering.rb: -------------------------------------------------------------------------------- 1 | module Hackathon::Gathering 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | enum :modality, in_person: 0, online: 1, hybrid: 2 6 | 7 | validates :expected_attendees, numericality: {greater_than: 0}, allow_nil: true 8 | validates :expected_attendees, presence: true, on: :submit 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/models/hackathon/named.rb: -------------------------------------------------------------------------------- 1 | module Hackathon::Named 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | validates :name, presence: true 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/models/hackathon/notifying.rb: -------------------------------------------------------------------------------- 1 | module Hackathon::Notifying 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | after_create_commit :deliver_confirmation, :notify_admins, on: :submit 6 | after_update_commit :deliver_approval, if: -> { saved_change_to_status? && approved? } 7 | end 8 | 9 | private 10 | 11 | def deliver_confirmation 12 | Hackathons::SubmissionMailer.with(hackathon: self).confirmation.deliver_later 13 | end 14 | 15 | def notify_admins 16 | Hackathons::SubmissionMailer.with(hackathon: self).admin_notification.deliver_later 17 | end 18 | 19 | def deliver_approval 20 | Hackathons::SubmissionMailer.with(hackathon: self).approval.deliver_later 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/models/hackathon/regional.rb: -------------------------------------------------------------------------------- 1 | module Hackathon::Regional 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | validates :address, presence: true, on: :submit, unless: :online? 6 | 7 | geocoded_by :address do |hackathon, results| 8 | # Geocode to coordinates and standardizes the location attributes 9 | if (result = results.first) 10 | hackathon.attributes = { 11 | latitude: result.latitude, 12 | longitude: result.longitude, 13 | 14 | address: result.address, 15 | street: result.try(:route) || result.try(:street), 16 | city: result.city, 17 | province: result.province || result.state, 18 | postal_code: result.postal_code, 19 | country_code: (ISO3166::Country.from_alpha3_to_alpha2(result.country_code) || result.country_code)&.upcase 20 | } 21 | end 22 | end 23 | before_save :geocode, if: -> { (new_record? || address_changed?) && valid? } 24 | 25 | before_validation :clear_location, if: :online?, on: :submit 26 | end 27 | 28 | def address 29 | super.presence || [street, city, province, postal_code, country_code].compact.presence&.join(", ") 30 | end 31 | 32 | def general_location 33 | [city, province, country_code].compact.join(", ") 34 | end 35 | 36 | def country 37 | ISO3166::Country[country_code]&.common_name 38 | end 39 | 40 | # TODO: This method and the `apac` column is a temporary solution! 41 | # The column contains the migrated `apac` boolean from Airtable. Newly created 42 | # hackathons (after the migration) will have the `apac` column set to `nil`, 43 | # where it uses on the Country gem to determine if the hackathon is in APAC. 44 | # 45 | # See https://github.com/hackclub/hackathons-backend/issues/106 46 | def apac? 47 | return apac unless apac.nil? 48 | 49 | ISO3166::Country[country_code]&.world_region == "APAC" 50 | end 51 | 52 | def to_location 53 | Location.new(city, province, country_code) 54 | end 55 | 56 | private 57 | 58 | def clear_location 59 | self.attributes = { 60 | latitude: nil, 61 | longitude: nil, 62 | 63 | address: nil, 64 | street: nil, 65 | city: nil, 66 | province: nil, 67 | postal_code: nil, 68 | country_code: nil 69 | } 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /app/models/hackathon/reviewable.rb: -------------------------------------------------------------------------------- 1 | module Hackathon::Reviewable 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | scope :reviewed_by, ->(user) do 6 | joins(:events).where(events: {action: REVIEW_ACTIONS, creator: user}) 7 | end 8 | 9 | after_save :record_status, if: :saved_change_to_status? 10 | end 11 | 12 | def reviewers 13 | events.includes(:creator).where(action: REVIEW_ACTIONS).collect(&:creator) 14 | end 15 | 16 | def hold 17 | transaction do 18 | update! status: :pending 19 | record :held_for_review 20 | end 21 | end 22 | 23 | private 24 | 25 | REVIEW_ACTIONS = %i[approved rejected held_for_review] 26 | 27 | def record_status 28 | record status unless pending? 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /app/models/hackathon/scheduled.rb: -------------------------------------------------------------------------------- 1 | module Hackathon::Scheduled 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | scope :upcoming, -> { where("starts_at > ?", Time.now) } 6 | 7 | validates :starts_at, presence: true 8 | validates :ends_at, presence: true 9 | 10 | validate :dates_are_chronological 11 | end 12 | 13 | def start_date 14 | starts_at.to_date 15 | end 16 | 17 | def end_date 18 | ends_at.to_date 19 | end 20 | 21 | private 22 | 23 | def dates_are_chronological 24 | if ends_at < starts_at 25 | errors.add(:ends_at, :non_chronological, message: "must be after the start time") 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /app/models/hackathon/status.rb: -------------------------------------------------------------------------------- 1 | module Hackathon::Status 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | enum :status, pending: 0, approved: 1, rejected: 2 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/models/hackathon/subscription.rb: -------------------------------------------------------------------------------- 1 | class Hackathon::Subscription < ApplicationRecord 2 | include Eventable 3 | 4 | include Status # depends on Eventable 5 | 6 | include Regional # depends on Status 7 | 8 | belongs_to :subscriber, class_name: "User", default: -> { Current.user }, touch: true 9 | has_many :listings, class_name: "Hackathon::Digest::Listing" 10 | end 11 | -------------------------------------------------------------------------------- /app/models/hackathon/subscription/regional.rb: -------------------------------------------------------------------------------- 1 | module Hackathon::Subscription::Regional 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | attribute :location_input 6 | validates :location, presence: {message: ->(object, _) { "'#{object.input_or_location}' could not be found." }} 7 | 8 | geocoded_by :input_or_location do |subscription, results| 9 | # Bias towards US results. This handles cases where the user enters "CA", 10 | # likely intending "California", but geocoding to "Canada". 11 | us = results.first(2).find { |r| r.country_code&.upcase&.starts_with? "US" } 12 | if (result = us || results.first) 13 | subscription.attributes = { 14 | latitude: result.latitude, 15 | longitude: result.longitude, 16 | 17 | city: result.city, 18 | province: result.province || result.state, 19 | country_code: (ISO3166::Country.from_alpha3_to_alpha2(result.country_code) || result.country_code)&.upcase 20 | } 21 | end 22 | end 23 | before_validation :geocode, if: -> { geocoding_needed? } 24 | after_save :record_result, if: -> { geocoding_needed? } 25 | 26 | validate :location_unique_per_subscriber, if: :active? 27 | end 28 | 29 | def location 30 | [city, province, country_code].compact.join(", ") 31 | end 32 | 33 | def input_or_location 34 | location_input || location 35 | end 36 | 37 | def to_location 38 | Location.new(city, province, country_code) 39 | end 40 | 41 | private 42 | 43 | def geocoding_needed? 44 | location_input_changed? || city_changed? || province_changed? || country_code_changed? 45 | end 46 | 47 | def location_unique_per_subscriber 48 | if self.class.active_for(subscriber) 49 | .where(city:, province:, country_code:) 50 | .excluding(self).exists? 51 | errors.add(:base, :already_subscribed, message: "You've already subscribed to this area!") 52 | end 53 | end 54 | 55 | def record_result 56 | if geocoded? 57 | record :geocoded, location_input:, location:, latitude:, longitude: 58 | else 59 | record :geocoding_failed, location_input: location 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /app/models/hackathon/subscription/status.rb: -------------------------------------------------------------------------------- 1 | module Hackathon::Subscription::Status 2 | extend ActiveSupport::Concern 3 | extend Suppressible 4 | 5 | included do 6 | enum :status, inactive: 0, active: 1 7 | after_update :track_changes, unless: -> { self.class::Status.suppressed? } 8 | 9 | scope :active_for, ->(user) { active.where(subscriber: user) } 10 | end 11 | 12 | # Unsubscribe URLs must be valid for at least 30 days. 13 | # https://www.ftc.gov/business-guidance/resources/can-spam-act-compliance-guide-business 14 | UNSUBSCRIBE_EXPIRATION = 2.months 15 | 16 | class_methods do 17 | def manage_subscriptions_url_for(user) 18 | user_signature = user.signed_id purpose: :manage_subscriptions, expires_in: UNSUBSCRIBE_EXPIRATION 19 | Rails.application.routes.url_helpers.user_subscriptions_url(user_signature) 20 | end 21 | 22 | def unsubscribe_all_url_for(user) 23 | user_signature = user.signed_id purpose: :manage_subscriptions, expires_in: UNSUBSCRIBE_EXPIRATION 24 | Rails.application.routes.url_helpers.unsubscribe_all_user_subscriptions_url(user_signature) 25 | end 26 | end 27 | 28 | def unsubscribe 29 | update status: :inactive 30 | end 31 | 32 | def resubscribe 33 | update status: :active 34 | end 35 | 36 | private 37 | 38 | def track_changes 39 | return unless saved_change_to_status? 40 | 41 | record :enabled if active? 42 | record :disabled if inactive? 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /app/models/hackathon/swag.rb: -------------------------------------------------------------------------------- 1 | module Hackathon::Swag 2 | extend ActiveSupport::Concern 3 | 4 | # Hack Club will ship stickers, postcards, and other swag to the mailing 5 | # address provided! yay! 6 | 7 | included do 8 | belongs_to :swag_mailing_address, class_name: "MailingAddress", optional: true 9 | 10 | has_one :swag_request, dependent: :destroy 11 | accepts_nested_attributes_for :swag_request 12 | 13 | after_commit :deliver_swag_request_later_if_pertinent 14 | end 15 | 16 | def requested_swag? 17 | swag_request 18 | end 19 | 20 | private 21 | 22 | SWAG_REQUEST_GRACE_PERIOD = 1.minute 23 | 24 | def deliver_swag_request_later_if_pertinent 25 | swag_request&.deliver_later_if_pertinent(wait: SWAG_REQUEST_GRACE_PERIOD) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/models/hackathon/swag_request.rb: -------------------------------------------------------------------------------- 1 | class Hackathon::SwagRequest < ApplicationRecord 2 | include Delivered 3 | 4 | belongs_to :hackathon 5 | 6 | belongs_to :mailing_address, class_name: "MailingAddress", dependent: :destroy 7 | accepts_nested_attributes_for :mailing_address, update_only: true 8 | end 9 | -------------------------------------------------------------------------------- /app/models/hackathon/swag_request/delivered.rb: -------------------------------------------------------------------------------- 1 | module Hackathon::SwagRequest::Delivered 2 | extend ActiveSupport::Concern 3 | extend Suppressible 4 | 5 | def deliver_later_if_pertinent(wait: nil) 6 | Hackathons::SwagRequestDeliveryJob.set(wait:).perform_later(self) if pertinent? && !Hackathon::SwagRequest::Delivered.suppressed? 7 | end 8 | 9 | def deliver_if_pertinent 10 | HackathonMailer.with(hackathon:).swag_request.deliver if pertinent? && !Hackathon::SwagRequest::Delivered.suppressed? 11 | touch :delivered_at 12 | end 13 | 14 | private 15 | 16 | def pertinent? 17 | hackathon.approved? && !delivered? 18 | end 19 | 20 | def delivered? 21 | delivered_at 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/models/hackathon/website.rb: -------------------------------------------------------------------------------- 1 | module Hackathon::Website 2 | extend ActiveSupport::Concern 3 | 4 | include Archivable 5 | 6 | included do 7 | validates :website, http_url: true 8 | validates :website, presence: true, on: :submit 9 | end 10 | 11 | def website_up? 12 | !website_down? 13 | end 14 | 15 | def website_down? 16 | tagged_with? "Website Down" 17 | end 18 | 19 | def refresh_website_status 20 | website_response&.success? ? untag("Website Down") : tag_with!("Website Down") 21 | end 22 | 23 | def website_likely_unassociated? 24 | !website_likely_associated? 25 | end 26 | 27 | def website_likely_associated? 28 | website_response&.body&.downcase&.include?(name.downcase) 29 | end 30 | 31 | private 32 | 33 | def website_response 34 | @website_response ||= begin 35 | connection = Faraday.new do |f| 36 | f.response :follow_redirects 37 | end 38 | connection.get website 39 | rescue Faraday::Error 40 | nil 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /app/models/hackathon/website/archivable.rb: -------------------------------------------------------------------------------- 1 | module Hackathon::Website::Archivable 2 | def website_or_archive_url 3 | if website_down? && website_archived? 4 | "https://web.archive.org/#{website.sub(/^https?:\/\/(www.)?/, "")}" 5 | else 6 | website 7 | end 8 | end 9 | 10 | def website_archived? 11 | if events.loaded? 12 | events.any? { |e| e.action == "archived_website" } 13 | else 14 | events.where(action: "archived_website").exists? 15 | end 16 | end 17 | 18 | def eligible_for_archive? 19 | website.present? && website_up? && website_likely_associated? 20 | end 21 | 22 | def archive_website 23 | capture = InternetArchive::Capture.new(website_url: website) 24 | request = capture.request 25 | 26 | if request["status"] == "error" 27 | Rails.logger.warn "Internet Archive returned an error capturing #{website}:" 28 | Rails.logger.warn request["message"] 29 | else 30 | FollowUpJob.set(wait: 3.minutes).perform_later(self, capture.job_id) 31 | end 32 | end 33 | 34 | def follow_up_on_archive(job_id) 35 | if archive_with(job_id).finished? 36 | record :website_archived 37 | else 38 | Rails.logger.warn "Internet Archive didn't finish capture for #{website} with job #{job_id}." 39 | end 40 | end 41 | 42 | private 43 | 44 | def archive_with(job_id) 45 | InternetArchive::Capture.new(job_id:) 46 | end 47 | 48 | class FollowUpJob < ApplicationJob 49 | rate_limit "Wayback Machine", to: 15, within: 1.minute 50 | queue_as :low 51 | 52 | def perform(hackathon, id) 53 | hackathon.follow_up_on_archive(id) 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /app/models/lock.rb: -------------------------------------------------------------------------------- 1 | class Lock < ApplicationRecord 2 | scope :expired, -> { where "expiration <= ?", Time.now } 3 | scope :active, -> { expired.invert_where } 4 | 5 | class << self 6 | def acquire(key, limit: 1, duration: nil) 7 | lock = nil 8 | transaction do 9 | lock = active.lock.find_or_create_by!(key:) 10 | 11 | if lock.capacity >= limit 12 | return false 13 | else 14 | lock.acquire 15 | lock.update! expiration: duration&.from_now 16 | end 17 | end 18 | 19 | begin 20 | yield 21 | true 22 | ensure 23 | lock.release 24 | end 25 | end 26 | end 27 | 28 | def acquire(capacity = 1) 29 | increment! :capacity, capacity 30 | end 31 | 32 | def release(quantity = 1) 33 | with_lock do 34 | if capacity <= 1 35 | destroy! 36 | else 37 | decrement! :capacity, quantity 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /app/models/mailing_address.rb: -------------------------------------------------------------------------------- 1 | class MailingAddress < ApplicationRecord 2 | include Eventable 3 | 4 | validates :line1, :city, :country_code, presence: true 5 | validates :country_code, inclusion: {in: ISO3166::Country.codes} 6 | 7 | def to_s 8 | components = [] 9 | components << [line1, line2].compact_blank.join(" ") 10 | components << city << province << postal_code << country_code 11 | 12 | components.compact_blank.join(", ") 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/models/tag.rb: -------------------------------------------------------------------------------- 1 | class Tag < ApplicationRecord 2 | has_many :taggings, dependent: :destroy 3 | has_many :taggables, through: :taggings 4 | 5 | validates :name, presence: true, uniqueness: true 6 | end 7 | -------------------------------------------------------------------------------- /app/models/tagging.rb: -------------------------------------------------------------------------------- 1 | class Tagging < ApplicationRecord 2 | belongs_to :taggable, polymorphic: true 3 | belongs_to :tag 4 | end 5 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ApplicationRecord 2 | include Broadcasting 3 | include Eventable 4 | 5 | include Authenticatable 6 | include Identifiable 7 | include Informed 8 | include Named 9 | include Privileged # depends on Eventable 10 | include Settings 11 | include Subscriber 12 | end 13 | -------------------------------------------------------------------------------- /app/models/user/authenticatable.rb: -------------------------------------------------------------------------------- 1 | module User::Authenticatable 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | has_many :authentications, dependent: :destroy 6 | has_many :sessions, through: :authentications 7 | end 8 | 9 | class_methods do 10 | def authenticate(token) 11 | if (authentication = User::Authentication.find_by(token:)) 12 | if authentication.expired? 13 | authentication.reject reason: :expired 14 | elsif authentication.succeeded? 15 | authentication.reject reason: :previously_succeeded 16 | else 17 | return authentication.create_session! 18 | end 19 | 20 | nil 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/models/user/authentication.rb: -------------------------------------------------------------------------------- 1 | class User::Authentication < ApplicationRecord 2 | include Delivered 3 | delivered :now 4 | include Eventable 5 | 6 | belongs_to :user 7 | 8 | has_secure_token 9 | has_one :session, dependent: :destroy 10 | 11 | def expired? 12 | (created_at + VALIDITY_PERIOD).past? 13 | end 14 | 15 | def succeeded? 16 | events.where(action: :completed).exists? 17 | end 18 | 19 | def completed 20 | record :completed, by: user 21 | end 22 | 23 | def reject(reason: nil) 24 | record :rejected, reason: 25 | end 26 | 27 | private 28 | 29 | VALIDITY_PERIOD = 1.hour 30 | 31 | def delivery 32 | UserMailer.authentication(self) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /app/models/user/identifiable.rb: -------------------------------------------------------------------------------- 1 | module User::Identifiable 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | validates :email_address, presence: true, uniqueness: true, format: {with: URI::MailTo::EMAIL_REGEXP} 6 | encrypts :email_address, downcase: true, deterministic: true 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/models/user/informed.rb: -------------------------------------------------------------------------------- 1 | module User::Informed 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | after_save_commit -> { LoopsSynchronizationJob.perform_later(self) }, if: :sync_with_loops? 6 | end 7 | 8 | def sync_with_loops 9 | if (contact = Loops::Contact.find(email_address)) 10 | contact.subscribedToHackathonsAt = created_at 11 | contact.save 12 | else 13 | Loops::Contact.create( 14 | email: email_address, 15 | firstName: first_name, 16 | lastName: name&.sub!(first_name, "")&.strip, 17 | userGroup: "Hack Clubber", 18 | source: "hackathons.hackclub.com", 19 | subscribedToHackathonsAt: created_at 20 | ) 21 | end 22 | end 23 | 24 | private 25 | 26 | def sync_with_loops? 27 | Rails.env.production? && saved_change_to_email_address? && subscriptions.active.present? 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /app/models/user/named.rb: -------------------------------------------------------------------------------- 1 | module User::Named 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | encrypts :name 6 | end 7 | 8 | def display_name 9 | name.presence || email_address 10 | end 11 | 12 | def first_name 13 | name&.split(" ")&.first 14 | end 15 | 16 | def gravatar_url 17 | hash = Digest::MD5.hexdigest email_address.downcase 18 | "https://www.gravatar.com/avatar/#{hash}" 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/models/user/privileged.rb: -------------------------------------------------------------------------------- 1 | module User::Privileged 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | scope :admins, -> { where admin: true } 6 | 7 | after_save :record_privilege_changes, if: :saved_change_to_admin? 8 | end 9 | 10 | private 11 | 12 | def record_privilege_changes 13 | if admin? 14 | record :promoted_to_admin 15 | else 16 | record :demoted_from_admin 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/models/user/session.rb: -------------------------------------------------------------------------------- 1 | class User::Session < ApplicationRecord 2 | belongs_to :authentication 3 | delegate :user, to: :authentication 4 | 5 | has_secure_token 6 | 7 | after_create -> { authentication.completed } 8 | 9 | def access 10 | touch :last_accessed_at unless ENV["READ_ONLY_MODE"] 11 | self 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/models/user/settings.rb: -------------------------------------------------------------------------------- 1 | class User 2 | DEFAULT_SETTINGS = { 3 | new_hackathon_submission_notifications: true 4 | } 5 | 6 | SETTINGS = User::DEFAULT_SETTINGS.keys 7 | 8 | module Settings 9 | extend ActiveSupport::Concern 10 | 11 | included do 12 | scope :with_setting_enabled, ->(setting) do 13 | where "COALESCE(settings ->> ?, ?) = 'true'", setting.to_s, User::DEFAULT_SETTINGS[setting].to_s 14 | end 15 | end 16 | 17 | User::SETTINGS.each do |setting| 18 | define_method setting do 19 | if settings[setting.to_s].nil? 20 | User::DEFAULT_SETTINGS[setting] 21 | else 22 | settings[setting.to_s] 23 | end 24 | end 25 | 26 | define_method :"#{setting}=" do |value| 27 | settings[setting.to_s] = value 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/models/user/subscriber.rb: -------------------------------------------------------------------------------- 1 | module User::Subscriber 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | has_many :subscriptions, class_name: "Hackathon::Subscription", inverse_of: :subscriber, dependent: :destroy 6 | has_many :digests, class_name: "Hackathon::Digest", inverse_of: :recipient, dependent: :destroy 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/views/admin/_header.html.erb: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /app/views/admin/hackathons/_snippet.html.erb: -------------------------------------------------------------------------------- 1 | <%= link_to admin_hackathon_path(hackathon), class: "no-underline" do %> 2 | <%= turbo_stream_from hackathon %> 3 | 4 |
5 |
6 |

<%= hackathon.name %>

7 | 8 | <%= hackathon.status.humanize %> 9 | 10 | 11 | 12 | 13 | submitted <%= local_time_ago(hackathon.created_at) %> 14 |
15 | 16 |
17 | <% if hackathon.logo.attached? %> 18 | <%= image_tag hackathon.logo.variant(:small), class: "max-w-[3rem] max-h-[3rem] bg-black" %> 19 | <% end %> 20 | 21 |
22 | Starts 23 | <%= time_tag(hackathon.starts_at, format: "%B %d, %Y at %l:%M %p") %> 24 | Ends 25 | <%= time_tag(hackathon.ends_at, format: "%B %d, %Y at %l:%M %p") %> (UTC) 26 |
27 |
28 |
29 | <% end %> 30 | -------------------------------------------------------------------------------- /app/views/admin/hackathons/addresses/edit.html.erb: -------------------------------------------------------------------------------- 1 | 2 | <%= form_with model: @hackathon, url: admin_hackathon_path(@hackathon), data: {controller: "form"} do |form| %> 3 | Address: 4 | <%= form.text_field :address, data: 5 | {action: "click@document->form#submitOnClickOutside keydown->form#submitOnEnter"} %> 6 | <% end %> 7 | 8 | -------------------------------------------------------------------------------- /app/views/admin/hackathons/expected_attendees/edit.html.erb: -------------------------------------------------------------------------------- 1 | 2 | <%= form_with model: @hackathon, url: admin_hackathon_path(@hackathon), 3 | data: {controller: "form", action: "click@document->form#submitOnClickOutside"} do |form| %> 4 | Expected Attendees: 5 | <%= form.number_field :expected_attendees %> 6 | <%= form.select :modality, ["in_person", "online", "hybrid"] %> 7 | <% end %> 8 | 9 | -------------------------------------------------------------------------------- /app/views/admin/hackathons/index.html.erb: -------------------------------------------------------------------------------- 1 | <% page :narrow %> 2 | <% @nav_active_item = admin_hackathons_path %> 3 | 4 |

Hackathons

5 | 6 | <%= turbo_frame_tag "hackathons-#{@page.number}", target: "_top", refresh: :morph do %> 7 | <%= render partial: "snippet", collection: @page.records, as: :hackathon %> 8 | 9 | <% unless @page.last? %> 10 | <%= turbo_stream.action :after_unless_duplicate, "hackathons-#{@page.number}" do %> 11 | <%= turbo_frame_tag "hackathons-#{@page.number + 1}", target: "_top", src: url_for(page: @page.next_param), loading: :lazy, refresh: :morph do %> 12 |
13 | <% end %> 14 | <% end %> 15 | <% end %> 16 | <% end %> 17 | -------------------------------------------------------------------------------- /app/views/admin/hackathons/names/edit.html.erb: -------------------------------------------------------------------------------- 1 | 2 | <%= form_with model: @hackathon, url: admin_hackathon_path(@hackathon), data: {controller: "form"} do |form| %> 3 |

4 | <%= form.text_field :name, required: true, data: 5 | {action: "click@document->form#submitOnClickOutside keydown->form#submitOnEnter"} %> 6 |

7 | <% end %> 8 |
9 | -------------------------------------------------------------------------------- /app/views/admin/hackathons/times/edit.html.erb: -------------------------------------------------------------------------------- 1 | 2 | <%= form_with model: @hackathon, url: admin_hackathon_path(@hackathon), data: {controller: "form"} do |form| %> 3 | Starts:<%= form.datetime_field :starts_at, include_seconds: false, required: true, data: 4 | {action: "click@document->form#submitOnClickOutside keydown->form#submitOnEnter"} %> 5 | ends:<%= form.datetime_field :ends_at, include_seconds: false, required: true, data: 6 | {action: "click@document->form#submitOnClickOutside keydown->form#submitOnEnter"} %> 7 | <% end %> 8 | 9 | -------------------------------------------------------------------------------- /app/views/admin/hackathons/websites/edit.html.erb: -------------------------------------------------------------------------------- 1 | 2 | <%= form_with model: @hackathon, url: admin_hackathon_path(@hackathon), data: {controller: "form"} do |form| %> 3 | Website: 4 | <%= form.text_field :website, data: 5 | {action: "click@document->form#submitOnClickOutside keydown->form#submitOnEnter"} %> 6 | <% end %> 7 | 8 | -------------------------------------------------------------------------------- /app/views/admin/header/_heading.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

3 | <%= link_to "Hackathons Dashboard", admin_hackathons_path, class: "no-underline" %> 4 |

5 |
6 | 7 | Welcome, <%= Current.user.first_name || "admin" %> 8 | 9 | <%= avatar Current.user %> 10 |
11 |
12 | -------------------------------------------------------------------------------- /app/views/admin/header/_nav.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= render "admin/header/nav/link", title: "Hackathons", link: admin_hackathons_path %> 3 | <%= render "admin/header/nav/link", title: "Users", link: admin_users_path %> 4 |
5 | -------------------------------------------------------------------------------- /app/views/admin/header/nav/_link.html.erb: -------------------------------------------------------------------------------- 1 | <% selected = @nav_active_item == link %> 2 | 3 | <%= link_to link, class: "no-underline px-0 py-2 text-(color:--slate) text-xl hover:text-(color:--red) border-b-4 hover:border-b-(color:--red) #{selected ? "border-black" : "border-transparent"}" do %> 4 | <%= title %> 5 | <% end %> 6 | -------------------------------------------------------------------------------- /app/views/admin/users/email_addresses/edit.html.erb: -------------------------------------------------------------------------------- 1 | 2 | <%= form_with model: @user, url: admin_user_path(@user), data: {controller: "form"} do |form| %> 3 |

4 | <%= form.email_field :email_address, required: true, data: 5 | {action: "click@document->form#submitOnClickOutside keydown->form#submitOnEnter"} %> 6 |

7 | <% end %> 8 |
9 | -------------------------------------------------------------------------------- /app/views/admin/users/index.html.erb: -------------------------------------------------------------------------------- 1 | <% page :narrow %> 2 | <% @nav_active_item = admin_users_path %> 3 |

Users

4 | 5 | <%= form_with method: :get, class: "simple_form mt-2" do |form| %> 6 |
7 | <%= form.email_field :email_address, placeholder: "Email address", 8 | required: true, value: @email_address, class: "flex-1" %> 9 |
10 | <% end %> 11 | -------------------------------------------------------------------------------- /app/views/admin/users/names/edit.html.erb: -------------------------------------------------------------------------------- 1 | 2 | <%= form_with model: @user, url: admin_user_path(@user), data: {controller: "form"} do |form| %> 3 |

4 | <%= form.text_field :name, data: 5 | {action: "click@document->form#submitOnClickOutside keydown->form#submitOnEnter"} %> 6 |

7 | <% end %> 8 |
9 | -------------------------------------------------------------------------------- /app/views/admin/users/show.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :title, (@user.display_name) %> 2 | 3 |
4 | <%= turbo_stream_from @user %> 5 | 6 | 7 |

8 | 🪪 9 | <%= @user.display_name %> 10 | <%= link_to "✏️", edit_admin_user_name_path(@user), class: "text-xs no-underline focus:underline decoration-wavy" %> 11 |

12 |
13 | 14 | 15 |

16 | 📧 17 | <%= @user.email_address %> 18 | <%= link_to "✏️", edit_admin_user_email_address_path(@user), class: "text-xs no-underline focus:underline decoration-wavy" %> 19 |

20 |
21 | 22 | 23 | <% if @user.subscriptions.active.any? %> 24 |
25 |

Active Subscriptions

26 |
    27 | <% @user.subscriptions.active.each do |subscription| %> 28 |
  • 29 | <%= subscription.location %> 30 | <%= link_to "⛔", admin_subscription_path(subscription), 31 | class: "no-underline", "data-turbo-method": "delete", 32 | "data-turbo-confirm": "Remove this subscription for #{subscription.location}?" %> 33 |
  • 34 | <% end %> 35 |
36 |
37 | <% end %> 38 |
39 | 40 | <% unless @user == Current.user %> 41 |
42 | <%= button_to "Promote to admin", admin_user_promotion_path(@user), class: "bg-[darkgray]", 43 | data: {"turbo-confirm": "Are you sure you want make this user an admin?"} unless @user.admin? %> 44 | <%= button_to "Demote from admin", admin_user_promotion_path(@user), class: "bg-[crimson]", method: :delete, 45 | data: {"turbo-confirm": "Are you sure you want to demote this user?"} if @user.admin? %> 46 |
47 | <% end %> 48 | 49 | <%= render "events/timeline", {eventable: @user} %> 50 |
51 | -------------------------------------------------------------------------------- /app/views/api/errors/_error.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.type @type 2 | json.title @title 3 | json.detail @detail 4 | json.status @status_code 5 | 6 | if Rails.env.development? 7 | json.development_debug do 8 | json.extract! @error, :inspect 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/views/api/hackathons/_hackathon.json.v1.jbuilder: -------------------------------------------------------------------------------- 1 | shape_for hackathon, json do 2 | json.extract!(hackathon, 3 | :name, 4 | :starts_at, 5 | :ends_at, 6 | :modality) 7 | 8 | json.website hackathon.website_or_archive_url 9 | 10 | json.logo_url file_url_for hackathon.logo, :small 11 | json.banner_url file_url_for hackathon.banner, :large 12 | 13 | json.location do 14 | json.city hackathon.city 15 | json.province hackathon.province 16 | json.country hackathon.country 17 | json.country_code hackathon.country_code 18 | 19 | json.longitude hackathon.longitude 20 | json.latitude hackathon.latitude 21 | end 22 | 23 | # This is temporary! See `Hackathon.apac?` for more info. 24 | json.apac hackathon.apac? 25 | end 26 | -------------------------------------------------------------------------------- /app/views/api/hackathons/index.json.v1.jbuilder: -------------------------------------------------------------------------------- 1 | paginated json do 2 | json.array! @page.records, partial: "hackathon", as: :hackathon 3 | end 4 | -------------------------------------------------------------------------------- /app/views/api/hackathons/show.json.v1.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! @hackathon 2 | -------------------------------------------------------------------------------- /app/views/api/hackathons/subscriptions/_subscription.json.jbuilder: -------------------------------------------------------------------------------- 1 | shape_for subscription, json do 2 | json.status subscription.status 3 | 4 | json.location do 5 | json.extract! subscription, :city, :province, :country_code, :postal_code 6 | end 7 | 8 | json.subscriber do 9 | json.partial! "api/users/user", user: subscription.subscriber 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/views/api/stats/hackathons/index.json.v1.jbuilder: -------------------------------------------------------------------------------- 1 | json.status do 2 | json.pending do 3 | json.meta do 4 | json.count @hackathons.pending.count 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/views/api/stats/hackathons/subscriptions/index.json.v1.jbuilder: -------------------------------------------------------------------------------- 1 | json.cities do 2 | json.meta do 3 | json.count @subscriptions.distinct.count(:city) 4 | end 5 | end 6 | 7 | json.countries do 8 | json.meta do 9 | json.count @subscriptions.distinct.count(:country_code) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/views/api/users/_user.json.jbuilder: -------------------------------------------------------------------------------- 1 | shape_for user, json do 2 | json.extract! user, :email_address 3 | end 4 | -------------------------------------------------------------------------------- /app/views/database_dumps/edit.html.erb: -------------------------------------------------------------------------------- 1 | 2 | <%= form_with model: @database_dump, data: {controller: "form"} do |form| %> 3 | <%= form.text_field :name, data: 4 | {action: "click@document->form#submitOnClickOutside keydown->form#submitOnEnter"} %> 5 | <% end %> 6 | 7 | -------------------------------------------------------------------------------- /app/views/database_dumps/index.html.erb: -------------------------------------------------------------------------------- 1 | <% page :narrow %> 2 | 3 |

Database Dumps

4 |

5 | Baked fresh regularly - contains <%= DatabaseDump::TABLES.join(", ") %> 6 | <%= link_to "🥖", database_dumps_path, title: "Put one in the oven!", data: 7 | {"turbo-method": "post", "turbo-confirm": "Are you sure you want to create a database dump?"} %> 8 |

9 | 10 | <% @database_dumps.each do |database_dump| %> 11 | <%= link_to (database_dump.file.attached? ? database_dump.file : nil), target: "_blank", class: "no-underline" do %> 12 |
13 | <%= turbo_stream_from database_dump %> 14 | 15 | 16 |

17 | <%= database_dump.name %> 18 |

19 |
20 | 21 |

22 | Created <%= local_time_ago(database_dump.created_at) %> 23 | <% unless database_dump.processed? %> 24 | , processing... 25 | <% end %> 26 |

27 |
28 | <% end %> 29 | <% end %> 30 | -------------------------------------------------------------------------------- /app/views/events/_timeline.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <% eventable.events.order(created_at: :asc).includes(:creator, :target).each do |event| %> 3 |
4 |
5 | <% if event.creator %> 6 |
7 | <%= avatar event.creator %> 8 |
9 | <% else %> 10 | <%= ui_avatars event.action, size: "32", length: 1, bold: true, 11 | html: {class: "rounded-full align-middle size-[32px]", alt: "#{event.action} icon"} %> 12 | <% end %> 13 |
14 | 15 |
16 | <%= event.description %> 17 | <%= local_time_ago(event.created_at) %> 18 |
19 |
20 | <% end %> 21 |
22 | -------------------------------------------------------------------------------- /app/views/hackathon_mailer/swag_request.text.erb: -------------------------------------------------------------------------------- 1 | Hey mail team! 👋 2 | 3 | Hackathon swag has been requested for: 4 | 5 | <%= @hackathon.name %><%= " c/o #{@hackathon.applicant.name}" if @hackathon.applicant.name.present? %> 6 | <%= [@mailing_address.line1, @mailing_address.line2].compact_blank.join("\n") %> 7 | <% city, *province_and_postal = [@mailing_address.city, @mailing_address.province, @mailing_address.postal_code].compact_blank %> 8 | <%= [city, province_and_postal.join(" ")].compact_blank.join(", ") %> 9 | <%= @mailing_address.country_code %> 10 | -------------------------------------------------------------------------------- /app/views/hackathons/digest_mailer/_hackathon.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | <% if hackathon.logo.attached? %> 4 | <%= image_tag hackathon.logo.variant(:small), class: "mr-2 w-8 align-middle" %> 5 | <% end %> 6 |

7 | <%= link_to hackathon.name, hackathon.website %> 8 |

9 | <%= local_assigns[:subtitle] %> 10 |
11 | 12 |
13 |

14 | <%= hackathon.start_date.to_fs(:long_ordinal) %> 15 | 16 | (<%= time_ago_in_words hackathon.starts_at %> from now) 17 | 18 |

19 | 20 | <%= yield %> 21 | 22 |

23 | <%= link_to hackathon.website, hackathon.website %> 24 |

25 |
26 |
27 | -------------------------------------------------------------------------------- /app/views/hackathons/digest_mailer/_hackathon.text.erb: -------------------------------------------------------------------------------- 1 | <%= hackathon.name %> 2 | <%= hackathon.start_date.to_fs(:long_ordinal) %> (<%= time_ago_in_words hackathon.starts_at %> from now) 3 | <%= hackathon.website %> 4 | -------------------------------------------------------------------------------- /app/views/hackathons/digest_mailer/_subscription.html.erb: -------------------------------------------------------------------------------- 1 |

2 | 3 | <%= subscription.to_location.city_most_significant? ? "Near" : "In" %> 4 | 5 | <%= subscription.to_location.to_fs(:short) %> 6 |

7 |
8 | <% listings.each do |listing| %> 9 | <%= render "hackathon", hackathon: listing.hackathon %> 10 | <% end %> 11 |
12 | -------------------------------------------------------------------------------- /app/views/hackathons/digest_mailer/_subscription.text.erb: -------------------------------------------------------------------------------- 1 | <%= subscription.to_location.city_most_significant? ? "Near" : "In" %> <%= subscription.to_location.to_fs(:short) %> 2 | 3 | <% listings.each do |listing| %> 4 | <%= render "hackathon", hackathon: listing.hackathon %> 5 | <% end %> 6 | -------------------------------------------------------------------------------- /app/views/hackathons/digest_mailer/admin_summary.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :greeting do %> 2 |

Howdy HCB team 👋

3 | <% end %> 4 | 5 |

6 | <%= @sent_digests.count %> <%= "subscriber".pluralize(@sent_digests.count) %> were notified 7 | for <%= @listed_hackathons.count %> <%= "hackathon".pluralize(@listed_hackathons.count) %>. 8 |

9 | 10 |
11 | <% @listed_hackathons.each do |hackathon| %> 12 | 13 | <% subtitle = capture do %> 14 | 15 | <% count = @sent_digests_by_hackathons[hackathon].count %> 16 | <%= count %> <%= "subscriber".pluralize(count) %> notified 17 | 18 | <% end %> 19 | 20 | <%= render "hackathon", hackathon:, subtitle: do %> 21 |

22 | <%= hackathon.general_location %> 23 |

24 | <% end %> 25 | <% end %> 26 |
27 | 28 | <% content_for :signature do %> 29 |

30 | keep on hackin'! 🦕💸 31 |

32 | <% end %> 33 | -------------------------------------------------------------------------------- /app/views/hackathons/digest_mailer/digest.html.erb: -------------------------------------------------------------------------------- 1 |

2 | Check out 3 | <% if (count = @digest.listings.count) > 1 %> 4 | these <%= count %> hackathons 5 | <% else %> 6 | this hackathon 7 | <% end %> 8 | coming up near you. 9 |

10 | 11 | <% @listings_by_subscription.each do |subscription, listings| %> 12 | <%= render "subscription", subscription:, listings: %> 13 | <% end %> 14 | -------------------------------------------------------------------------------- /app/views/hackathons/digest_mailer/digest.text.erb: -------------------------------------------------------------------------------- 1 | Check out 2 | <%= (count = @digest.listings.count) > 1 ? "these #{count} hackathons" : "this hackathon" %> 3 | coming up near you. 4 | 5 | <% @listings_by_subscription.each do |subscription, listings| %> 6 | <%= render "subscription", subscription:, listings: %> 7 | <% end %> 8 | -------------------------------------------------------------------------------- /app/views/hackathons/index.html.erb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackathons-backend/92a9670ac669d9e3ec3237801f1d8949f6e7e552/app/views/hackathons/index.html.erb -------------------------------------------------------------------------------- /app/views/hackathons/submission_mailer/admin_notification.html.erb: -------------------------------------------------------------------------------- 1 |

2 | A new Hackathon named 3 | <%= link_to @hackathon.name, admin_hackathon_url(@hackathon) %> 4 | was submitted! 5 |

6 | 7 | <% content_for :unsubscribe do %> 8 |

9 | Don't wanna get these emails? <%= link_to "Toggle new submission notifications!", admin_submissions_notifications_url %> 10 |

11 | <% end %> 12 | -------------------------------------------------------------------------------- /app/views/hackathons/submission_mailer/admin_notification.text.erb: -------------------------------------------------------------------------------- 1 | A new Hackathon named "<%= @hackathon.name %>" was submitted! 2 | 3 | View the submission here: <%= admin_hackathon_url(@hackathon) %> 4 | 5 | <% content_for :unsubscribe do %> 6 | Toggle notifications for new submissions here: <%= admin_submissions_notifications_url %> 7 | <% end %> 8 | -------------------------------------------------------------------------------- /app/views/hackathons/submission_mailer/approval.html.erb: -------------------------------------------------------------------------------- 1 |

2 | Your hackathon ("<%= @hackathon.name %>") has been approved! 3 | Check it out at <%= link_to "hackathons.hackclub.com", Hackathons::WEBSITE %>. 4 |

5 | 6 |

7 | Congrats, and best of luck! 8 |

9 | -------------------------------------------------------------------------------- /app/views/hackathons/submission_mailer/approval.text.erb: -------------------------------------------------------------------------------- 1 | Your hackathon ("<%= @hackathon.name %>") has been approved! 2 | Check it out at <%= Hackathons::WEBSITE %>. 3 | 4 | Congrats, and best of luck! 5 | -------------------------------------------------------------------------------- /app/views/hackathons/submission_mailer/confirmation.html.erb: -------------------------------------------------------------------------------- 1 |

2 | Your hackathon ("<%= @hackathon.name %>") has been submitted for review! 3 | We'll reach out if we have any questions. 4 |

5 | -------------------------------------------------------------------------------- /app/views/hackathons/submission_mailer/confirmation.text.erb: -------------------------------------------------------------------------------- 1 | Your hackathon ("<%= @hackathon.name %>") has been submitted for review! 2 | We'll reach out if we have any questions. 3 | -------------------------------------------------------------------------------- /app/views/hackathons/submissions/index.html.erb: -------------------------------------------------------------------------------- 1 |

Your Hackathon Submissions

2 | 3 |
4 | <% @hackathons.each do |hackathon| %> 5 |
6 | <%= link_to hackathon.name, hackathons_submission_path(hackathon) %> 7 | 8 | submitted <%= local_time_ago hackathon.created_at %> 9 |
10 | <% end %> 11 |
12 | 13 | <%= link_to "New submission", new_hackathons_submission_path %> 14 | -------------------------------------------------------------------------------- /app/views/hackathons/submissions/new.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :title, "List your hackathon on hackathons.hackclub.com" %> 2 | <% image = "https://cloud-rmjrb2y89-hack-club-bot.vercel.app/0oy1pj1mznloa96gh1sg-oiontn0tkxelxwjutwobkza.jpeg" %> 3 | 4 | <%= render "shared/full_page_card", background_url: image do %> 5 | <%= image_tag "flag-standalone.png", class: "max-w-1/3 mb-4" %> 6 |

7 | List your hackathon on <%= link_to "hackathons.hackclub.com", Hackathons::WEBSITE, target: "_blank" %> 8 |

9 | 10 |
11 |

12 | If you'd like to list your hackathon on 13 | <%= link_to "hackathons.hackclub.com", Hackathons::WEBSITE, target: "_blank" %>, 14 | please fill out this form! We will get back to you within a couple days. 15 |

16 | 17 |

18 | To be listed on our site, your hackathon must: 19 |

20 |
    21 |
  1. Be high school (or younger) student led
  2. 22 |
  3. Have a custom coded website on an event-specific domain (with open registrations!)
  4. 23 |
24 | 25 |

26 | If this isn’t the case, we won’t be able to include your hackathon. 27 |

28 |
29 | 30 | <%= render "hackathons/submissions/form" %> 31 | <% end %> 32 | -------------------------------------------------------------------------------- /app/views/hackathons/submissions/show.html.erb: -------------------------------------------------------------------------------- 1 |

2 | <%= @hackathon.name %> 3 |

4 | -------------------------------------------------------------------------------- /app/views/hackathons/subscriptions/_manage.html.erb: -------------------------------------------------------------------------------- 1 | <% if subscriptions.present? %> 2 | <%= form_with url: user_subscriptions_bulk_path, method: :delete, 3 | data: { 4 | controller: "form--required-checkboxes", 5 | "form--required-checkboxes-require-value": "some" 6 | } do |form| %> 7 | 8 | <% subscriptions.each do |subscription| %> 9 |
10 | <%= check_box_tag "ids[]", subscription.id, false, data: { 11 | action: "change->form--required-checkboxes#changed", 12 | "form--required-checkboxes-target": "input" 13 | }, id: dom_id(subscription) %> 14 | <%= form.label dom_id(subscription), subscription.location %> 15 |
16 | <% end %> 17 | 18 |
19 | <%= form.button "Unsubscribe", type: "submit", class: "btn mr-2", 20 | data: {"form--required-checkboxes-target": "button"}, disabled: true %> 21 | or 22 | <%= link_to "unsubscribe from all", unsubscribe_all_user_subscriptions_url %> 23 |
24 | <% end %> 25 | 26 | <% else %> 27 |

You are not subscribed to any locations.

28 | <% end %> 29 | -------------------------------------------------------------------------------- /app/views/hackathons/subscriptions/index.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :title, "Manage Subscriptions" %> 2 | 3 |

Manage Subscriptions

4 | 5 | <% if @expired %> 6 |

This link has expired.

7 | <% else %> 8 | 9 |

10 | Hey there, <%= @user.first_name || @user.email_address %>! 11 |
12 | You are currently subscribed to the following locations. 13 |

14 | 15 | <%= render "manage", subscriptions: @subscriptions %> 16 | <% end %> 17 | -------------------------------------------------------------------------------- /app/views/hackathons/subscriptions/unsubscribe_all.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :title, "Unsubscribe" %> 2 | 3 |

Unsubscribe

4 | 5 | <% if @expired %> 6 |

This link has expired.

7 | <% else %> 8 | 9 |

10 | Hey there, <%= @user.first_name || @user.email_address %>! 11 |

12 | 13 |

14 | You have unsubscribed from all emails. 15 | <% unless @unsubscribe_count.zero? %> 16 | You were previous subscribed to 17 | <%= @unsubscribe_count %> <%= "location".pluralize(@unsubscribe_count) %>. 18 | <% end %> 19 | 20 |
21 | Sad to see you go. :( 22 |

23 | 24 | <% unless @unsubscribe_count.zero? %> 25 | <%= form_with url: user_subscriptions_bulk_path, method: :put do |form| %> 26 | <% @subscriptions.ids.each do |id| %> 27 | <%= form.hidden_field "ids[]", value: id %> 28 | <% end %> 29 | 30 | <%= form.button "Undo", class: "cta" %> 31 | <% end %> 32 | <% end %> 33 | <% end %> 34 | -------------------------------------------------------------------------------- /app/views/layouts/admin.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= yield(:title).presence&.+(" — ") %> hackathons.hackclub.com 5 | 6 | <%= csrf_meta_tags %> 7 | <%= csp_meta_tag %> 8 | 9 | <%= stylesheet_link_tag "https://css.hackclub.com/fonts.min.css" %> 10 | <%= stylesheet_link_tag "https://css.hackclub.com/theme.min.css" %> 11 | <%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %> 12 | 13 | 21 | 22 | <% turbo_refreshes_with method: :morph, scroll: :preserve %> 23 | 24 | <%= yield :head %> <%# used by Turbo Drive helpers %> 25 | 26 | <%= javascript_importmap_tags %> 27 | 28 | 29 | 30 | 31 | <%= render "admin/header" %> 32 | <%= render "shared/flash" %> 33 | 34 |
35 | <%= yield %> 36 |
37 | 38 | <%= render "shared/footer" %> 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= yield(:title).presence&.+(" — ") %> hackathons.hackclub.com 5 | 6 | <%= csrf_meta_tags %> 7 | <%= csp_meta_tag %> 8 | 9 | <%= stylesheet_link_tag "https://css.hackclub.com/theme.min.css" %> 10 | <%= stylesheet_link_tag "https://css.hackclub.com/fonts.min.css" %> 11 | <%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %> 12 | 13 | 21 | <%= javascript_importmap_tags %> 22 | 23 | 24 | 25 | <%= render "shared/flash" %> 26 | 27 |
28 | <%= yield %> 29 |
30 | 31 | <%= render "shared/footer" %> 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <% yield_with_default_for :greeting do %> 2 | Hey <%= recipient_name || "there" %>! 3 | <% end %> 4 | 5 | <%= yield %> 6 | 7 | <% yield_with_default_for :signature do %> 8 | Best, 9 | Hack Club 10 | <% end %> 11 | 12 | <% yield_with_default_for :fine_print do %> 13 | hackathons.hackclub.com (<%= Hackathons::WEBSITE %>) is maintained by Hack Club (https://hackclub.com) staff. 14 | <% end %> 15 | 16 | ================================================================================ 17 | 18 | Excited about more hackathons across the globe? Want to list your own? Check out our website (<%= Hackathons::WEBSITE %>)! 19 | 20 | <% yield_with_default_for :unsubscribe do %> 21 | <% if @email_preferences_url && @unsubscribe_url %> 22 | Update your email preferences (<%= @email_preferences_url %>) to choose which emails you receive, or unsubscribe (<%= @unsubscribe_url %>) from all future emails. 23 | <% end %> 24 | <% end %> 25 | 26 | Hack Club, <%= Hackathons::HACK_CLUB_ADDRESS[:full] %>. 27 | -------------------------------------------------------------------------------- /app/views/mailing_addresses/_fields.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= form.label :line1, "Street" %> 3 | <%= form.text_field :line1 %> 4 |
5 | 6 |
7 | <%= form.label :line2, "Apartment, Suite, etc." %> 8 | <%= form.text_field :line2 %> 9 |
10 | 11 |
12 | <%= form.label :city %> 13 | <%= form.text_field :city %> 14 |
15 | 16 |
17 | <%= form.label :province, "State/Province" %> 18 | <%= form.text_field :province %> 19 |
20 | 21 |
22 | <%= form.label :country_code, "Country" %> 23 | <%= form.collection_select :country_code, ISO3166::Country.all, :alpha2, :common_name, selected: "US" %> 24 |
25 | -------------------------------------------------------------------------------- /app/views/shared/_flash.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <% if flash.notice %> 3 |
4 |
5 | <%= flash.notice %> 6 |
7 |
8 | <% end %> 9 |
10 | -------------------------------------------------------------------------------- /app/views/shared/_footer.html.erb: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /app/views/shared/_full_page_card.html.erb: -------------------------------------------------------------------------------- 1 | <% page :full %> 2 | <% content_for :body_style, "background-color: var(--sheet)" %> 3 | 4 |
5 |
6 |
7 |
8 | 9 |
10 | <%= yield %> 11 |
12 |
13 | -------------------------------------------------------------------------------- /app/views/user_mailer/authentication.html.erb: -------------------------------------------------------------------------------- 1 | Here's your <%= link_to "sign in link", new_session_url(auth_token: @authentication.token) %>: 2 | 3 | <%= link_to nil, new_session_url(auth_token: @authentication.token) %> 4 | -------------------------------------------------------------------------------- /app/views/user_mailer/authentication.text.erb: -------------------------------------------------------------------------------- 1 | Here's your sign in link: 2 | 3 | <%= new_session_url(auth_token: @authentication.token) %> 4 | -------------------------------------------------------------------------------- /app/views/users/authentications/new.html.erb: -------------------------------------------------------------------------------- 1 |

Sign In

2 | 3 | <%= turbo_frame_tag :authentication, target: "_top" do %> 4 | <% if @user&.persisted? %> 5 |

6 | An email has been sent to <%= @user.email_address %> with a link to sign in. 7 |

8 | <% if Rails.env.development? %> 9 |

10 | Psst. See the email in <%= link_to "letter opener", letter_opener_web_path %>. 11 |

12 | <% end %> 13 | <% else %> 14 | <%= form_with url: authentication_path do |form| %> 15 | <%= form.text_field :email_address, placeholder: "orpheus@hackclub.test" %> 16 | <%= form.button "Send Sign In Link" %> 17 | <% end %> 18 | <% end %> 19 | <% end %> 20 | -------------------------------------------------------------------------------- /bin/docker-entrypoint: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | if [ "${*}" == "./bin/rails server" ]; then 4 | ./bin/rails db:prepare 5 | fi 6 | 7 | if [ -f tmp/pids/server.pid ]; then 8 | rm tmp/pids/server.pid 9 | fi 10 | 11 | exec "${@}" 12 | -------------------------------------------------------------------------------- /bin/importmap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require_relative "../config/application" 4 | require "importmap/commands" 5 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path("../config/application", __dir__) 3 | require_relative "../config/boot" 4 | require "rails/commands" 5 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative "../config/boot" 3 | require "rake" 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "fileutils" 3 | 4 | # path to your application root. 5 | APP_ROOT = File.expand_path("..", __dir__) 6 | 7 | def system!(*args) 8 | system(*args) || abort("\n== Command #{args} failed ==") 9 | end 10 | 11 | FileUtils.chdir APP_ROOT do 12 | # This script is a way to set up or update your development environment automatically. 13 | # This script is idempotent, so that you can run it at any time and get an expectable outcome. 14 | # Add necessary setup steps to this file. 15 | 16 | puts "== Installing dependencies ==" 17 | system! "gem install bundler --conservative" 18 | system("bundle check") || system!("bundle install") 19 | 20 | # puts "\n== Copying sample files ==" 21 | # unless File.exist?("config/database.yml") 22 | # FileUtils.cp "config/database.yml.sample", "config/database.yml" 23 | # end 24 | 25 | puts "\n== Preparing database ==" 26 | system! "bin/rails db:prepare" 27 | 28 | puts "\n== Removing old logs and tempfiles ==" 29 | system! "bin/rails log:clear tmp:clear" 30 | 31 | puts "\n== Restarting application server ==" 32 | system! "bin/rails restart" 33 | end 34 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | begin 4 | require_relative "config/environment" 5 | rescue Exception => error # standard:disable Lint/RescueException 6 | Appsignal.send_error(error) 7 | raise 8 | end 9 | 10 | run Rails.application 11 | Rails.application.load_server 12 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative "boot" 2 | 3 | require "rails/all" 4 | 5 | # Require the gems listed in Gemfile, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(*Rails.groups) 8 | 9 | module Hackathons 10 | class Application < Rails::Application 11 | config.load_defaults 7.2 12 | 13 | config.autoload_lib ignore: %w[assets tasks templates puma] 14 | 15 | config.mission_control.jobs.http_basic_auth_enabled = false 16 | 17 | config.active_record.encryption.hash_digest_class = OpenSSL::Digest::SHA1 18 | config.active_record.encryption.encrypt_fixtures = true 19 | 20 | config.active_record.automatically_invert_plural_associations = true 21 | 22 | host = ENV["HOST"] || "localhost:3000" 23 | Rails.application.routes.default_url_options[:host] = host 24 | config.action_mailer.default_url_options = {host:} 25 | config.action_mailer.default_options = { 26 | from: "hackathons@hackclub.com" 27 | 28 | # Emails sent to hackathons@hackclub.com are received by 29 | # the Hack Club Bank team at bank@hackclub.com 30 | } 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /config/appsignal.yml: -------------------------------------------------------------------------------- 1 | default: &defaults 2 | push_api_key: "<%= Rails.application.credentials.dig(:appsignal, :push_api_key) %>" 3 | 4 | name: "Hackathons" 5 | 6 | ignore_actions: 7 | - Rails::HealthController#show 8 | 9 | ignore_errors: 10 | - ActionDispatch::Http::MimeNegotiation::InvalidType 11 | - ActionController::BadRequest 12 | - ActionController::InvalidAuthenticityToken 13 | - ActiveRecord::RecordNotFound 14 | 15 | development: 16 | <<: *defaults 17 | active: false 18 | 19 | test: 20 | <<: *defaults 21 | active: false 22 | 23 | production: 24 | <<: *defaults 25 | active: true 26 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 2 | 3 | require "bundler/setup" # Set up gems listed in the Gemfile. 4 | require "bootsnap/setup" # Speed up boot time by caching expensive operations. 5 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: postgresql 3 | 4 | test: 5 | adapter: test 6 | 7 | production: 8 | adapter: postgresql 9 | -------------------------------------------------------------------------------- /config/credentials.yml.enc: -------------------------------------------------------------------------------- 1 | VNO6nXI84Xq/z3hBEOtVM6T9kLLaf1C+Wy5OVxvzRaLeLec0nwm3rn4psgYSLHJz4kBJO2DmVrGXG2GAW9lNgsqUqvKSXmrr9fiZ3VW35zoQarHLy9d07vdQUTY87Oa/U56QiAYLqQtobR75adx1GaJ5DEnSs27FiKlWO28Qun4SEPH/tu2d2JGvq1XdRfufEMo12j7KL4/LXmx4OBcl/8zkgbXkRzsoafk6IObjuG8ANycstaURCwOTBLepqfwTZpsfiiYQFtZXzP27XpLbU5hjJeds/EDFuyoPkJh+Y5h3oZmQ77GnOElmvsJ7qVN5ouUrI4+gmAc5TwqxYaxvvfxWuMoGt/dQOKxoPTHj2RSGiSmoCmw1RjYwGLHYJX0EfE4+GrijAZu1HXGgnZ/HY440BU44PwnMhFXFtD1jWXC869ODH8zwxxaoqK2qwwvHViHQZXmMFWC4/KGcOSw8oyOcQnwxH3FDJ5yluWD0nD1pG0EH9IFiwqEkCa3cuYyfJraygCpku/pFMczvDjuk+mR620TEXl0sY9+9emGvcQy4ZE7SqbcQyv9X/7qgnGYXZOKr8GxxkBJC3C4ZEcndacLR+1AisCAHG7yRNndFlERVMhhjOxhA+0MOKjVWVTWT+fNUSdrjaQJsOWh4fOsc+Deuk5bBq1TZfO6YH9gg3+pKF9IH4DTCKa5OTv3cVLpJS93hCAljL7JPTLP4o5waxhwni8CqezzDPds4SolwFGVDGZ05cBntFdZTgY6m+8wCTIeXPs1ji1YEDzFTduZ2bLXgjE7IrQwbi0kue2QvP/TF326rnPg7r9am5QNG9Jf3QVHQIeLFhLxVkrFTx2uoxhtdNS98Kym19yl4t9No9xhSj3S5i/xlhFKpSuu7xMNJg92ty9vwrVbJHVhOFV1LW3wa0qhplVhS1Tuw4Uhk+K0QyrY8rZ5ssCQSBS9ORtcz8vHeJz+FcLdKAkJ1wBC21OWUYgb8U9QivwmgFko3gVuMKf0u2Nbi7x74mpxqSsQ28iCqMAAxW4oBuubsKCcUHgzjIgqwvHin33c5t2zZyOOpWgBQD4GsyQJJGa0jllAaJCVcYDJJYIiWDVaUsRSLNY+HOpCSrpaFXcOU4+nYgy5IUABGJQdmOE0KTF6fSbpz8tQgMNNnppV8PJvfjA1KC32x1jX0FrBXzQupzEadKk5vZy/WDNXrkJtL6Hw6wnR7QsNZTn2wszZ/HnGSWMUjfOyWk7TqOf394Trq4pKSPaVAp8g0zN3q6b7QABwgBOs44meXB++LVBAAnIANVMrkKOowwjJBeNVBS50/QOm2gKa8X7aPoLilrVrxvUUZgO3VcKN/QEgyJ/J0Mbb6OshjGkVLOUe/qDOZb7Q7/PX1H++Jo5ZQZH6xbj1ARYbKF47u5Him0AkGZnxHDPZvbtlXtru8H3c+UbqwnjehAxxmZgDa2QdOpO9qrkeeInOHuIP3K8Wp40yjl1VruA0eJYS4O8TvSkMw03GLre/+IlbN19ES1yXvUG2b95KmpIBUjiiv/m832mJc7zP+DYVt//sjBkgKdbVsPwP2wuxcLkFX+lyY4X85/FHeOaGx2EzUNNs1Qza7LWzK5GT3kej1xK5NEwdoFdY2BVkpc8xkgHJ+ZXI15yTgkvH5NU1w53+31MExK2W33BWJNEqzoOqQejK/UZBMs+llZcMaj/YjAbNnhmAW3gJbPJykuTy4b4RoIq0dGwIG08qSwH51Rqu92AMUwtnuZ4C0YnBaEfWtSsLZd5xsiub05/tXGRPuTz4Zue1UMqaMFUBrRYfaju9KmpD+6PLTVTRw0094iTalSyjYb+aVy6MSDaIOusPJvP9weIrmOOHUWouC1MofEMAyB5S3MZ3NoRwAL+IwFu00rkg4fQXWE+wNI8XayqDSF0An4sneTUmLinzakmnM1/IhmaAtLdhTRMWGCUlr/V1Chh6dMmWhPR0jAogF+zKSI1qmaW7TfXQ/UE+UbnjWwCVVBLKZ4h7hAlvHUcXq7oK7BiiAUOcUd/IutH23UI4/rfNu6A/Edx2Zp5VDeDb52dkILVIMRK+YWCYPdyREIr7xD5vF8AdY9XENrdP+FHXZc1huFnPY9UTDw12qp6AWL4yDs1V95BXvfyliQlBuzitQiKs=--V6jFSKyVZxbZDLI1--1g8B0/42uOJNEs40Yj7H4A== -------------------------------------------------------------------------------- /config/credentials/development.key: -------------------------------------------------------------------------------- 1 | 3fcc2c1345bd6e6b3db0b1fb8a805192 -------------------------------------------------------------------------------- /config/credentials/development.yml.enc: -------------------------------------------------------------------------------- 1 | RzENvZuY9++cgrC8sN+UKhF/Gb+AV6n+srpMejEDw/dCWYImK6DuKyvT+1qgVNpIL9sNZ2tX+pv6QqbqtTGafUGbjN5sSqkJfUD1Ab/3qcxoFLpmQ+gXiaTQ4OvECI47Tde4qHMEkQ/x5iDkShRr/RIxadT7IbAyAsamGRWdyuJxmy3e7VhsC7JzcqbLGSx7ZikhktqrkKetjlS6/gJkY/e32Y57es3nx/y+j5R/n+tA9jOBWb5Yts49YG9kiKCpaXTRruW9aNNaGq0n9Y8/pOb5LUKsDwHfK+BahuF/XlwW2sEspySXWDFGFa1w50yRlTuhI/KQBT3kj5+tvF83K9EeD6pjlFj9Plhcpy3GRRFpTv/ew5lijYDVskwXU+0qs+JTyaZ3oF/2bBrBG895uhahkoaMrfdAndhX7UyOcRwi8GJMgN/rYDtuemhshPPug0+6vX00Mr2iSdUdBCZXNyBcSO9WXBHaaJbt3xJmdMl4C+XQ/4G9T9bTJwpZo+QbOkPtDJ8E--6gn0xfcjIokb07Py--qLqBv2+e/pNO0/jg4WdImQ== -------------------------------------------------------------------------------- /config/credentials/test.key: -------------------------------------------------------------------------------- 1 | 3fcc2c1345bd6e6b3db0b1fb8a805192 -------------------------------------------------------------------------------- /config/credentials/test.yml.enc: -------------------------------------------------------------------------------- 1 | 2IzHW+vXn18uTHdKfPhNdNc/iOXM4hKyloOHI++huoyEswjOsH4u32CphL+4UgKifnV6mKkx0DcQyb73hIr3crQ318wBvcTaH5CnyUkHv3pHAHdXgJ9He7ume/0v8SjH776/moZ142P/BE8EKz/xWcMsFijlurqrsXU8t39dk/u4duHrDQ8qqY6qbyLlvUBjSsLs48i3CrJUcFizO2ISgH7vgNvD5Wi3V0dBm2SP+iMNlkWOWnXy+RcT7THEFlvbgSxn6MRx8ELWWdKi6xF26ITFcVoVGNzvu0LuDYyjg3fu4kEUlADXlpsSwSvAn7QjZt/FesAVo4eIDrINq9hRqQZKOv/Wjy2Ihy25W3owORhABBbFbnD+2haeB8Vb+XdlUfr9vxBDclxWP6XiQXpL1mOAGzQCdonqJAWtSulYnn1eydKFyIz1RweNWMqT8yfu5w4Arf1gQ4j4aYjsDbrMvUHR6SY/5ti+EQLRCfUzAkV7+Mp03uQDghwEdo1Ltkdca9ictkUfqoY=--ZpQ0enKvcsD1D3dS--mffMIyd1BA06NlZxZ9QFcQ== -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | default: &default 2 | adapter: postgresql 3 | encoding: unicode 4 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 5 | 6 | local: &local 7 | <<: *default 8 | username: hackathons 9 | host: 127.0.0.1 10 | gssencmode: disable # https://github.com/ged/ruby-pg/issues/311#issuecomment-561927000 11 | 12 | development: 13 | <<: *local 14 | database: hackathons_development 15 | 16 | test: 17 | <<: *local 18 | database: hackathons_test 19 | 20 | production: 21 | <<: *default 22 | database: hackathons_production 23 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative "application" 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /config/importmap.rb: -------------------------------------------------------------------------------- 1 | # Pin npm packages by running ./bin/importmap 2 | 3 | pin "application", preload: true 4 | pin "custom_turbo_stream_actions", preload: true 5 | pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true 6 | pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true 7 | pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true 8 | pin_all_from "app/javascript/controllers", under: "controllers" 9 | pin "local-time", to: "https://ga.jspm.io/npm:local-time@2.1.0/app/assets/javascripts/local-time.js" 10 | pin "@appsignal/javascript", to: "https://ga.jspm.io/npm:@appsignal/javascript@1.3.26/dist/esm/index.js" 11 | pin "@appsignal/stimulus", to: "https://ga.jspm.io/npm:@appsignal/stimulus@1.0.17/dist/esm/index.js" 12 | pin "@appsignal/core", to: "https://ga.jspm.io/npm:@appsignal/core@1.1.19/dist/esm/index.js" 13 | pin "https", to: "https://ga.jspm.io/npm:@jspm/core@2.0.1/nodelibs/browser/https.js" 14 | pin "isomorphic-unfetch", to: "https://ga.jspm.io/npm:isomorphic-unfetch@3.1.0/browser.js" 15 | pin "tslib", to: "https://ga.jspm.io/npm:tslib@2.6.1/tslib.es6.mjs" 16 | pin "unfetch", to: "https://ga.jspm.io/npm:unfetch@4.2.0/dist/unfetch.js" 17 | pin "appsignal", preload: true 18 | pin "@appsignal/plugin-breadcrumbs-console", to: "https://ga.jspm.io/npm:@appsignal/plugin-breadcrumbs-console@1.1.27/dist/esm/index.js" 19 | pin "@appsignal/plugin-breadcrumbs-network", to: "https://ga.jspm.io/npm:@appsignal/plugin-breadcrumbs-network@1.1.21/dist/esm/index.js" 20 | pin "@appsignal/plugin-path-decorator", to: "https://ga.jspm.io/npm:@appsignal/plugin-path-decorator@1.0.15/dist/esm/index.js" 21 | -------------------------------------------------------------------------------- /config/initializers/action_mailer_concurrency.rb: -------------------------------------------------------------------------------- 1 | Rails.application.config.after_initialize do 2 | # AWS SES rate-limits to 14/second, so we'll set it to 10 to be safe 3 | ActionMailer::MailDeliveryJob.include(RateLimitable) 4 | .rate_limit to: 10, within: 1.second 5 | end 6 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.configure do 4 | config.assets.version = "1.0" # change to expire all assets 5 | end 6 | -------------------------------------------------------------------------------- /config/initializers/console1984.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | config.console1984.ask_for_username_if_empty = true 3 | config.console1984.incinerate = false 4 | 5 | config.console1984.production_data_warning = <<~WARNING 6 | You have access to production data here. That's a big deal. 7 | To keep sensitive info safe and private, we audit the commands you type here. 8 | WARNING 9 | end 10 | -------------------------------------------------------------------------------- /config/initializers/constants.rb: -------------------------------------------------------------------------------- 1 | module Hackathons 2 | WEBSITE = "https://hackathons.hackclub.com" 3 | SUPPORT_EMAIL = "bank@hackclub.com" 4 | HACK_CLUB_ADDRESS = { 5 | full: "8605 Santa Monica Blvd #86294 West Hollywood, CA 90069", 6 | 7 | street: "8605 Santa Monica Blvd #86294", 8 | city: "West Hollywood", 9 | province: "CA", 10 | zip: "90069" 11 | } 12 | end 13 | -------------------------------------------------------------------------------- /config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide content security policy. 4 | # See the Securing Rails Applications Guide for more information: 5 | # https://guides.rubyonrails.org/security.html#content-security-policy-header 6 | 7 | # Rails.application.configure do 8 | # config.content_security_policy do |policy| 9 | # policy.default_src :self, :https 10 | # policy.font_src :self, :https, :data 11 | # policy.img_src :self, :https, :data 12 | # policy.object_src :none 13 | # policy.script_src :self, :https 14 | # policy.style_src :self, :https 15 | # # Specify URI for violation reports 16 | # # policy.report_uri "/csp-violation-report-endpoint" 17 | # end 18 | # 19 | # # Generate session nonces for permitted importmap and inline scripts 20 | # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } 21 | # config.content_security_policy_nonce_directives = %w(script-src) 22 | # 23 | # # Report violations without enforcing the policy. 24 | # # config.content_security_policy_report_only = true 25 | # end 26 | -------------------------------------------------------------------------------- /config/initializers/cors.rb: -------------------------------------------------------------------------------- 1 | Rails.application.config.middleware.insert_before 0, Rack::Cors do 2 | allow do 3 | origins "*" 4 | resource "/api/*", headers: :any, methods: :any 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure parameters to be filtered from the log file. Use this to limit dissemination of 4 | # sensitive information. See the ActiveSupport::ParameterFilter documentation for supported 5 | # notations and behaviors. 6 | Rails.application.config.filter_parameters += [ 7 | :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn 8 | ] 9 | -------------------------------------------------------------------------------- /config/initializers/geocoder.rb: -------------------------------------------------------------------------------- 1 | if Rails.env.production? 2 | Geocoder.configure( 3 | always_raise: :all, 4 | lookup: :amazon_location_service, 5 | amazon_location_service: { 6 | index_name: "hackathons", 7 | api_key: { 8 | region: Rails.application.credentials.dig(:aws, :region), 9 | access_key_id: Rails.application.credentials.dig(:aws, :access_key_id), 10 | secret_access_key: Rails.application.credentials.dig(:aws, :secret_access_key) 11 | } 12 | } 13 | ) 14 | end 15 | -------------------------------------------------------------------------------- /config/initializers/hashid.rb: -------------------------------------------------------------------------------- 1 | Hashid::Rails.configure do |config| 2 | # The salt to use for generating hashid. Prepended with pepper (table name). 3 | config.salt = Rails.application.credentials.dig(:hashid, :salt) 4 | 5 | # The minimum length of generated hashids 6 | config.min_hash_length = 6 7 | end 8 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, "\\1en" 8 | # inflect.singular /^(ox)en/i, "\\1" 9 | # inflect.irregular "person", "people" 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym "RESTful" 16 | # end 17 | -------------------------------------------------------------------------------- /config/initializers/permissions_policy.rb: -------------------------------------------------------------------------------- 1 | # Define an application-wide HTTP permissions policy. For further 2 | # information see https://developers.google.com/web/updates/2018/06/feature-policy 3 | # 4 | # Rails.application.config.permissions_policy do |f| 5 | # f.camera :none 6 | # f.gyroscope :none 7 | # f.microphone :none 8 | # f.usb :none 9 | # f.fullscreen :self 10 | # f.payment :self, "https://secure.example.com" 11 | # end 12 | -------------------------------------------------------------------------------- /config/initializers/read_only_mode.rb: -------------------------------------------------------------------------------- 1 | module ReadOnly 2 | def readonly?(mode: true) 3 | if mode 4 | super() || ENV["READ_ONLY_MODE"].present? 5 | else 6 | super() 7 | end 8 | end 9 | 10 | def _raise_readonly_record_error 11 | if readonly? mode: false 12 | super 13 | else 14 | raise ReadOnlyModeError 15 | end 16 | end 17 | end 18 | 19 | ActiveRecord::Base.prepend ReadOnly 20 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t "hello" 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t("hello") %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # "true": "foo" 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at https://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | -------------------------------------------------------------------------------- /config/locales/simple_form.en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | simple_form: 3 | "yes": 'Yes' 4 | "no": 'No' 5 | required: 6 | text: 'required' 7 | mark: '*' 8 | # You can uncomment the line below if you need to overwrite the whole required html. 9 | # When using html, text and mark won't be used. 10 | # html: '*' 11 | error_notification: 12 | default_message: "Please review the problems below:" 13 | # Examples 14 | # labels: 15 | # defaults: 16 | # password: 'Password' 17 | # user: 18 | # new: 19 | # email: 'E-mail to sign in.' 20 | # edit: 21 | # email: 'E-mail.' 22 | # hints: 23 | # defaults: 24 | # username: 'User name to sign in.' 25 | # password: 'No special characters, please.' 26 | # include_blanks: 27 | # defaults: 28 | # age: 'Rather not say' 29 | # prompts: 30 | # defaults: 31 | # age: 'Select your age' 32 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | require_relative "environment" 2 | 3 | # Puma can serve each request in a thread from an internal thread pool. 4 | # The `threads` method setting takes two numbers: a minimum and maximum. 5 | # Any libraries that use thread pools should be configured to match 6 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 7 | # and maximum; this matches the default thread size of Active Record. 8 | max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } 9 | min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } 10 | threads min_threads_count, max_threads_count 11 | 12 | # Specifies the threshold that Puma will use to wait before 13 | # terminating a worker in development environments. 14 | worker_timeout 3600 if Rails.env.development? 15 | 16 | port ENV.fetch("PORT") { 3000 } 17 | 18 | environment Rails.env 19 | 20 | pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } 21 | 22 | # Specifies the number of `workers` to boot in clustered mode. 23 | # Workers are forked web server processes. If using threads and workers together 24 | # the concurrency of the application would be max `threads` * `workers`. 25 | # Workers do not work on JRuby or Windows (both of which do not support 26 | # processes). 27 | 28 | # Use the `preload_app!` method when specifying a `workers` number. 29 | # This directive tells Puma to first boot the application and load code 30 | # before forking the application. This takes advantage of Copy On Write 31 | # process behavior so workers use less memory. 32 | web_concurrency = (ENV.fetch("WEB_CONCURRENCY") { 1 }).to_i 33 | if web_concurrency > 1 34 | workers web_concurrency 35 | preload_app! 36 | end 37 | 38 | # Allow puma to be restarted by `bin/rails restart` command. 39 | plugin :tmp_restart 40 | 41 | $LOAD_PATH << File.expand_path("../lib", __dir__) 42 | 43 | plugin :tailwindcss if Rails.env.development? 44 | -------------------------------------------------------------------------------- /config/queue.yml: -------------------------------------------------------------------------------- 1 | default: &default 2 | dispatchers: [{}] # default options 3 | workers: 4 | - queues: 5 | - critical 6 | - default 7 | - low 8 | threads: <%= (ENV.fetch("RAILS_MAX_THREADS") { 5 }).to_i - 2 %> 9 | 10 | development: 11 | <<: *default 12 | 13 | test: 14 | <<: *default 15 | 16 | production: 17 | <<: *default 18 | -------------------------------------------------------------------------------- /config/recurring.yml: -------------------------------------------------------------------------------- 1 | production: 2 | digest_deliveries: 3 | class: Hackathons::DigestsDeliveryJob 4 | schedule: every Tuesday at 10am in America/Los_Angeles 5 | 6 | website_status_refreshes: 7 | class: Hackathons::WebsiteStatusesRefreshJob 8 | schedule: every day at 9am in America/Los_Angeles 9 | 10 | website_archivals: 11 | class: Hackathons::WebsiteArchivalsJob 12 | schedule: every Monday at 10am in America/Los_Angeles 13 | -------------------------------------------------------------------------------- /config/schedule.yml: -------------------------------------------------------------------------------- 1 | lock_maintenance: 2 | cron: "every second" 3 | command: "Lock.expired.delete_all" 4 | 5 | digests_delivery: 6 | cron: "every tuesday at 10am on America/Los_Angeles" 7 | class: "Hackathons::DigestsDeliveryJob" 8 | 9 | website_archivals: 10 | cron: "every monday at 10am on America/Los_Angeles" 11 | class: "Hackathons::WebsiteArchivalsJob" 12 | 13 | website_statuses_refresh: 14 | cron: "every day at 10am on America/Los_Angeles" 15 | class: "Hackathons::WebsiteStatusesRefreshJob" 16 | -------------------------------------------------------------------------------- /config/storage.yml: -------------------------------------------------------------------------------- 1 | test: 2 | service: Disk 3 | root: <%= Rails.root.join("tmp/storage") %> 4 | 5 | test_fixtures: 6 | service: Disk 7 | root: <%= Rails.root.join("tmp/storage_fixtures") %> 8 | 9 | local: 10 | service: Disk 11 | root: <%= Rails.root.join("storage") %> 12 | 13 | amazon: 14 | service: S3 15 | access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> 16 | secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> 17 | region: <%= Rails.application.credentials.dig(:aws, :region) %> 18 | bucket: <%= Rails.application.credentials.dig(:aws, :bucket) %> 19 | 20 | # Remember not to checkin your GCS keyfile to a repository 21 | # google: 22 | # service: GCS 23 | # project: your_project 24 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> 25 | # bucket: your_own_bucket-<%= Rails.env %> 26 | 27 | # Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) 28 | # microsoft: 29 | # service: AzureStorage 30 | # storage_account_name: your_account_name 31 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> 32 | # container: your_container_name-<%= Rails.env %> 33 | 34 | # mirror: 35 | # service: Mirror 36 | # primary: local 37 | # mirrors: [ amazon, google, microsoft ] 38 | -------------------------------------------------------------------------------- /db/migrate/20230710190106_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :users do |t| 4 | t.string :email_address, null: false, index: {unique: true} 5 | t.string :name 6 | t.boolean :admin, default: false, null: false, index: true 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20230711133014_create_events.rb: -------------------------------------------------------------------------------- 1 | class CreateEvents < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :events do |t| 4 | t.belongs_to :eventable, polymorphic: true 5 | 6 | t.references :creator, foreign_key: {to_table: :users} 7 | t.references :target, foreign_key: {to_table: :users} 8 | 9 | t.string :action 10 | t.jsonb :details, default: {}, null: false 11 | 12 | t.timestamps 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/migrate/20230711134548_create_hackathons.rb: -------------------------------------------------------------------------------- 1 | class CreateHackathons < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :hackathons do |t| 4 | t.string :name, null: false 5 | 6 | t.integer :status, default: 0, null: false 7 | 8 | t.datetime :starts_at 9 | t.datetime :ends_at 10 | 11 | t.index [:status, :starts_at, :ends_at] 12 | 13 | t.string :country_code 14 | t.string :province 15 | t.string :city 16 | t.string :postal_code, index: true 17 | t.string :address, index: true 18 | 19 | t.index [:country_code, :province, :city] 20 | t.index [:country_code, :city] 21 | 22 | t.float :latitude 23 | t.float :longitude 24 | 25 | t.index [:latitude, :longitude] 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /db/migrate/20230714152817_create_tags.rb: -------------------------------------------------------------------------------- 1 | class CreateTags < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :tags do |t| 4 | t.string :name, null: false, index: true 5 | t.string :color_hex 6 | 7 | t.timestamps 8 | end 9 | 10 | create_table :taggings do |t| 11 | t.belongs_to :taggable, polymorphic: true, null: false, index: false 12 | t.belongs_to :tag, null: false 13 | 14 | t.index [:taggable_type, :taggable_id, :tag_id], unique: true 15 | 16 | t.timestamps 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /db/migrate/20230717112942_make_hackathon_dates_not_null.rb: -------------------------------------------------------------------------------- 1 | class MakeHackathonDatesNotNull < ActiveRecord::Migration[7.0] 2 | def change 3 | change_column_null :hackathons, :starts_at, false 4 | change_column_null :hackathons, :ends_at, false 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20230717171620_add_street_to_hackathons.rb: -------------------------------------------------------------------------------- 1 | class AddStreetToHackathons < ActiveRecord::Migration[7.0] 2 | def change 3 | add_column :hackathons, :street, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20230718091913_add_timestamps_to_hackathons.rb: -------------------------------------------------------------------------------- 1 | class AddTimestampsToHackathons < ActiveRecord::Migration[7.0] 2 | def change 3 | # If you have existing Hackathons in your database, this migration will fail 4 | # due to the NOT NULL constraint on `created_at` and `updated_at`. 5 | # 6 | # Since we currently don't have any production data, this migration is safe 7 | # as is. To fix this failed migration, destroy all Hackathon records by 8 | # running the following in the Rails console: 9 | # Hackathon.destroy_all 10 | # Then, re-run this migration: 11 | # rails db:migrate 12 | 13 | add_timestamps :hackathons 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/migrate/20230718223502_create_user_sessions.rb: -------------------------------------------------------------------------------- 1 | class CreateUserSessions < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :user_authentications do |t| 4 | t.belongs_to :user, null: false, foreign_key: true 5 | 6 | t.string :token, null: false, index: true 7 | 8 | t.timestamps 9 | end 10 | 11 | create_table :user_sessions do |t| 12 | t.references :authentication, null: false, foreign_key: {to_table: :user_authentications} 13 | 14 | t.string :token, null: false, index: true 15 | 16 | t.timestamp :last_accessed_at 17 | 18 | t.timestamps 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /db/migrate/20230718223503_create_event_requests.rb: -------------------------------------------------------------------------------- 1 | class CreateEventRequests < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :event_requests do |t| 4 | t.references :event, null: false, foreign_key: true 5 | 6 | t.string :uuid 7 | t.string :user_agent 8 | t.string :ip_address 9 | 10 | t.timestamps 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20230718235719_add_applicant_to_hackathons.rb: -------------------------------------------------------------------------------- 1 | class AddApplicantToHackathons < ActiveRecord::Migration[7.0] 2 | def change 3 | add_reference :hackathons, :applicant, null: false, foreign_key: {to_table: :users} 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20230719004620_add_fields_to_hackathons.rb: -------------------------------------------------------------------------------- 1 | class AddFieldsToHackathons < ActiveRecord::Migration[7.0] 2 | def change 3 | add_column :hackathons, :website, :string, null: false 4 | add_column :hackathons, :high_school_led, :boolean, null: false 5 | add_column :hackathons, :expected_attendees, :integer, null: false 6 | add_column :hackathons, :modality, :integer, null: false, default: 0 7 | add_column :hackathons, :financial_assistance, :boolean, null: false 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20230719234350_add_default_true_to_high_school_led_on_hackathons.rb: -------------------------------------------------------------------------------- 1 | class AddDefaultTrueToHighSchoolLedOnHackathons < ActiveRecord::Migration[7.0] 2 | def change 3 | change_column_default :hackathons, :high_school_led, from: nil, to: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20230720000103_create_mailing_addresses.rb: -------------------------------------------------------------------------------- 1 | class CreateMailingAddresses < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :mailing_addresses do |t| 4 | t.string :name 5 | t.string :line1, null: false 6 | t.string :line2 7 | t.string :city, null: false 8 | t.string :province 9 | t.string :postal_code 10 | t.string :country_code, null: false, default: "US" 11 | 12 | t.timestamps 13 | end 14 | 15 | add_reference :hackathons, :swag_mailing_address, foreign_key: {to_table: :mailing_addresses} 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /db/migrate/20230720054548_create_hackathon_subscriptions.rb: -------------------------------------------------------------------------------- 1 | class CreateHackathonSubscriptions < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :hackathon_subscriptions do |t| 4 | t.belongs_to :subscriber, null: false, foreign_key: {to_table: :users} 5 | 6 | t.integer :status, default: 1, null: false 7 | 8 | t.index [:status, :subscriber_id] 9 | 10 | t.string :country_code 11 | t.string :province 12 | t.string :city 13 | t.string :postal_code, index: true 14 | 15 | t.index [:country_code, :province, :city], name: :index_hackathon_subscriptions_on_country_and_province_and_city 16 | t.index [:country_code, :city] 17 | 18 | t.float :latitude 19 | t.float :longitude 20 | 21 | t.index [:latitude, :longitude] 22 | 23 | t.timestamps 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /db/migrate/20230720205641_create_hackathon_digests.rb: -------------------------------------------------------------------------------- 1 | class CreateHackathonDigests < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :hackathon_digests do |t| 4 | t.belongs_to :recipient, null: false, foreign_key: {to_table: :users} 5 | 6 | t.timestamps 7 | end 8 | 9 | create_table :hackathon_digest_listings do |t| 10 | t.belongs_to :digest, null: false, foreign_key: {to_table: :hackathon_digests} 11 | t.references :hackathon, null: false, foreign_key: true 12 | 13 | t.timestamps 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /db/migrate/20230726064503_add_belongs_to_subscription_on_hackathon_digest_lisings.rb: -------------------------------------------------------------------------------- 1 | class AddBelongsToSubscriptionOnHackathonDigestLisings < ActiveRecord::Migration[7.0] 2 | def change 3 | add_belongs_to :hackathon_digest_listings, :subscription, null: false, foreign_key: {to_table: :hackathon_subscriptions} 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20230726225359_drop_financial_assistance_on_hackathons.rb: -------------------------------------------------------------------------------- 1 | class DropFinancialAssistanceOnHackathons < ActiveRecord::Migration[7.0] 2 | def change 3 | remove_column :hackathons, :financial_assistance 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20230728125411_allow_null_on_hackathon_expected_attendees.rb: -------------------------------------------------------------------------------- 1 | class AllowNullOnHackathonExpectedAttendees < ActiveRecord::Migration[7.0] 2 | def change 3 | change_column_null :hackathons, :expected_attendees, true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20230811051252_make_website_optional_on_hackathons.rb: -------------------------------------------------------------------------------- 1 | class MakeWebsiteOptionalOnHackathons < ActiveRecord::Migration[7.0] 2 | def change 3 | change_column_null :hackathons, :website, true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20230814030407_add_apac_to_hackathon.rb: -------------------------------------------------------------------------------- 1 | class AddApacToHackathon < ActiveRecord::Migration[7.0] 2 | def change 3 | add_column :hackathons, :apac, :boolean 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20230815050917_add_airtable_id_to_hackathons.rb: -------------------------------------------------------------------------------- 1 | class AddAirtableIdToHackathons < ActiveRecord::Migration[7.1] 2 | def change 3 | add_column :hackathons, :airtable_id, :string 4 | add_index :hackathons, :airtable_id, unique: true 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20230815052624_add_airtable_id_to_hackathon_subscriptions.rb: -------------------------------------------------------------------------------- 1 | class AddAirtableIdToHackathonSubscriptions < ActiveRecord::Migration[7.1] 2 | def change 3 | add_column :hackathon_subscriptions, :airtable_id, :string 4 | add_index :hackathon_subscriptions, :airtable_id, unique: true 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20230815053636_make_auth_tokens_unique.rb: -------------------------------------------------------------------------------- 1 | class MakeAuthTokensUnique < ActiveRecord::Migration[7.0] 2 | def change 3 | remove_index :user_authentications, :token 4 | add_index :user_authentications, :token, unique: true 5 | remove_index :user_sessions, :token 6 | add_index :user_sessions, :token, unique: true 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20230815191654_create_console1984_tables.console1984.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from console1984 (originally 20210517203931) 2 | class CreateConsole1984Tables < ActiveRecord::Migration[7.0] 3 | def change 4 | create_table :console1984_sessions do |t| 5 | t.text :reason 6 | t.references :user, null: false, index: false 7 | t.timestamps 8 | 9 | t.index :created_at 10 | t.index [:user_id, :created_at] 11 | end 12 | 13 | create_table :console1984_users do |t| 14 | t.string :username, null: false 15 | t.timestamps 16 | 17 | t.index [:username] 18 | end 19 | 20 | create_table :console1984_commands do |t| 21 | t.text :statements 22 | t.references :sensitive_access 23 | t.references :session, null: false, index: false 24 | t.timestamps 25 | 26 | t.index [:session_id, :created_at, :sensitive_access_id], name: "on_session_and_sensitive_chronologically" 27 | end 28 | 29 | create_table :console1984_sensitive_accesses do |t| 30 | t.text :justification 31 | t.references :session, null: false 32 | 33 | t.timestamps 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /db/migrate/20230815205623_create_auditing_tables.audits1984.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from audits1984 (originally 20210810092639) 2 | class CreateAuditingTables < ActiveRecord::Migration[7.0] 3 | def change 4 | create_table :audits1984_audits do |t| 5 | t.integer :status, default: 0, null: false 6 | t.text :notes 7 | t.references :session, null: false 8 | t.references :auditor, null: false 9 | 10 | t.timestamps 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20230818183712_index_created_at_for_hackathon_digests.rb: -------------------------------------------------------------------------------- 1 | class IndexCreatedAtForHackathonDigests < ActiveRecord::Migration[7.1] 2 | def change 3 | add_index :hackathon_digests, :created_at 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20231204215503_change_jfif_files_to_jpeg.rb: -------------------------------------------------------------------------------- 1 | class ChangeJfifFilesToJpeg < ActiveRecord::Migration[7.2] 2 | def change 3 | reversible do |dir| 4 | dir.up do 5 | ActiveStorage::Blob.where("filename ILIKE ?", "%.jfif").find_each do |blob| 6 | blob.update!(filename: blob.filename.to_s.gsub(/\.jfif/i, ".jpeg")) 7 | end 8 | end 9 | 10 | dir.down do 11 | ActiveStorage::Blob.where("filename ILIKE ?", "%.jpeg").find_each do |blob| 12 | blob.update!(filename: blob.filename.to_s.gsub(/\.jpeg/i, ".jfif")) 13 | end 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /db/migrate/20231223215503_add_settings_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddSettingsToUsers < ActiveRecord::Migration[7.2] 2 | def change 3 | add_column :users, :settings, :jsonb, default: {}, null: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20240107231455_create_database_dumps.rb: -------------------------------------------------------------------------------- 1 | class CreateDatabaseDumps < ActiveRecord::Migration[7.2] 2 | def change 3 | create_table :database_dumps do |t| 4 | t.string :name 5 | 6 | t.timestamps 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20240206140353_create_swag_requests.rb: -------------------------------------------------------------------------------- 1 | class CreateSwagRequests < ActiveRecord::Migration[7.2] 2 | def change 3 | create_table :hackathon_swag_requests do |t| 4 | t.references :hackathon, null: false, foreign_key: true 5 | t.references :mailing_address, null: false, foreign_key: true 6 | 7 | t.timestamp :delivered_at 8 | 9 | t.timestamps 10 | end 11 | 12 | Hackathon.where.associated(:swag_mailing_address).includes(:swag_mailing_address).find_each do |hackathon| 13 | Hackathon::SwagRequest::Delivered.suppress do 14 | hackathon.create_swag_request!(mailing_address: hackathon.swag_mailing_address) 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /db/migrate/20240426144308_add_contacts_to_loops.rb: -------------------------------------------------------------------------------- 1 | class AddContactsToLoops < ActiveRecord::Migration[7.2] 2 | def change 3 | return unless Rails.env.production? 4 | 5 | User.includes(:subscriptions).where(subscriptions: {status: :active}).find_each.with_index do |user, index| 6 | LoopsSynchronizationJob.set(wait: 3.seconds * index).perform_later(user) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20240704165824_jsonb_to_json.rb: -------------------------------------------------------------------------------- 1 | class JsonbToJson < ActiveRecord::Migration[8.0] 2 | def change 3 | change_column :events, :details, :json 4 | change_column :users, :settings, :json 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20250103164545_create_locks.rb: -------------------------------------------------------------------------------- 1 | class CreateLocks < ActiveRecord::Migration[8.1] 2 | def change 3 | create_table :locks do |t| 4 | t.string :key, null: false, index: {unique: true} 5 | t.integer :capacity, null: false, default: 0 6 | t.datetime :expiration, index: true 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20250128210851_remove_unique_constraint_on_locks.rb: -------------------------------------------------------------------------------- 1 | class RemoveUniqueConstraintOnLocks < ActiveRecord::Migration[8.1] 2 | def change 3 | remove_index :locks, :key, unique: true 4 | add_index :locks, :key, unique: false 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20250220140522_alpha3_country_codes_to_alpha2.rb: -------------------------------------------------------------------------------- 1 | class Alpha3CountryCodesToAlpha2 < ActiveRecord::Migration[8.1] 2 | def up 3 | Hackathon.where("LENGTH(country_code) = 3").find_each do 4 | it.update_attribute! :country_code, ISO3166::Country.from_alpha3_to_alpha2(it.country_code) 5 | end 6 | 7 | Hackathon::Subscription.where("LENGTH(country_code) = 3").find_each do 8 | it.update_attribute! :country_code, ISO3166::Country.from_alpha3_to_alpha2(it.country_code) 9 | end 10 | end 11 | 12 | def down 13 | raise ActiveRecord::IrreversibleMigration 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # movies = Movie.create([{ name: "Star Wars" }, { name: "Lord of the Rings" }]) 7 | # Character.create(name: "Luke", movie: movies.first) 8 | -------------------------------------------------------------------------------- /docker-compose.production.yml: -------------------------------------------------------------------------------- 1 | services: 2 | web: 3 | build: . 4 | deploy: 5 | resources: 6 | limits: 7 | cpus: 1 8 | memory: 2G 9 | environment: 10 | WEB_CONCURRENCY: 2 11 | healthcheck: 12 | test: ["CMD", "curl", "-f", "http://localhost:3000/up"] 13 | volumes: ["storage:/hackathons/storage"] 14 | jobs: 15 | volumes: ["storage:/hackathons/storage"] 16 | build: . 17 | depends_on: [web] 18 | command: bin/rails solid_queue:start 19 | deploy: 20 | resources: 21 | limits: 22 | cpus: 1 23 | memory: 1G 24 | 25 | volumes: 26 | storage: {} 27 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | postgres: 3 | image: postgres:17.4 4 | ports: 5 | - ${POSTGRES_PORT-:5432}:5432 6 | environment: 7 | POSTGRES_USER: hackathons 8 | POSTGRES_DB: hackathons_test 9 | POSTGRES_HOST_AUTH_METHOD: trust 10 | volumes: 11 | - postgres-data:/var/lib/postgresql/data 12 | 13 | volumes: 14 | postgres-data: {} 15 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackathons-backend/92a9670ac669d9e3ec3237801f1d8949f6e7e552/lib/assets/.keep -------------------------------------------------------------------------------- /lib/constraints/admin.rb: -------------------------------------------------------------------------------- 1 | module Constraints 2 | class Admin 3 | def self.matches?(request) 4 | cookies = ActionDispatch::Cookies::CookieJar.build(request, request.cookies) 5 | session = User::Session.find_by(token: cookies.permanent.signed[:session_token]) 6 | 7 | session&.user&.admin? 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/http_url_validator.rb: -------------------------------------------------------------------------------- 1 | class HttpUrlValidator < ActiveModel::EachValidator 2 | def validate_each(record, attribute, value) 3 | return if value.blank? 4 | 5 | uri = URI.parse(value) 6 | 7 | unless uri.is_a?(URI::HTTP) && uri.host.present? 8 | raise URI::InvalidURIError 9 | end 10 | rescue URI::InvalidURIError 11 | record.errors.add attribute, "is not a valid URL" 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/internet_archive.rb: -------------------------------------------------------------------------------- 1 | module InternetArchive 2 | end 3 | -------------------------------------------------------------------------------- /lib/internet_archive/capture.rb: -------------------------------------------------------------------------------- 1 | # Uses the Save Page Now 2 API 2 | # (https://docs.google.com/document/d/1Nsv52MvSjbLb2PCpHlat0gkzw0EvtSgpKHu4mk0MnrA) 3 | class InternetArchive::Capture 4 | attr_reader :website_url, :job_id 5 | 6 | def initialize(website_url: nil, job_id: nil) 7 | @website_url = website_url 8 | @job_id = job_id 9 | end 10 | 11 | def request 12 | response = connection.post("save", "url=#{website_url}").body 13 | @job_id = response["job_id"] 14 | response 15 | end 16 | 17 | def finished? 18 | status == "success" 19 | end 20 | 21 | private 22 | 23 | BASE_URL = "https://web.archive.org/" 24 | 25 | def status 26 | connection.get("save/status/#{job_id}").body&.dig("status") 27 | end 28 | 29 | def connection 30 | @connection ||= Faraday.new(BASE_URL, headers:) do |faraday| 31 | faraday.response :json 32 | faraday.response :raise_error 33 | end 34 | end 35 | 36 | def headers 37 | { 38 | Accept: "application/json", 39 | Authorization: "LOW #{access_key}:#{access_secret}" 40 | } 41 | end 42 | 43 | def access_key 44 | Rails.application.credentials.dig(:internet_archive, :access_key) 45 | end 46 | 47 | def access_secret 48 | Rails.application.credentials.dig(:internet_archive, :access_secret) 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/loops.rb: -------------------------------------------------------------------------------- 1 | module Loops 2 | end 3 | -------------------------------------------------------------------------------- /lib/loops/contact.rb: -------------------------------------------------------------------------------- 1 | module Loops 2 | class Contact < Resource 3 | ENDPOINT = "contacts/" 4 | 5 | class << self 6 | def find(email_address) 7 | response = connection.get(ENDPOINT + "find", email: email_address) 8 | 9 | if response.body.present? 10 | new(**response.body.first) 11 | end 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/loops/resource.rb: -------------------------------------------------------------------------------- 1 | class Loops::Resource 2 | BASE_URL = "https://app.loops.so/api/v1/" 3 | ENDPOINT = "" 4 | 5 | class << self 6 | def create(**) 7 | new(**).create 8 | end 9 | 10 | def connection 11 | @connection ||= Faraday.new(BASE_URL) do |faraday| 12 | faraday.request :authorization, "Bearer", Rails.application.credentials.loops_api_key 13 | faraday.request :json 14 | faraday.response :json 15 | faraday.response :raise_error 16 | end 17 | end 18 | end 19 | 20 | def initialize(**attributes) 21 | @attributes = attributes 22 | end 23 | 24 | def create 25 | self.class.connection.post(self.class::ENDPOINT + "create", attributes) 26 | end 27 | 28 | def save 29 | self.class.connection.put(self.class::ENDPOINT + "update", attributes) 30 | end 31 | 32 | private 33 | 34 | attr_reader :attributes 35 | 36 | def respond_to_missing?(_, _) 37 | true 38 | end 39 | 40 | def method_missing(name, *args) 41 | if name.to_s.end_with?("=") 42 | attributes[name.to_s.delete("=").to_sym] = args.first 43 | else 44 | attributes[name.to_sym] 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/puma/plugin/dartsass.rb: -------------------------------------------------------------------------------- 1 | require "puma/plugin" 2 | require "dartsass/runner" 3 | require "active_support/all" 4 | 5 | Puma::Plugin.create do 6 | def start(launcher) 7 | @log_writer = launcher.log_writer 8 | @puma_pid = Process.pid 9 | @dartsass_pid = fork do 10 | Thread.new { monitor_puma } 11 | 12 | # If we use system(*command), IRB and Debug can't read from $stdin 13 | # correctly because some keystrokes will be sent to dartsass. 14 | IO.popen(Dartsass::Runner.dartsass_compile_command << "--watch", "r+") do |io| 15 | IO.copy_stream io, $stdout 16 | end 17 | end 18 | 19 | launcher.events.on_stopped { stop_dartsass } 20 | end 21 | 22 | private 23 | 24 | attr_reader :puma_pid, :dartsass_pid 25 | 26 | def monitor_puma 27 | stop_when :puma_dead?, "Puma is gone, stopping dartsass..." 28 | end 29 | 30 | def stop_when(condition_met, message) 31 | loop do 32 | if send(condition_met) 33 | log message 34 | Process.kill(:INT, Process.pid) 35 | break 36 | else 37 | sleep 2.seconds 38 | end 39 | end 40 | end 41 | 42 | def puma_dead? 43 | Process.ppid != puma_pid 44 | end 45 | 46 | def stop_dartsass 47 | suppress Errno::ECHILD, Errno::ESRCH do 48 | Process.wait(dartsass_pid, Process::WNOHANG) 49 | log "Stopping dartsass..." 50 | Process.kill(:INT, dartsass_pid) 51 | Process.wait(dartsass_pid) 52 | end 53 | end 54 | 55 | def log(...) 56 | @log_writer.log(...) 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/tasks/hackathons.rake: -------------------------------------------------------------------------------- 1 | namespace :hackathons do 2 | desc "Archive all hackathon websites" 3 | task archive_all_websites: :environment do 4 | # I recommend running this task using: 5 | # ```sh 6 | # rake to_stdout hackathons:archive_all_websites 7 | # ``` 8 | # In order to see the output logs 9 | Hackathon.find_each do |hackathon| 10 | next if hackathon.events.where(action: "archived_website").last&.created_at&.after? 1.day.ago 11 | Hackathons::WebsiteArchivalJob.perform_now(hackathon) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/tasks/to_stdout.rake: -------------------------------------------------------------------------------- 1 | # https://stackoverflow.com/questions/2246141/puts-vs-logger-in-rails-rake-tasks 2 | desc "switch logger to stdout" 3 | task to_stdout: [:environment] do 4 | Rails.logger = Logger.new($stdout) 5 | end 6 | -------------------------------------------------------------------------------- /lib/templates/erb/scaffold/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%# frozen_string_literal: true %> 2 | <%%= simple_form_for(@<%= singular_table_name %>) do |f| %> 3 | <%%= f.error_notification %> 4 | <%%= f.error_notification message: f.object.errors[:base].to_sentence if f.object.errors[:base].present? %> 5 | 6 |
7 | <%- attributes.each do |attribute| -%> 8 | <%%= f.<%= attribute.reference? ? :association : :input %> :<%= attribute.name %> %> 9 | <%- end -%> 10 |
11 | 12 |
13 | <%%= f.button :submit %> 14 |
15 | <%% end %> 16 | -------------------------------------------------------------------------------- /log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackathons-backend/92a9670ac669d9e3ec3237801f1d8949f6e7e552/log/.keep -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackathons-backend/92a9670ac669d9e3ec3237801f1d8949f6e7e552/public/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackathons-backend/92a9670ac669d9e3ec3237801f1d8949f6e7e552/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackathons-backend/92a9670ac669d9e3ec3237801f1d8949f6e7e552/public/favicon.ico -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-Agent: * 2 | Disallow: / 3 | -------------------------------------------------------------------------------- /storage/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackathons-backend/92a9670ac669d9e3ec3237801f1d8949f6e7e552/storage/.keep -------------------------------------------------------------------------------- /test/application_system_test_case.rb: -------------------------------------------------------------------------------- 1 | require "capybara/cuprite" 2 | require "test_helper" 3 | 4 | class ApplicationSystemTestCase < ActionDispatch::SystemTestCase 5 | driven_by :cuprite, options: {headless: :new, pending_connection_errors: false, js_errors: true} 6 | 7 | def sign_in_as(user) 8 | visit sign_in_path 9 | 10 | fill_in :email_address, with: user.email_address 11 | click_on "Send Sign In Link" 12 | assert_text(/sent/i) 13 | 14 | visit new_session_path(auth_token: user.authentications.last.token) 15 | end 16 | 17 | def visit(path) 18 | super 19 | find 'body[data-stimulus-ready="true"]' 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/fixtures/active_storage/attachments.yml: -------------------------------------------------------------------------------- 1 | assemble_logo: 2 | name: logo 3 | record: assemble (Hackathon) 4 | blob: assemble_logo 5 | 6 | assemble: 7 | name: banner 8 | record: assemble (Hackathon) 9 | blob: assemble 10 | -------------------------------------------------------------------------------- /test/fixtures/active_storage/blobs.yml: -------------------------------------------------------------------------------- 1 | hack_club_logo: <%= ActiveStorage::FixtureSet.blob filename: "hack_club_logo.jpg", service_name: "test_fixtures" %> 2 | assemble: <%= ActiveStorage::FixtureSet.blob filename: "assemble.jpg", service_name: "test_fixtures" %> 3 | assemble_logo: <%= ActiveStorage::FixtureSet.blob filename: "assemble_logo.jpg", service_name: "test_fixtures" %> 4 | -------------------------------------------------------------------------------- /test/fixtures/files/assemble.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackathons-backend/92a9670ac669d9e3ec3237801f1d8949f6e7e552/test/fixtures/files/assemble.jpg -------------------------------------------------------------------------------- /test/fixtures/files/assemble_logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackathons-backend/92a9670ac669d9e3ec3237801f1d8949f6e7e552/test/fixtures/files/assemble_logo.jpg -------------------------------------------------------------------------------- /test/fixtures/files/hack_club_logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackathons-backend/92a9670ac669d9e3ec3237801f1d8949f6e7e552/test/fixtures/files/hack_club_logo.jpg -------------------------------------------------------------------------------- /test/fixtures/hackathon/digest/listings.yml: -------------------------------------------------------------------------------- 1 | one: 2 | digest: one 3 | hackathon: seattle_hacks 4 | subscription: gary_seattle 5 | -------------------------------------------------------------------------------- /test/fixtures/hackathon/digests.yml: -------------------------------------------------------------------------------- 1 | one: 2 | recipient: gary 3 | -------------------------------------------------------------------------------- /test/fixtures/hackathon/subscriptions.yml: -------------------------------------------------------------------------------- 1 | gary_seattle: 2 | subscriber: gary 3 | city: Seattle 4 | province: Washington 5 | country_code: US 6 | latitude: 47.6038321 7 | longitude: -122.330062 8 | -------------------------------------------------------------------------------- /test/fixtures/hackathons.yml: -------------------------------------------------------------------------------- 1 | zephyr: 2 | name: The Hacker Zephyr 3 | status: <%= Hackathon.statuses[:approved] %> 4 | starts_at: <%= 1.day.from_now %> 5 | ends_at: <%= 2.days.from_now %> 6 | website: https://zephyr.hackclub.com 7 | high_school_led: true 8 | expected_attendees: 50 9 | modality: <%= Hackathon.modalities[:in_person] %> 10 | applicant: gary 11 | seattle_hacks: 12 | name: SeattleHacks 13 | status: <%= Hackathon.statuses[:approved] %> 14 | starts_at: <%= 1.day.from_now %> 15 | ends_at: <%= 2.days.from_now %> 16 | website: https://hackclub.com 17 | high_school_led: true 18 | expected_attendees: 50 19 | modality: <%= Hackathon.modalities[:in_person] %> 20 | applicant: gary 21 | city: Seattle 22 | province: Washington 23 | country_code: US 24 | latitude: 47.6038321 25 | longitude: -122.330062 26 | bellevue_hacks: 27 | name: BellevueHacks 28 | status: <%= Hackathon.statuses[:approved] %> 29 | starts_at: <%= 1.day.from_now %> 30 | ends_at: <%= 2.days.from_now %> 31 | website: https://hackclub.com 32 | high_school_led: true 33 | expected_attendees: 50 34 | modality: <%= Hackathon.modalities[:in_person] %> 35 | applicant: gary 36 | city: Bellevue 37 | province: Washington 38 | country_code: US 39 | latitude: 47.6144219 40 | longitude: -122.192337 41 | -------------------------------------------------------------------------------- /test/fixtures/mailing_addresses.yml: -------------------------------------------------------------------------------- 1 | hack_club: 2 | name: Hack Club 3 | line1: 8605 Santa Monica Blvd 4 | line2: "#86294" 5 | city: West Hollywood 6 | province: CA 7 | postal_code: 90069 8 | country_code: US 9 | 10 | hack_club_hq: 11 | line1: 15 Falls Rd 12 | city: Shelburne 13 | province: Vermont 14 | postal_code: 05482 15 | country_code: US 16 | -------------------------------------------------------------------------------- /test/fixtures/users.yml: -------------------------------------------------------------------------------- 1 | matt: 2 | name: Matt Almeida 3 | email_address: matt7@hey.com 4 | admin: true 5 | 6 | peasant_matt: 7 | name: Matt Almeida 8 | email_address: matt7@hey.local 9 | admin: false 10 | 11 | gary: 12 | name: Gary Tou 13 | email_address: gary@hackclub.com 14 | admin: true 15 | -------------------------------------------------------------------------------- /test/mailers/hackathons/digest_mailer_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class Hackathons::DigestMailerTest < ActionMailer::TestCase 4 | test "digest" do 5 | digest = hackathon_digests(:one) 6 | mail = Hackathons::DigestMailer.digest(digest) 7 | 8 | assert_equal "Hackathons near you", mail.subject 9 | assert_equal [digest.recipient.email_address], mail.to 10 | 11 | digest.listings.map(&:subscription).each do |subscription| 12 | assert_match "Near #{subscription.to_location.to_fs(:short)}", mail.body.encoded 13 | end 14 | 15 | digest.listings.map(&:hackathon).each do |hackathon| 16 | assert_match hackathon.name, mail.body.encoded 17 | end 18 | 19 | # Email CAN-SPAM compliance 20 | assert_match "unsubscribe", mail.body.encoded 21 | assert_match Hackathons::HACK_CLUB_ADDRESS[:full], mail.body.encoded 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/mailers/hackathons/submission_mailer_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class Hackathons::SubmissionMailerTest < ActionMailer::TestCase 4 | include Rails.application.routes.url_helpers 5 | 6 | setup do 7 | @hackathon = hackathons(:zephyr) 8 | end 9 | 10 | test "confirmation" do 11 | email = Hackathons::SubmissionMailer.with(hackathon: @hackathon).confirmation.deliver_now 12 | 13 | assert email.to, @hackathon.applicant.email_address 14 | 15 | assert_includes email.subject, "submitted" 16 | end 17 | 18 | test "admin notification" do 19 | email = Hackathons::SubmissionMailer.with(hackathon: @hackathon).admin_notification.deliver_now 20 | 21 | assert email.to, User.admins.pluck(:email_address) 22 | 23 | assert_includes email.subject, "submitted" 24 | assert_includes email.subject, @hackathon.name 25 | 26 | assert_includes email.to_s, admin_hackathon_url(@hackathon) 27 | end 28 | 29 | test "admin notification with some admins opting out" do 30 | User.admins.first.update!(new_hackathon_submission_notifications: false) 31 | 32 | email = Hackathons::SubmissionMailer.with(hackathon: @hackathon).admin_notification.deliver_now 33 | 34 | assert email.to, User.admins.pluck(:email_address) - [User.admins.first.email_address] 35 | 36 | assert_includes email.subject, "submitted" 37 | assert_includes email.subject, @hackathon.name 38 | 39 | assert_includes email.to_s, admin_hackathon_url(@hackathon) 40 | end 41 | 42 | test "approval" do 43 | email = Hackathons::SubmissionMailer.with(hackathon: @hackathon).approval.deliver_now 44 | 45 | assert email.to, @hackathon.applicant.email_address 46 | 47 | assert_includes email.subject, "approved" 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /test/mailers/previews/hackathon/digest_mailer_preview.rb: -------------------------------------------------------------------------------- 1 | # Preview all emails at http://localhost:3000/rails/mailers/hackathons/digest_mailer 2 | class Hackathons::DigestMailerPreview < ActionMailer::Preview 3 | def digest 4 | Hackathons::DigestMailer.digest(Hackathon::Digest.last) 5 | end 6 | 7 | def admin_summary 8 | Hackathons::DigestMailer.admin_summary(Hackathon::Digest.all) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/models/database_dump_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class DatabaseDumpTest < ActiveSupport::TestCase 4 | test "dumping" do 5 | dump = DatabaseDump.create! 6 | 7 | assert dump.processed? 8 | assert dump.file.attached? 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/models/hackathon/regional_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class Hackathon::RegionalTest < ActiveSupport::TestCase 4 | setup do 5 | @hackathon = Hackathon.new( 6 | name: "HQHacks", 7 | starts_at: Time.now, 8 | ends_at: 1.day.from_now, 9 | website: "https://hackclub.com", 10 | high_school_led: true, 11 | expected_attendees: 20, 12 | modality: "in_person", 13 | logo: active_storage_blobs(:assemble_logo), 14 | banner: active_storage_blobs(:assemble), 15 | applicant: users(:gary) 16 | ) 17 | 18 | [{ 19 | coordinates: [44.3803059, -73.2271145], 20 | address: "15, Falls Road, Shelburne, Chittenden County, Vermont, 05482, United States", 21 | house_number: "15", 22 | street: "Falls Road", 23 | city: "Shelburne", 24 | province: "Vermont", 25 | state: "Vermont", 26 | postal_code: "05482", 27 | country_code: "us" 28 | }].tap do |results| 29 | # The geocode results are the same for both lookup strings 30 | Geocoder::Lookup::Test.add_stub("15 Falls Road, Shelburne, Vermont", results) 31 | Geocoder::Lookup::Test.add_stub("15 Falls Road, VT", results) 32 | end 33 | end 34 | 35 | test "creating a hackathon with seperated location attributes" do 36 | @hackathon.street = "15 Falls Road" 37 | @hackathon.city = "Shelburne" 38 | @hackathon.province = "Vermont" 39 | 40 | assert @hackathon.save 41 | assert_equal @hackathon.address, "15, Falls Road, Shelburne, Chittenden County, Vermont, 05482, United States" 42 | end 43 | 44 | test "creating a hackathon with a full address" do 45 | @hackathon.address = "15 Falls Road, VT" 46 | 47 | assert @hackathon.save 48 | assert_equal @hackathon.province, "Vermont" 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /test/models/hackathon/subscription_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class SubscriptionTest < ActiveSupport::TestCase 4 | setup do 5 | Geocoder::Lookup::Test.add_stub( 6 | "05482", [{ 7 | coordinates: [44.3909, -73.2187], 8 | city: "Shelburne", 9 | province: "Vermont", 10 | state: "Vermont", 11 | country_code: "us" 12 | }] 13 | ) 14 | 15 | @user = users(:matt) 16 | Current.user = @user 17 | end 18 | 19 | test "subscribing to hackathons for an area" do 20 | assert_difference -> { @user.subscriptions.count } do 21 | Hackathon::Subscription.create location_input: "05482" 22 | end 23 | 24 | assert_equal "Shelburne, Vermont, US", Hackathon::Subscription.last.location 25 | end 26 | 27 | test "subscribing for the same area" do 28 | assert_difference -> { @user.subscriptions.count } do 29 | Hackathon::Subscription.create location_input: "05482" 30 | end 31 | 32 | assert_no_difference -> { @user.subscriptions.active.count } do 33 | Hackathon::Subscription.create location_input: "05482" 34 | end 35 | end 36 | 37 | test "disabling a subscription" do 38 | subscription = Hackathon::Subscription.new location_input: "05482" 39 | assert subscription.save 40 | assert subscription.active? 41 | 42 | assert_difference -> { subscription.events.count } do 43 | subscription.update! status: :inactive 44 | end 45 | 46 | assert_includes subscription.events.collect(&:action), "disabled" 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /test/models/mailing_address_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class MailingAddressTest < ActiveSupport::TestCase 4 | test "creating a mailing address" do 5 | mailing_address = MailingAddress.new( 6 | line1: "15 Falls Rd", 7 | city: "Shelburne", 8 | province: "Vermont", 9 | postal_code: "05482", 10 | country_code: "US" 11 | ) 12 | 13 | assert mailing_address.save 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/models/tag_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class TagTest < ActiveSupport::TestCase 4 | test "creating a tag without a name" do 5 | assert_not Tag.new.save 6 | end 7 | 8 | test "creating a tag" do 9 | assert Tag.new(name: "test").save 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/models/user/informed_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class User::InformedTest < ActiveSupport::TestCase 4 | test "synchronization" do 5 | User.first.sync_with_loops 6 | end 7 | 8 | setup do 9 | stub_request(:get, Loops::Resource::BASE_URL + "contacts/find") 10 | .with(query: hash_including(:email)).to_return do |request| 11 | {body: [ 12 | { 13 | id: Random.uuid, 14 | email: request.headers[:email], 15 | firstName: "Orpheus", 16 | lastName: nil, 17 | subscribed: true, 18 | userGroup: "Hack Clubber" 19 | } 20 | ]} 21 | end 22 | 23 | stub_request(:put, Loops::Resource::BASE_URL + "contacts/update") 24 | .with(body: hash_including(:email)) 25 | .to_return({body: {id: Random.uuid}.to_json}) 26 | 27 | stub_request(:post, Loops::Resource::BASE_URL + "contacts/create") 28 | .with(body: hash_including(:email)) 29 | .to_return({body: {id: Random.uuid}.to_json}) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/models/user/privilege_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class User::PrivilegeTest < ActiveSupport::TestCase 4 | test "promoting a user" do 5 | user = users(:peasant_matt) 6 | assert_not user.admin? 7 | 8 | user.update admin: true 9 | 10 | assert user.admin? 11 | assert_equal "promoted_to_admin", user.events.last&.action 12 | end 13 | 14 | test "demoting a user" do 15 | user = users(:matt) 16 | assert user.admin? 17 | 18 | user.update admin: false 19 | 20 | assert_not user.admin? 21 | assert_equal "demoted_from_admin", user.events.last&.action 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/models/user_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class UserTest < ActiveSupport::TestCase 4 | test "creating a user" do 5 | user = User.new name: "Valid User", email_address: "user@hey.test" 6 | assert user.save 7 | end 8 | 9 | test "creating a user without required fields" do 10 | assert_not User.new.save 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/system/admin/users_test.rb: -------------------------------------------------------------------------------- 1 | require "application_system_test_case" 2 | 3 | class Admin::UsersTest < ApplicationSystemTestCase 4 | setup do 5 | sign_in_as users(:matt) 6 | 7 | @user = User.first 8 | end 9 | 10 | test "searching for a user" do 11 | visit admin_users_path 12 | 13 | email_address = find_field(:email_address) 14 | email_address.click 15 | email_address.fill_in with: "nonexistent@hey.test" 16 | email_address.send_keys :enter 17 | 18 | assert_text(/not found/i) 19 | 20 | email_address.fill_in with: @user.email_address 21 | email_address.send_keys :enter 22 | 23 | assert_text @user.display_name 24 | end 25 | 26 | test "editing a user's name" do 27 | visit admin_user_path(@user) 28 | 29 | within("turbo-frame#name") do 30 | click_on "✏️" 31 | end 32 | 33 | name = find_field(:user_name) 34 | name.click 35 | name.fill_in with: "#{@user.name} 2.0" 36 | name.send_keys :enter 37 | 38 | assert_no_field :user_name 39 | assert_equal "#{@user.name} 2.0", @user.reload.name 40 | end 41 | 42 | test "changing a user's email address" do 43 | visit admin_user_path(@user) 44 | 45 | within("turbo-frame#email_address") do 46 | click_on "✏️" 47 | end 48 | 49 | email_address = find_field(:user_email_address) 50 | email_address.click 51 | email_address.fill_in with: "different@hey.test" 52 | email_address.send_keys :enter 53 | 54 | assert_no_field :user_email_address 55 | assert_equal @user.reload.email_address, "different@hey.test" 56 | end 57 | 58 | test "changing a user's email address to one already in use" do 59 | visit admin_user_path(@user) 60 | 61 | within("turbo-frame#email_address") do 62 | click_on "✏️" 63 | end 64 | 65 | email_address = find_field(:user_email_address) 66 | email_address.click 67 | email_address.fill_in with: User.second.email_address 68 | email_address.send_keys :enter 69 | 70 | assert_text(/taken/i) 71 | assert_field :user_email_address 72 | 73 | assert_not_equal @user.reload.email_address, User.second.email_address 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /test/system/authentication_test.rb: -------------------------------------------------------------------------------- 1 | require "application_system_test_case" 2 | 3 | class AuthenticationTest < ApplicationSystemTestCase 4 | setup do 5 | @user = users(:matt) 6 | end 7 | 8 | test "requesting a sign in link" do 9 | ApplicationMailer.deliveries.clear 10 | visit sign_in_path 11 | 12 | fill_in :email_address, with: @user.email_address 13 | click_on "Send Sign In Link" 14 | 15 | assert_text(/sent/i) 16 | assert_text(@user.email_address) 17 | 18 | authentication = @user.authentications.last 19 | assert authentication 20 | assert_not authentication&.succeeded? 21 | 22 | assert_equal 1, UserMailer.deliveries.count 23 | 24 | delivery = UserMailer.deliveries.first 25 | 26 | assert_includes delivery&.to, @user.email_address 27 | assert_includes delivery&.text_part&.body, new_session_path(auth_token: authentication&.token) 28 | assert_includes delivery&.html_part&.body, new_session_path(auth_token: authentication&.token) 29 | end 30 | 31 | test "using an invalid link" do 32 | assert_no_difference -> { @user.sessions.reload.count } do 33 | visit new_session_path(auth_token: "1234") 34 | end 35 | 36 | assert_current_path sign_in_path 37 | end 38 | 39 | test "using an expired link" do 40 | authentication = @user.authentications.create! 41 | 42 | travel 1.day 43 | 44 | assert_no_difference -> { @user.sessions.reload.count } do 45 | visit new_session_path(auth_token: authentication.token) 46 | end 47 | 48 | assert_not authentication.succeeded? 49 | assert_equal "rejected", authentication.events&.last&.action 50 | assert_equal "expired", authentication.events&.last&.details&.dig("reason") 51 | 52 | assert_current_path sign_in_path 53 | end 54 | 55 | test "using a valid link" do 56 | authentication = @user.authentications.create! 57 | 58 | assert_difference -> { @user.sessions.reload.count } do 59 | visit new_session_path(auth_token: authentication.token) 60 | end 61 | 62 | assert authentication.succeeded? 63 | 64 | assert_no_current_path sign_in_path 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /test/system/hackathon_submission_test.rb: -------------------------------------------------------------------------------- 1 | require "application_system_test_case" 2 | 3 | class HackathonSubmissionTest < ApplicationSystemTestCase 4 | setup do 5 | Geocoder::Lookup::Test.add_stub( 6 | "760 Market St, San Francisco, CA, 94102, US", [{ 7 | coordinates: [37.7866671, -122.40505], 8 | address: "760, Market Street, Union Square, San Francisco, CAL Fire Northern Region, California, 94102, United States", 9 | house_number: "760", 10 | street: "Market Street", 11 | city: "San Francisco", 12 | province: "California", 13 | state: "California", 14 | postal_code: "94102", 15 | country_code: "us" 16 | }] 17 | ) 18 | end 19 | 20 | test "submitting a hackathon" do 21 | assert_not Current.user 22 | 23 | visit new_hackathons_submission_path 24 | 25 | fill_in "Your name", with: "Gary" 26 | fill_in "Email address", with: "not.a.user.yet@hey.test" 27 | select "No", from: "Are you a high schooler?" 28 | 29 | fill_in "Name of the hackathon", with: "Assemble" 30 | fill_in "Start date", with: 1.month.from_now.beginning_of_minute 31 | fill_in "End date", with: (1.month.from_now + 2.days).beginning_of_minute 32 | fill_in "Website", with: "https://assemble.hackclub.com" 33 | 34 | attach_file "Logo", Rails.root.join("test/fixtures/files/assemble_logo.jpg") 35 | attach_file "Banner", Rails.root.join("test/fixtures/files/assemble.jpg") 36 | 37 | select "In Person", from: "Where is the hackathon taking place?" 38 | fill_in "Street", with: "760 Market St" 39 | fill_in "City", with: "San Francisco" 40 | fill_in "State/Province", with: "CA" 41 | fill_in "ZIP/Postal Code", with: "94102" 42 | select "United States", from: "Country" 43 | 44 | fill_in :hackathon_expected_attendees, with: 100 45 | 46 | select "Yes", from: :hackathon_offers_financial_assistance 47 | select "No", from: :requested_swag 48 | 49 | sleep 1.second # let browser catch up, preventing flaky tests 50 | 51 | click_on "Submit for Review" 52 | assert_text(/submitted/i) 53 | 54 | assert_equal "not.a.user.yet@hey.test", Hackathon.last.applicant.email_address 55 | assert_equal "Assemble", Hackathon.last.name 56 | assert_not Hackathon.last.requested_swag? 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /test/system/read_only_mode_test.rb: -------------------------------------------------------------------------------- 1 | require "application_system_test_case" 2 | 3 | class ReadOnlyModeTest < ApplicationSystemTestCase 4 | setup do 5 | ENV["READ_ONLY_MODE"] = "true" 6 | end 7 | 8 | teardown do 9 | ENV.delete "READ_ONLY_MODE" 10 | end 11 | 12 | test "signing in with read only mode" do 13 | visit sign_in_path 14 | assert_no_text(/read only/i) 15 | 16 | fill_in :email_address, with: users(:matt).email_address 17 | click_on "Send Sign In Link" 18 | 19 | assert_text(/read only/i) 20 | assert_not User::Authentication.exists? user: users(:matt) 21 | end 22 | 23 | test "default behavior takes priority" do 24 | assert_raises ReadOnlyModeError do 25 | users(:matt).update! name: "different" 26 | end 27 | 28 | users(:matt).readonly! 29 | 30 | assert_raises ActiveRecord::ReadOnlyRecord do 31 | users(:matt).update! name: "different" 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV["RAILS_ENV"] ||= "test" 2 | require_relative "../config/environment" 3 | require "rails/test_help" 4 | 5 | require "webmock/minitest" 6 | WebMock.disable_net_connect!(allow_localhost: true) 7 | 8 | class ActiveSupport::TestCase 9 | # Run tests in parallel with specified workers 10 | parallelize(workers: ENV.fetch("PARALLEL_WORKERS", :number_of_processors)) 11 | 12 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. 13 | fixtures :all 14 | 15 | Geocoder.configure(lookup: :test, ip_lookup: :test) 16 | Geocoder::Lookup::Test.reset 17 | end 18 | -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackathons-backend/92a9670ac669d9e3ec3237801f1d8949f6e7e552/tmp/.keep -------------------------------------------------------------------------------- /tmp/pids/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackathons-backend/92a9670ac669d9e3ec3237801f1d8949f6e7e552/tmp/pids/.keep -------------------------------------------------------------------------------- /tmp/storage/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackathons-backend/92a9670ac669d9e3ec3237801f1d8949f6e7e552/tmp/storage/.keep -------------------------------------------------------------------------------- /vendor/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackathons-backend/92a9670ac669d9e3ec3237801f1d8949f6e7e552/vendor/.keep -------------------------------------------------------------------------------- /vendor/javascript/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackclub/hackathons-backend/92a9670ac669d9e3ec3237801f1d8949f6e7e552/vendor/javascript/.keep --------------------------------------------------------------------------------