Skip to content
This repository was archived by the owner on Feb 28, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api/Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ group :development, :test do
gem 'database_cleaner', '~> 1.5', '>= 1.5.3'
gem 'dotenv-rails', '~> 2.1', '>= 2.1.1'
gem 'factory_girl_rails', '~> 4.7', '>= 4.7.0'
gem 'letter_opener', '~> 1.4.1'
gem 'rspec-rails', '~> 3.5', '>= 3.5.2'
gem 'vcr', '~> 3.0'
gem 'webmock', '~> 2.1'
Expand Down
5 changes: 5 additions & 0 deletions api/Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ GEM
i18n (0.8.4)
json (2.0.2)
jsonapi-renderer (0.1.2)
launchy (2.4.3)
addressable (~> 2.3)
letter_opener (1.4.1)
launchy (~> 2.2)
listen (3.0.8)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
Expand Down Expand Up @@ -292,6 +296,7 @@ DEPENDENCIES
geocoder (~> 1.4)
gruff
guard-rspec (~> 4.7)
letter_opener (~> 1.4.1)
listen (~> 3.0.5)
octokit (~> 4.7)
pg (~> 0.18.4)
Expand Down
26 changes: 26 additions & 0 deletions api/app/controllers/v1/club_applications_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
module V1
class ClubApplicationsController < ApplicationController
def create
application = ClubApplication.new(application_params)

if application.save
render json: application, status: 201

ClubApplicationMailer.application_confirmation(application).deliver
ClubApplicationMailer.admin_notification(application).deliver
else
render json: { errors: application.errors }, status: 422
end
end

private

def application_params
params.require(:club_application).permit(
:first_name, :last_name, :email, :github, :twitter, :high_school,
:interesting_project, :systems_hacked, :steps_taken, :year, :referer,
:phone_number, :start_date
)
end
end
end
21 changes: 21 additions & 0 deletions api/app/mailers/club_application_mailer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
class ClubApplicationMailer < ApplicationMailer
default from: 'Hack Club Team <team@hackclub.com>'

def application_confirmation(application)
@application = application

to = Mail::Address.new @application.email
to.display_name = @application.full_name

mail(to: to.format, subject: 'Application Confirmation')
end

def admin_notification(application)
@application = application

to = Mail::Address.new 'team@hackclub.com'
to.display_name = 'Hack Club Team'

mail(to: to.format, subject: @application.high_school)
end
end
10 changes: 2 additions & 8 deletions api/app/models/club.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@ class Club < ApplicationRecord
'Hack Camp' => '9010'
}
},
point_of_contact_name: '1012',
activation_date: '1015'
)

streak_read_only point_of_contact_name: '1012'

geocode_attrs address: :address,
latitude: :latitude,
longitude: :longitude
Expand Down Expand Up @@ -62,13 +63,6 @@ def dormant?
stage_key == DORMANT_STAGE
end

# This setter prevents the point of contact name from being set from Streak.
# The point of contact should only be changed in the database, which will
# update the Streak pipeline.
def point_of_contact_name=(_)
nil
end

def make_active
self.stage_key = ACTIVE_STAGE

Expand Down
116 changes: 116 additions & 0 deletions api/app/models/club_application.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# rubocop:disable Metrics/ClassLength
class ClubApplication < ApplicationRecord
include Streakable

streak_pipeline_key(
Rails.application.secrets.streak_club_applications_pipeline_key
)

streak_default_field_mappings key: :streak_key, name: :high_school,
notes: :notes, stage: :stage_key
streak_field_mappings(
first_name: '1010',
last_name: '1011',
email: '1012',
github: '1013',
twitter: '1014',
phone_number: '1019',
interesting_project: '1015',
systems_hacked: '1016',
steps_taken: '1017',
referer: '1018',
start_date: {
key: '1008',
type: 'DATE'
},
year: {
key: '1020',
type: 'DROPDOWN',
options: {
'2016' => '9007',
'2017' => '9006',
'2018' => '9005',
'2019' => '9004',
'2020' => '9003',
'2021' => '9002',
'2022' => '9001',
'Graduated' => '9008',
'Teacher' => '9009',
'Unknown' => '9010'
}
},
application_quality: {
key: '1009',
type: 'DROPDOWN',
options: {
'Satisfactory' => '9001',
'Above Average' => '9002',
'Excellent' => '9003',
'Unknown' => '9004'
}
},
rejection_reason: {
key: '1004',
type: 'DROPDOWN',
options: {
'Spam' => '9001',
'Not High School Student' => '9002',
'Did not respond to acceptance email' => '9004',
'Bad timing' => '9005',
'Not enough detail' => '9006',
'Teacher' => '9007',
'Merged with another application' => '9008',
'Bad fit' => '9009',
'Unknown' => '9010',
"Didn't show to onboarding call" => '9011',
"Didn't show to interview" => '9012'
}
},
source: {
key: '1003',
type: 'DROPDOWN',
options: {
'Word of Mouth' => '9001',
'Unknown' => '9002',
'Free Code Camp' => '9003',
'GitHub' => '9004',
'Press' => '9005',
'Searching online' => '9006',
'Hackathon' => '9007',
'Website' => '9008',
'Social media' => '9009',
'Hack Camp' => '9010',
'Hacker News' => '9011',
'French TV' => '9012',
'Code HS' => '9013',
'Direct Marketing' => '9014',
'Direct Marketing - Emailing Teachers' => '9016',
'Direct Marketing - GitHub Outreach' => '9018',
'Facebook Ad' => '9015',
'Product Hunt' => '9017',
'Direct Marketing - CodeDay Outreach' => '9019',
'Matthew Email Campaign' => '9020',
'Github Outreach—Hackathon Participant' => '9021'
}
}
)

streak_read_only spam: '1021'

validates :first_name, :last_name, :email, :high_school,
:interesting_project, :systems_hacked, :steps_taken, :year,
:referer, :start_date, presence: true

def full_name
"#{first_name} #{last_name}"
end

def spam
spam?.to_s
end

def spam?
ClubApplicationSpamService.new.spam? self
end
end
# rubocop:enable Metrics/ClassLength
40 changes: 38 additions & 2 deletions api/app/models/concerns/streakable.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# frozen_string_literal: true
# rubocop:disable Metrics/ModuleLength
module Streakable
extend ActiveSupport::Concern

Expand Down Expand Up @@ -35,6 +36,18 @@ def streak_default_field_mappings(key:, name:, notes:, stage:)
@notes_attribute = notes
@stage_attribute = stage
end

def streak_read_only(mapping)
@field_mappings ||= {}
@field_mappings = @field_mappings.merge(mapping)

mapping.keys.each do |sym|
# Construct a setter out of the symbol
read_only = :"#{sym.to_s + '='}"

define_method(read_only) { |_| nil }
end
end
end

included do
Expand Down Expand Up @@ -63,7 +76,16 @@ def streak_field_key_for_attribute(attribute)
end

def streak_field_value_for_attribute(attribute)
send(attribute)
val = send(attribute)
mapping = self.class.field_mappings[attribute]

return val unless mapping.is_a? Hash

if mapping[:type] == 'DATE'
time_to_ms_since_epoch(val)
else
val
end
end

# Given a symbol attribute name, get that attribute's value, parse the model's
Expand Down Expand Up @@ -92,7 +114,8 @@ def linked_streak_box_keys

def create_box
unless streak_key_val
resp = StreakClient::Box.create_in_pipeline(self.class.pipeline_key, name)
resp = StreakClient::Box.create_in_pipeline(self.class.pipeline_key,
name_key_val)

# Need to use self here because it'll try to create a variable by default
# (try removing 'self.' from the beginning and running tests to see for
Expand Down Expand Up @@ -156,4 +179,17 @@ def stage_key_val
def stage_key_val=(val)
send("#{self.class.stage_attribute}=", val)
end

def name_key_val
send(self.class.name_attribute)
end

def name_key_val=(val)
send("#{self.class.name_attribute}=", val)
end

def time_to_ms_since_epoch(time)
(time.to_f * 1000).to_i
end
end
# rubocop:enable Metrics/ModuleLength
14 changes: 14 additions & 0 deletions api/app/services/club_application_spam_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
class ClubApplicationSpamService
WORD_CUTOFF = 5

def spam?(application)
word_count(application.interesting_project) <= WORD_CUTOFF ||
word_count(application.systems_hacked) <= WORD_CUTOFF
end

private

def word_count(str)
str.split(' ').count
end
end
30 changes: 30 additions & 0 deletions api/app/views/club_application/_club_application.text.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
Name: <%= @application.full_name %>
High school: <%= @application.high_school %>
Year: <%= @application.year %>
Email: <%= @application.email %>
Phone number: <%= @application.phone_number %>
GitHub: <%= @application.github %>
Twitter: <%= @application.twitter %>
Start date: <%= @application.start_date %>


Please tell us about an interesting project, preferably outside of
class, that you created or worked on.

<%= @application.interesting_project %>


Please tell us about the time you most successfully hacked some
(non-computer) system to your advantage.

<%= @application.systems_hacked %>


What steps have you taken so far in starting your club?

<%= @application.steps_taken %>


How did you hear about us?

<%= @application.referer %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
New application for Hack Club received:

<%= render "club_application/club_application" %>

<% if @application.spam? %>
This application is suspected to be spam!
<% end %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Hey <%= @application.first_name %>!

Thanks for applying to Hack Club. We've received your application and you
can expect to hear from us shortly. If you have any questions, please
don't hesitate to email me at zach@hackclub.com. Below is a copy of your
application for your records.

<%= render "club_application/club_application" %>
37 changes: 37 additions & 0 deletions api/circle.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
machine:
environment:
GOOGLE_MAPS_API_KEY: test_maps_api_key
STREAK_CLUB_PIPELINE_KEY: clubs
STREAK_LEADER_PIPELINE_KEY: leaders
STREAK_LETTER_PIPELINE_KEY: letters
STREAK_API_KEY: test_api_key
CLOUD9_USERNAME: test_username
CLOUD9_PASSWORD: test_password
CLOUD9_TEAM_NAME: fake_team_name
STREAK_OUTREACH_SCHOOL_PIPELINE_KEY: schools
STREAK_OUTREACH_TEACHER_PIPELINE_KEY: teachers
SLACK_CLIENT_ID: fake_client_id
SLACK_CLIENT_SECRET: fake_client_secret
DEFAULT_SLACK_TEAM_ID: fake_slack_team_id
TECH_DOMAIN_REDEMPTION_SECRET_CODE: fake_secret_code
GIPHY_API_KEY: fake_giphy_api_key
DEFAULT_STREAK_TASK_ASSIGNEE: email@email.email
HACKBOT_MIRROR_CHANNEL_ID: fake_channel_id
HACKBOT_ADMINS: fake,admin,list
STREAK_DEMO_USER_BOX_KEY: fake_streak_key
GITHUB_BOT_ACCESS_TOKEN: fake_access_token
test:
override:
- bundle exec rubocop
- bundle exec rspec

deployment:
production:
branch: master
commands:
# CircleCI shallow clones by default. This fetches the remainder of the
# repo's history. Heroku deploys will occasionally fail without this.
- "[[ ! -s \"$(git rev-parse --git-dir)/shallow\" ]] || git fetch --unshallow"
- git push git@heroku.com:api-hackclub.git $CIRCLE_SHA1:refs/heads/master
- heroku run rake db:migrate --app api-hackclub:
timeout: 400 # Deploys may take a long time
Loading