This commit is contained in:
Seth Call 2016-08-31 04:19:16 -05:00
parent c5f7711850
commit 5353b75c2e
78 changed files with 3356 additions and 181 deletions

View File

@ -14,7 +14,7 @@ class ArtifactsController < ApplicationController
ArtifactUpdate.transaction do
# VRFS-1071: Postpone client update notification until installer is available for download
ArtifactUpdate.connection.execute('SET TRANSACTION ISOLATION LEVEL READ COMMITTED')
@artifact = ArtifactUpdate.find_or_create_by({product: product, environement: environment})
@artifact = ArtifactUpdate.find_or_create_by({product: product, environment: environment})
@artifact.version = version
@artifact.uri = file

View File

@ -363,4 +363,5 @@ jamblasters_network.sql
immediate_recordings.sql
nullable_user_id_jamblaster.sql
rails4_migration.sql
non_free_jamtracks.sql
non_free_jamtracks.sql
retailers.sql

96
db/up/retailers.sql Normal file
View File

@ -0,0 +1,96 @@
CREATE TABLE retailers (
id INTEGER PRIMARY KEY,
user_id VARCHAR(64) REFERENCES users(id) NOT NULL,
name VARCHAR,
enabled BOOLEAN DEFAULT TRUE,
city VARCHAR,
state VARCHAR,
slug VARCHAR NOT NULL,
encrypted_password VARCHAR NOT NULL DEFAULT uuid_generate_v4(),
photo_url VARCHAR(2048),
original_fpfile VARCHAR(8000),
cropped_fpfile VARCHAR(8000),
cropped_s3_path VARCHAR(8000),
crop_selection VARCHAR(256),
large_photo_url VARCHAR(512),
cropped_large_s3_path VARCHAR(512),
cropped_large_fpfile VARCHAR(8000),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE SEQUENCE retailer_key_sequence;
ALTER SEQUENCE retailer_key_sequence RESTART WITH 10000;
ALTER TABLE retailers ALTER COLUMN id SET DEFAULT nextval('retailer_key_sequence');
CREATE TABLE retailer_invitations (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id VARCHAR(64) REFERENCES users(id),
retailer_id INTEGER REFERENCES retailers(id) NOT NULL,
invitation_code VARCHAR(256) NOT NULL UNIQUE,
note VARCHAR,
email VARCHAR NOT NULL,
first_name VARCHAR,
last_name VARCHAR,
accepted BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE posa_cards (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
code VARCHAR(64) UNIQUE NOT NULL,
user_id VARCHAR (64) REFERENCES users(id) ON DELETE SET NULL,
card_type VARCHAR(64) NOT NULL,
origin VARCHAR(200),
activated_at TIMESTAMP,
claimed_at TIMESTAMP,
retailer_id INTEGER REFERENCES retailers(id) ON DELETE SET NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX posa_card_user_id_idx ON posa_cards(user_id);
ALTER TABLE users ADD COLUMN jamclass_credits INTEGER DEFAULT 0;
CREATE TABLE posa_card_types (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
card_type VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO posa_card_types (id, card_type) VALUES ('jam_tracks_5', 'jam_tracks_5');
INSERT INTO posa_card_types (id, card_type) VALUES ('jam_tracks_10', 'jam_tracks_10');
INSERT INTO posa_card_types (id, card_type) VALUES ('jam_class_10', 'jam_class_10');
CREATE TABLE posa_card_purchases (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE SET NULL,
posa_card_type_id VARCHAR(64) REFERENCES posa_card_types(id) ON DELETE SET NULL,
posa_card_id VARCHAR(64) REFERENCES posa_cards(id) ON DELETE SET NULL,
recurly_adjustment_uuid VARCHAR(500),
recurly_adjustment_credit_uuid VARCHAR(500),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
ALTER TABLE sale_line_items ADD COLUMN posa_card_purchase_id VARCHAR(64) REFERENCES posa_card_purchases(id);
ALTER TABLE teachers ADD COLUMN retailer_id INTEGER REFERENCES retailers(id);
ALTER TABLE teachers ADD COLUMN joined_retailer_at TIMESTAMP;
ALTER TABLE retailers ADD jamkazam_rate NUMERIC (8, 2) DEFAULT 0.25;
ALTER TABLE retailers ADD COLUMN affiliate_partner_id INTEGER REFERENCES affiliate_partners(id);
ALTER TABLE lesson_bookings ADD COLUMN retailer_id INTEGER REFERENCES retailers(id);
ALTER TABLE teacher_payments ADD COLUMN retailer_id INTEGER REFERENCES retailers(id);
ALTER TABLE teacher_distributions ADD COLUMN retailer_id INTEGER REFERENCES retailers(id);
ALTER TABLE sales ALTER COLUMN user_id DROP NOT NULL;
ALTER TABLE sales ADD COLUMN retailer_id INTEGER REFERENCES retailers(id);
ALTER TABLE sale_line_items ADD COLUMN retailer_id INTEGER REFERENCES retailers(id);

View File

@ -280,6 +280,9 @@ require "jam_ruby/models/subject"
require "jam_ruby/models/band_search"
require "jam_ruby/import/tency_stem_mapping"
require "jam_ruby/models/jam_track_search"
require "jam_ruby/models/posa_card"
require "jam_ruby/models/posa_card_type"
require "jam_ruby/models/posa_card_purchase"
require "jam_ruby/models/gift_card"
require "jam_ruby/models/gift_card_purchase"
require "jam_ruby/models/gift_card_type"
@ -305,6 +308,8 @@ require "jam_ruby/models/affiliate_distribution"
require "jam_ruby/models/teacher_intent"
require "jam_ruby/models/school"
require "jam_ruby/models/school_invitation"
require "jam_ruby/models/retailer"
require "jam_ruby/models/retailer_invitation"
require "jam_ruby/models/teacher_instrument"
require "jam_ruby/models/teacher_subject"
require "jam_ruby/models/teacher_language"

View File

@ -1698,6 +1698,26 @@ module JamRuby
end
end
def invite_retailer_teacher(retailer_invitations)
@retailer_invitation = retailer_invitations
@retailer = retailer_invitations.retailer
email = retailer_invitations.email
@subject = "#{@retailer.owner.name} has sent you an invitation to join #{@retailer.name} on JamKazam"
unique_args = {:type => "invite_retailer_teacher"}
sendgrid_category "Notification"
sendgrid_unique_args :type => unique_args[:type]
sendgrid_recipients([email])
@suppress_user_has_account_footer = true
mail(:to => email, :subject => @subject) do |format|
format.text
format.html
end
end
def invite_school_teacher(school_invitation)
@school_invitation = school_invitation
@school = school_invitation.school
@ -1882,7 +1902,22 @@ module JamRuby
format.text
format.html { render :layout => "from_user_mailer" }
end
end
def retailer_customer_blast(email, retailer)
@retailer = retailer
@subject = "Check out our teachers at #{@retailer.name}"
unique_args = {:type => "retailer_customer_email"}
sendgrid_category "Notification"
sendgrid_unique_args :type => unique_args[:type]
sendgrid_recipients([email])
@suppress_user_has_account_footer = true
mail(:to => email, :subject => @subject) do |format|
format.text
format.html
end
end
end
end

View File

@ -0,0 +1,16 @@
<% provide(:title, @subject) %>
Hello <%= @retailer_invitation.first_name %> -
<p><%= @retailer.owner.first_name %> is using JamKazam to deliver online music lessons, and has sent you this invitation so that you can
register to take online music lessons with <%= @retailer.name %>. To accept this invitation, please click the SIGN UP NOW
button below, and follow the instructions on the web page to which you are taken. Thanks, and on behalf of
<%= @retailer.name %>, welcome to JamKazam!</p>
<br/>
<p>
<a href="<%= @retailer_invitation.generate_signup_url %>" style="margin: 8px 0 0 0;background-color: #ed3618;border: solid 1px #F27861;padding: 3px 10px;font-size: 12px;font-weight: 300;cursor: pointer;color: #FC9;text-decoration: none;line-height: 12px;text-align: center;">SIGN
UP NOW</a>
</p>
<br/>
<br/>
Best Regards,<br>
Team JamKazam

View File

@ -0,0 +1,18 @@
<% provide(:title, @subject) %>
Hello <%= @retailer_invitation.first_name %> -
<br/>
<p>
<%= @retailer.owner.first_name %> has set up <%= @retailer.name %> on JamKazam, enabling you to deliver online music
lessons in an amazing new way that really works. To accept this invitation, please click the SIGN UP NOW button below,
and follow the instructions on the web page to which you are taken. Thanks, and welcome to JamKazam!</p>
<br/>
<p>
<a href="<%= @retailer_invitation.generate_signup_url %>" style="margin: 8px 0 0 0;background-color: #ed3618;border: solid 1px #F27861;padding: 3px 10px;font-size: 12px;font-weight: 300;cursor: pointer;color: #FC9;text-decoration: none;line-height: 12px;text-align: center;">SIGN
UP NOW</a>
</p>
<br/>
<br/>
Best Regards,<br>
Team JamKazam

View File

@ -0,0 +1,11 @@
<% provide(:title, @subject) %>
Hello <%= @retailer_invitation.first_name %> -
<%= @retailer.owner.first_name %> has set up <%= @retailer.name %> on JamKazam, enabling you to deliver online music
lessons in an amazing new way that really works. To accept this invitation, please click the link below,
and follow the instructions on the web page to which you are taken. Thanks, and welcome to JamKazam!
<%= @retailer_invitation.generate_signup_url %>
Best Regards,
Team JamKazam

View File

@ -0,0 +1,8 @@
<% provide(:title, @subject) %>
<p>Click the link of each teacher's profile at <%= @retailer.name %> to find the best fit for you:</p>
<ul>
<% @retailer.teachers.each do |teacher| %>
<li><a href="<%= teacher.user.teacher_profile_url%>" style="color:#fc0"><%= teacher.user.name %></a></li>
<% end %>
</ul>

View File

@ -0,0 +1,6 @@
Check out each teacher's profile at <%= @retailer.name %> to find the best fit for you:
<% @retailer.teachers.each do |teacher| %>
<%= teacher.user.name %>: <%= teacher.user.teacher_profile_url%>
<% end %>

View File

@ -3,6 +3,7 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base
belongs_to :partner_user, :class_name => "JamRuby::User", :foreign_key => :partner_user_id, inverse_of: :affiliate_partner
has_one :school, class_name: "JamRuby::School"
has_one :retailer, class_name: "JamRuby::Retailer"
has_many :user_referrals, :class_name => "JamRuby::User", :foreign_key => :affiliate_referral_id
belongs_to :affiliate_legalese, :class_name => "JamRuby::AffiliateLegalese", :foreign_key => :legalese_id
has_many :sale_line_items, :class_name => 'JamRuby::SaleLineItem', foreign_key: :affiliate_referral_id
@ -94,6 +95,17 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base
oo.save
end
def self.create_from_retailer(retailer)
oo = AffiliatePartner.new
oo.partner_name = "Affiliate from Retailer #{retailer.id}"
oo.partner_user = retailer.owner
oo.entity_type = 'Other'
oo.retailer = retailer
oo.signed_at = nil
oo.save
end
def self.coded_id(code=nil)
self.where(:partner_code => code).limit(1).pluck(:id).first if code.present?
end

View File

@ -76,6 +76,7 @@ module JamRuby
end
def can_download?(some_user)
return false if some_user.nil?
claimed_recording = ClaimedRecording.find_by_user_id_and_recording_id(some_user.id, recording.id)
if claimed_recording

View File

@ -0,0 +1,126 @@
# represents the gift card you hold in your hand
module JamRuby
class PosaCard < ActiveRecord::Base
@@log = Logging.logger[PosaCard]
JAM_TRACKS_5 = 'jam_tracks_5'
JAM_TRACKS_10 = 'jam_tracks_10'
JAM_CLASS_4 = 'jam_tracks_4'
CARD_TYPES =
[
JAM_TRACKS_5,
JAM_TRACKS_10,
JAM_CLASS_4
]
belongs_to :user, class_name: "JamRuby::User"
belongs_to :retailer, class_name: "JamRuby::Retailer"
has_many :posa_card_purchases, class_name: 'JamRuby::PosaCardPurchase'
validates :card_type, presence: true, inclusion: {in: CARD_TYPES}
validates :code, presence: true, uniqueness: true
after_save :check_attributed
validate :already_activated
validate :retailer_set
validate :already_claimed
validate :user_set
validate :must_be_activated
validate :within_one_year
def already_activated
if activated_at && activated_at_was && activated_at_changed?
if retailer && retailer_id == retailer_id_was
self.errors.add(:activated_at, 'already activated. Please give card to customer. Thank you!')
else
self.errors.add(:activated_at, 'already activated by someone else. Please contact support@jamkaazm.com')
end
end
end
def within_one_year
if user && claimed_at && claimed_at_was && claimed_at_changed?
if !user.can_claim_posa_card
self.errors.add(:claimed_at, 'was within 1 year')
end
end
end
def already_claimed
if claimed_at && claimed_at_was && claimed_at_changed?
self.errors.add(:claimed_at, 'already claimed')
end
end
def retailer_set
if activated_at && !retailer
self.errors.add(:retailer, 'must be specified')
end
end
def user_set
if claimed_at && !user
self.errors.add(:user, 'must be specified')
end
end
def must_be_activated
if claimed_at && !activated_at
self.errors.add(:activated_at, 'must already be set')
end
end
def check_attributed
if user && user_id_changed?
if card_type == JAM_TRACKS_5
user.gifted_jamtracks += 5
elsif card_type == JAM_TRACKS_10
user.gifted_jamtracks += 10
elsif card_type == JAM_CLASS_4
user.jamclass_credits += 4
else
raise "unknown card type #{card_type}"
end
user.save!
end
end
def product_info
price = nil
plan_code = nil
if card_type == JAM_TRACKS_5
price = 9.99
plan_code = 'posa-jamtracks-5'
elsif card_type == JAM_TRACKS_10
price = 19.99
plan_code = 'posa-jatracks-10'
elsif card_type == JAM_CLASS_4
price = 49.99
plan_code = 'posa-jamclass-4'
else
raise "unknown card type #{card_type}"
end
{price: price, quantity: 1, marked_for_redeem: false, plan_code: plan_code}
end
def self.activate(posa_card, retailer)
Sale.posa_activate(posa_card, retailer)
end
def activate(retailer)
self.activated_at = Time.now
self.retailer = retailer
self.save
end
def claim(user)
self.user = user
self.claimed_at = Time.now
self.save
end
end
end

View File

@ -0,0 +1,18 @@
# reperesents the gift card you buy from the site (but physical gift card is modeled by GiftCard)
module JamRuby
class PosaCardPurchase < ActiveRecord::Base
@@log = Logging.logger[PosaCardPurchase]
attr_accessible :user, :posa_card_type
def name
posa_card_type.sale_display
end
# who purchased the card?
belongs_to :user, class_name: "JamRuby::User"
belongs_to :posa_card_type, class_name: "JamRuby::PosaCardType"
belongs_to :posa_card, class_name: 'JamRuby::PosaCard'
end
end

View File

@ -0,0 +1,84 @@
# reperesents the posa card you buy from the site
module JamRuby
class PosaCardType < ActiveRecord::Base
@@log = Logging.logger[PosaCardType]
PRODUCT_TYPE = 'PosaCardType'
JAM_TRACKS_5 = 'jam_tracks_5'
JAM_TRACKS_10 = 'jam_tracks_10'
JAM_CLASS_4 = 'jam_tracks_4'
CARD_TYPES =
[
JAM_TRACKS_5,
JAM_TRACKS_10,
JAM_CLASS_4
]
validates :card_type, presence: true, inclusion: {in: CARD_TYPES}
def self.jam_track_5
PosaCardType.find(JAM_TRACKS_5)
end
def self.jam_track_10
PosaCardType.find(JAM_TRACKS_10)
end
def self.jam_class_4
PosaCardType.find(JAM_CLASS_4)
end
def name
sale_display
end
def price
if card_type == JAM_TRACKS_5
10.00
elsif card_type == JAM_TRACKS_10
20.00
elsif card_type == JAM_CLASS_4
49.99
else
raise "unknown card type #{card_type}"
end
end
def sale_display
if card_type == JAM_TRACKS_5
'JamTracks Card (5)'
elsif card_type == JAM_TRACKS_10
'JamTracks Card (10)'
elsif card_type == JAM_TRACKS_10
'JamClass Card (4)'
else
raise "unknown card type #{card_type}"
end
end
def plan_code
if card_type == JAM_TRACKS_5
"jamtrack-posacard-5"
elsif card_type == JAM_TRACKS_10
"jamtrack-posacard-10"
elsif card_type == JAM_CLASS_4
"jamclass-posacard-4"
else
raise "unknown card type #{card_type}"
end
end
def sales_region
'Worldwide'
end
def to_s
sale_display
end
end
end

View File

@ -0,0 +1,124 @@
module JamRuby
class Retailer < ActiveRecord::Base
include HtmlSanitize
html_sanitize strict: [:name]
attr_accessor :updating_avatar, :password, :should_validate_password
attr_accessible :original_fpfile, :cropped_fpfile, :cropped_large_fpfile, :cropped_s3_path, :cropped_large_s3_path, :photo_url, :large_photo_url, :crop_selection
belongs_to :posa_cards, class_name: 'JamRuby::PosaCard'
belongs_to :user, class_name: ::JamRuby::User, inverse_of: :owned_retailer
belongs_to :affiliate_partner, class_name: "JamRuby::AffiliatePartner"
has_many :teachers, class_name: "JamRuby::Teacher"
has_many :retailer_invitations, class_name: 'JamRuby::RetailerInvitation'
has_many :teacher_payments, class_name: 'JamRuby::TeacherPayment'
has_many :teacher_distributions, class_name: 'JamRuby::TeacherDistribution'
has_many :sales, class_name: 'JamRuby::Sale'
has_many :sale_line_items, class_name: 'JamRuby::SaleLineItem'
validates :user, presence: true
validates :slug, presence: true
validates :enabled, inclusion: {in: [true, false]}
validates_length_of :password, minimum: 6, maximum: 100, :if => :should_validate_password
after_create :create_affiliate
before_save :stringify_avatar_info, :if => :updating_avatar
def create_affiliate
AffiliatePartner.create_from_retailer(self)
end
def encrypt(password)
BCrypt::Password.create(password, cost: 12).to_s
end
def matches_password(password)
BCrypt::Password.new(self.encrypted_password) == password
end
def update_from_params(params)
self.name = params[:name] if params[:name].present?
self.city = params[:city] if params[:city].present?
self.city = params[:state] if params[:state].present?
self.slug = params[:slug] if params[:slug].present?
if params[:password].present?
self.should_validate_password = true
self.password = params[:password]
self.encrypted_password = encrypt(params[:password])
end
self.save
end
def owner
user
end
def validate_avatar_info
if updating_avatar
# we want to mak sure that original_fpfile and cropped_fpfile seems like real fpfile info objects (i.e, json objects from filepicker.io)
errors.add(:original_fpfile, ValidationMessages::INVALID_FPFILE) if self.original_fpfile.nil? || self.original_fpfile["key"].nil? || self.original_fpfile["url"].nil?
errors.add(:cropped_fpfile, ValidationMessages::INVALID_FPFILE) if self.cropped_fpfile.nil? || self.cropped_fpfile["key"].nil? || self.cropped_fpfile["url"].nil?
errors.add(:cropped_large_fpfile, ValidationMessages::INVALID_FPFILE) if self.cropped_large_fpfile.nil? || self.cropped_large_fpfile["key"].nil? || self.cropped_large_fpfile["url"].nil?
end
end
def escape_filename(path)
dir = File.dirname(path)
file = File.basename(path)
"#{dir}/#{ERB::Util.url_encode(file)}"
end
def update_avatar(original_fpfile, cropped_fpfile, cropped_large_fpfile, crop_selection, aws_bucket)
self.updating_avatar = true
cropped_s3_path = cropped_fpfile["key"]
cropped_large_s3_path = cropped_large_fpfile["key"]
self.update_attributes(
:original_fpfile => original_fpfile,
:cropped_fpfile => cropped_fpfile,
:cropped_large_fpfile => cropped_large_fpfile,
:cropped_s3_path => cropped_s3_path,
:cropped_large_s3_path => cropped_large_s3_path,
:crop_selection => crop_selection,
:photo_url => S3Util.url(aws_bucket, escape_filename(cropped_s3_path), :secure => true),
:large_photo_url => S3Util.url(aws_bucket, escape_filename(cropped_large_s3_path), :secure => true)
)
end
def delete_avatar(aws_bucket)
User.transaction do
unless self.cropped_s3_path.nil?
S3Util.delete(aws_bucket, File.dirname(self.cropped_s3_path) + '/cropped.jpg')
S3Util.delete(aws_bucket, self.cropped_s3_path)
S3Util.delete(aws_bucket, self.cropped_large_s3_path)
end
return self.update_attributes(
:original_fpfile => nil,
:cropped_fpfile => nil,
:cropped_large_fpfile => nil,
:cropped_s3_path => nil,
:cropped_large_s3_path => nil,
:photo_url => nil,
:crop_selection => nil,
:large_photo_url => nil
)
end
end
def stringify_avatar_info
# fpfile comes in as a hash, which is a easy-to-use and validate form. However, we store it as a VARCHAR,
# so we need t oconvert it to JSON before storing it (otherwise it gets serialized as a ruby object)
# later, when serving this data out to the REST API, we currently just leave it as a string and make a JSON capable
# client parse it, because it's very rare when it's needed at all
self.original_fpfile = original_fpfile.to_json if !original_fpfile.nil?
self.cropped_fpfile = cropped_fpfile.to_json if !cropped_fpfile.nil?
self.crop_selection = crop_selection.to_json if !crop_selection.nil?
end
end
end

View File

@ -0,0 +1,89 @@
module JamRuby
class RetailerInvitation < ActiveRecord::Base
include HtmlSanitize
html_sanitize strict: [:note]
belongs_to :user, class_name: ::JamRuby::User
belongs_to :retailer, class_name: ::JamRuby::Retailer
validates :retailer, presence: true
validates :email, email: true
validates :invitation_code, presence: true
validates :accepted, inclusion: {in: [true, false]}
validates :first_name, presence: true
validates :last_name, presence: true
validate :retailer_has_name, on: :create
before_validation(on: :create) do
self.invitation_code = SecureRandom.urlsafe_base64 if self.invitation_code.nil?
end
def retailer_has_name
if retailer && retailer.name.blank?
errors.add(:retailer, "must have name")
end
end
def self.index(retailer, params)
limit = params[:per_page]
limit ||= 100
limit = limit.to_i
query = RetailerInvitation.where(retailer_id: retailer.id)
query = query.includes([:user, :retailer])
query = query.order('created_at')
query = query.where(accepted:false)
current_page = params[:page].nil? ? 1 : params[:page].to_i
next_page = current_page + 1
# will_paginate gem
query = query.paginate(:page => current_page, :per_page => limit)
if query.length == 0 # no more results
{query: query, next_page: nil}
elsif query.length < limit # no more results
{query: query, next_page: nil}
else
{query: query, next_page: next_page}
end
end
def self.create(current_user, specified_retailer, params)
invitation = RetailerInvitation.new
invitation.retailer = specified_retailer
invitation.email = params[:email]
invitation.first_name = params[:first_name]
invitation.last_name = params[:last_name]
if invitation.save
invitation.send_invitation
end
invitation
end
def send_invitation
UserMailer.invite_retailer_teacher(self).deliver_now
end
def generate_signup_url
"#{APP_CONFIG.external_root_url}/retailer/#{retailer.id}/teacher?invitation_code=#{self.invitation_code}"
end
def delete
self.destroy
end
def resend
send_invitation
end
end
end

View File

@ -5,17 +5,20 @@ module JamRuby
JAMTRACK_SALE = 'jamtrack'
LESSON_SALE = 'lesson'
POSA_SALE = 'posacard'
SOURCE_RECURLY = 'recurly'
SOURCE_IOS = 'ios'
belongs_to :retailer, class_name: 'JamRuby::Retailer'
belongs_to :user, class_name: 'JamRuby::User'
has_many :sale_line_items, class_name: 'JamRuby::SaleLineItem'
has_many :recurly_transactions, class_name: 'JamRuby::RecurlyTransactionWebHook', inverse_of: :sale, foreign_key: 'invoice_id', primary_key: 'recurly_invoice_id'
validates :order_total, numericality: {only_integer: false}
validates :user, presence: true
#validates :user
#validates :retailer
@@log = Logging.logger[Sale]
@ -215,6 +218,26 @@ module JamRuby
def self.post_sale_test_failure
return true
end
def self.posa_activate(posa_card, retailer)
sale = nil
Sale.transaction(:requires_new => true) do
posa_card.activate(retailer)
if !posa_card.errors.any?
sale = create_posa_sale(retailer, posa_card)
SaleLineItem.create_from_posa_card(sale, retailer, posa_card)
sale.save
end
end
{sale: sale}
end
# this is easy to make generic, but right now, it just purchases lessons
def self.purchase_lesson(charge, current_user, lesson_booking, lesson_package_type, lesson_session = nil, lesson_package_purchase = nil, force = false)
stripe_charge = nil
@ -223,43 +246,43 @@ module JamRuby
# everything needs to go into a transaction! If anything goes wrong, we need to raise an exception to break it
Sale.transaction(:requires_new => true) do
sale = create_lesson_sale(current_user)
sale = create_lesson_sale(current_user)
if sale.valid?
if sale.valid?
if lesson_booking
lesson_booking.current_lesson = lesson_session
lesson_booking.current_purchase = lesson_package_purchase
end
sale_line_item = SaleLineItem.create_from_lesson_package(current_user, sale, lesson_package_type, lesson_booking)
price_info = charge_stripe_for_lesson(charge, current_user, lesson_booking, lesson_package_type, sale_line_item, lesson_session, lesson_package_purchase, force)
post_sale_test_failure
if price_info[:purchase] && price_info[:purchase].errors.any?
purchase = price_info[:purchase]
raise ActiveRecord::Rollback
end
if !sale_line_item.valid?
raise "invalid sale_line_item object for user #{current_user.email} and lesson_booking #{lesson_booking.id}"
end
# sale.source = 'stripe'
sale.recurly_subtotal_in_cents = price_info[:subtotal_in_cents]
sale.recurly_tax_in_cents = price_info[:tax_in_cents]
sale.recurly_total_in_cents = price_info[:total_in_cents]
sale.recurly_currency = price_info[:currency]
sale.stripe_charge_id = price_info[:charge_id]
sale.save
stripe_charge = price_info[:charge]
purchase = price_info[:purchase]
else
# should not get out of testing. This would be very rare (i.e., from a big regression). Sale is always valid at this point.
puts "invalid sale object"
raise "invalid sale object"
if lesson_booking
lesson_booking.current_lesson = lesson_session
lesson_booking.current_purchase = lesson_package_purchase
end
sale_line_item = SaleLineItem.create_from_lesson_package(current_user, sale, lesson_package_type, lesson_booking)
price_info = charge_stripe_for_lesson(charge, current_user, lesson_booking, lesson_package_type, sale_line_item, lesson_session, lesson_package_purchase, force)
post_sale_test_failure
if price_info[:purchase] && price_info[:purchase].errors.any?
purchase = price_info[:purchase]
raise ActiveRecord::Rollback
end
if !sale_line_item.valid?
raise "invalid sale_line_item object for user #{current_user.email} and lesson_booking #{lesson_booking.id}"
end
# sale.source = 'stripe'
sale.recurly_subtotal_in_cents = price_info[:subtotal_in_cents]
sale.recurly_tax_in_cents = price_info[:tax_in_cents]
sale.recurly_total_in_cents = price_info[:total_in_cents]
sale.recurly_currency = price_info[:currency]
sale.stripe_charge_id = price_info[:charge_id]
sale.save
stripe_charge = price_info[:charge]
purchase = price_info[:purchase]
else
# should not get out of testing. This would be very rare (i.e., from a big regression). Sale is always valid at this point.
puts "invalid sale object"
raise "invalid sale object"
end
end
{sale: sale, stripe_charge: stripe_charge, purchase: purchase}
@ -634,6 +657,15 @@ module JamRuby
sale
end
def self.create_posa_sale(retailer, posa_card)
sale = Sale.new
sale.retailer = retailer
sale.sale_type = POSA_SALE # gift cards and jam tracks are sold with this type of sale
sale.order_total = 0
sale.save
sale
end
# this checks just jamtrack sales appropriately
def self.check_integrity_of_jam_track_sales
Sale.select([:total, :voided]).find_by_sql(

View File

@ -6,12 +6,14 @@ module JamRuby
JAMTRACK = 'JamTrack'
GIFTCARD = 'GiftCardType'
LESSON = 'LessonPackageType'
POSACARD = 'PosaCard'
belongs_to :sale, class_name: 'JamRuby::Sale'
belongs_to :jam_track, class_name: 'JamRuby::JamTrack'
belongs_to :jam_track_right, class_name: 'JamRuby::JamTrackRight'
belongs_to :gift_card, class_name: 'JamRuby::GiftCard'
belongs_to :lesson_package_purchase, class_name: 'JamRuby::LessonPackagePurchase'
belongs_to :retailer, class_name: 'JamRuby::Retailer'
# deprecated; use affiliate_distribution !!
belongs_to :affiliate_referral, class_name: 'JamRuby::AffiliatePartner', foreign_key: :affiliate_referral_id
@ -20,7 +22,7 @@ module JamRuby
has_many :recurly_transactions, class_name: 'JamRuby::RecurlyTransactionWebHook', inverse_of: :sale_line_item, foreign_key: 'subscription_id', primary_key: 'recurly_subscription_uuid'
validates :product_type, inclusion: {in: [JAMBLASTER, JAMCLOUD, JAMTRACK, GIFTCARD, LESSON]}
validates :product_type, inclusion: {in: [JAMBLASTER, JAMCLOUD, JAMTRACK, GIFTCARD, LESSON, POSACARD]}
validates :unit_price, numericality: {only_integer: false}
validates :quantity, numericality: {only_integer: true}
validates :free, numericality: {only_integer: true}
@ -128,6 +130,33 @@ module JamRuby
line_item
end
def self.create_from_posa_card(sale, retailer, posa_card)
product_info = posa_card.product_info
sale_line_item = SaleLineItem.new
sale_line_item.retailer = retailer
sale_line_item.product_type = POSACARD
sale_line_item.product_id = posa_card.id
sale_line_item.unit_price = product_info[:price]
sale_line_item.quantity = product_info[:quantity]
sale_line_item.free = product_info[:marked_for_redeem]
sale_line_item.sales_tax = nil
sale_line_item.shipping_handling = 0
sale_line_item.recurly_plan_code = product_info[:plan_code]
#referral_info = retailer.referral_info
#if referral_info
# sale_line_item.affiliate_distributions << AffiliateDistribution.create(retailer.affiliate_partner, referral_info[:fee_in_cents], sale_line_item)
# sale_line_item.affiliate_referral = retailer.affiliate_partner
# sale_line_item.affiliate_referral_fee_in_cents = referral_info[:fee_in_cents]
#end
sale.sale_line_items << sale_line_item
sale_line_item.save
sale_line_item
end
def self.create_from_shopping_cart(sale, shopping_cart, recurly_subscription_uuid, recurly_adjustment_uuid, recurly_adjustment_credit_uuid, instance = nil)
product_info = shopping_cart.product_info(instance)

View File

@ -22,6 +22,7 @@ module JamRuby
has_one :review_summary, :class_name => "JamRuby::ReviewSummary", as: :target
has_one :user, :class_name => 'JamRuby::User', foreign_key: :teacher_id
belongs_to :school, :class_name => "JamRuby::School", inverse_of: :teachers
belongs_to :retailer, :class_name => "JamRuby::Retailer", inverse_of: :teachers
validates :user, :presence => true
validates :biography, length: {minimum: 5, maximum: 4096}, :if => :validate_introduction

View File

@ -6,6 +6,7 @@ module JamRuby
belongs_to :lesson_session, class_name: "JamRuby::LessonSession"
belongs_to :lesson_package_purchase, class_name: "JamRuby::LessonPackagePurchase"
belongs_to :school, class_name: "JamRuby::School"
belongs_to :retailer, class_name: "JamRuby::Retailer"
validates :teacher, presence: true
validates :amount_in_cents, presence: true

View File

@ -5,6 +5,7 @@ module JamRuby
belongs_to :teacher_payment_charge, class_name: "JamRuby::TeacherPaymentCharge", foreign_key: :charge_id
has_one :teacher_distribution, class_name: "JamRuby::TeacherDistribution"
belongs_to :school, class_name: "JamRuby::School"
belongs_to :retailer, class_name: "JamRuby::Retailer"
def self.hourly_check

View File

@ -204,11 +204,13 @@ module JamRuby
has_many :taught_lessons, :class_name => "JamRuby::LessonSession", inverse_of: :teacher, foreign_key: :teacher_id
belongs_to :school, :class_name => "JamRuby::School", inverse_of: :students
has_one :owned_school, :class_name => "JamRuby::School", inverse_of: :user
has_one :owned_retailer, :class_name => "JamRuby::Retailer", inverse_of: :user
has_many :test_drive_package_choices, :class_name =>"JamRuby::TestDrivePackageChoice"
has_many :jamblasters_users, class_name: "JamRuby::JamblasterUser"
has_many :jamblasters, class_name: 'JamRuby::Jamblaster', through: :jamblasters_users
has_many :proposed_slots, class_name: 'JamRuby::LessonBookingSlot', inverse_of: :proposer, dependent: :destroy, foreign_key: :proposer_id
has_many :charges, class_name: 'JamRuby::Charge', dependent: :destroy
has_many :posa_cards, class_name: 'JamRuby::PosaCard', dependent: :destroy
before_save :default_anonymous_names
before_save :create_remember_token, :if => :should_validate_password?
@ -1157,6 +1159,7 @@ module JamRuby
user.terms_of_service = terms_of_service
user.reuse_card unless reuse_card.nil?
user.gifted_jamtracks = 0
user.jamclass_credits = 0
user.has_redeemable_jamtrack = true
user.is_a_student = !!student
user.is_a_teacher = !!teacher
@ -1304,9 +1307,18 @@ module JamRuby
# if a gift card value was passed in, then try to find that gift card and apply it to user
if gift_card
user.expecting_gift_card = true
found_gift_card = GiftCard.where(code: gift_card).where(user_id: nil).first
user.gift_cards << found_gift_card if found_gift_card
# first try posa card
posa_card = PosaCard.where(code: gift_card)
if posa_card
posa_card.claim(user)
user.posa_cards << posa_card
else
user.expecting_gift_card = true
found_gift_card = GiftCard.where(code: gift_card).where(user_id: nil).first
user.gift_cards << found_gift_card if found_gift_card
end
end
user.save
@ -1961,6 +1973,11 @@ module JamRuby
lesson_purchases.where('lesson_package_type_id in (?)', LessonPackageType.test_drive_package_ids).where('created_at > ?', APP_CONFIG.test_drive_wait_period_year.years.ago).count == 0
end
# validate if within waiting period
def can_claim_posa_card
posa_cards.where('card_type = ?', PosaCard::JAM_CLASS_4).where('claimed_at > ?', APP_CONFIG.jam_class_card_wait_period_year.years.ago).count == 0
end
def lessons_with_teacher(teacher)
taken_lessons.where(teacher_id: teacher.id)
end

View File

@ -898,6 +898,19 @@ FactoryGirl.define do
association :user, factory: :user
end
factory :posa_card, class: 'JamRuby::PosaCard' do
sequence(:code) { |n| n.to_s }
card_type JamRuby::PosaCardType::JAM_TRACKS_5
end
factory :posa_card_type, class: 'JamRuby::PosaCardType' do
card_type JamRuby::PosaCardType::JAM_TRACKS_5
end
factory :posa_card_purchase, class: 'JamRuby::PosaCardPurchase' do
association :user, factory: :user
end
factory :jamblaster, class: 'JamRuby::Jamblaster' do
association :user, factory: :user
@ -932,6 +945,22 @@ FactoryGirl.define do
accepted false
end
factory :retailer, class: 'JamRuby::Retailer' do
association :user, factory: :user
sequence(:name) { |n| "Dat Music Retailer" }
sequence(:slug) { |n| "retailer-#{n}" }
enabled true
end
factory :retailer_invitation, class: 'JamRuby::RetailerInvitation' do
association :retailer, factory: :retailer
note "hey come in in"
sequence(:email) { |n| "retail_person#{n}@example.com" }
sequence(:first_name) { |n| "FirstName" }
sequence(:last_name) { |n| "LastName" }
accepted false
end
factory :lesson_booking_slot, class: 'JamRuby::LessonBookingSlot' do
factory :lesson_booking_slot_single do
slot_type 'single'

View File

@ -0,0 +1,93 @@
require 'spec_helper'
describe PosaCard do
let(:user) {FactoryGirl.create(:user)}
let(:card) {FactoryGirl.create(:posa_card)}
let(:card2) {FactoryGirl.create(:posa_card)}
let(:retailer) {FactoryGirl.create(:retailer)}
it "created by factory" do
card.touch
end
describe "activated" do
it "succeeds" do
card.activate(retailer)
card.errors.any?.should be false
card.activated_at.should_not be_nil
card.retailer.should eql retailer
end
it "cant be re-activated" do
card.activate(retailer)
Timecop.travel(Time.now + 1)
card.activate(retailer)
card.errors.any?.should be true
card.errors[:activated_at].should eql ['already activated. Please give card to customer. Thank you!']
end
it "must have retailer" do
card.activate(nil)
card.errors.any?.should be true
card.errors[:retailer].should eql ['must be specified']
end
end
describe "claim" do
it "succeeds" do
card.activate(retailer)
card.claim(user)
card.errors.any?.should be false
card.claimed_at.should_not be_nil
card.user.should eql user
end
it "must be already activated" do
card.claim(user)
card.errors.any?.should be true
card.errors[:activated_at].should eql ['must already be set']
end
it "cant be re-claimed" do
card.activate(retailer)
card.claim(user)
Timecop.travel(Time.now + 1)
card.claim(user)
card.errors.any?.should be true
card.errors[:claimed_at].should eql ['already claimed']
end
it "must have user" do
card.activate(retailer)
card.claim(nil)
card.errors.any?.should be true
card.errors[:user].should eql ['must be specified']
end
it "can't be within one year" do
card.activate(retailer)
card.claim(user)
card2.activate(retailer)
card2.claim(user)
card2.errors.any?.should be true
card2.errors[:user].should eql ['was within 1 year']
end
end
end

View File

@ -0,0 +1,33 @@
require 'spec_helper'
describe RetailerInvitation do
let(:retailer) {FactoryGirl.create(:retailer)}
it "created by factory" do
FactoryGirl.create(:retailer_invitation)
end
it "created by method" do
RetailerInvitation.create(retailer.user, retailer, {first_name: "Bobby", last_name: "Jimes", email: 'somewhere@jamkazam.com'})
end
describe "index" do
it "works" do
RetailerInvitation.index(retailer, {})[:query].count.should eql 0
FactoryGirl.create(:retailer_invitation)
RetailerInvitation.index(retailer, {})[:query].count.should eql 0
RetailerInvitation.index(retailer, {})[:query].count.should eql 0
FactoryGirl.create(:retailer_invitation, retailer: retailer, )
RetailerInvitation.index(retailer, {})[:query].count.should eql 1
FactoryGirl.create(:retailer_invitation, retailer: retailer, )
RetailerInvitation.index(retailer, {})[:query].count.should eql 2
end
end
end

View File

@ -0,0 +1,41 @@
require 'spec_helper'
describe Retailer do
it "created by factory" do
FactoryGirl.create(:retailer)
end
it "has correct associations" do
retailer = FactoryGirl.create(:retailer)
retailer.should eql retailer.user.owned_retailer
teacher = FactoryGirl.create(:teacher, retailer: retailer)
retailer.reload
retailer.teachers.to_a.should eql [teacher]
teacher.retailer.should eql retailer
end
it "updates" do
retailer = FactoryGirl.create(:retailer)
retailer.update_from_params({name: 'hahah'})
retailer.errors.any?.should be false
end
it "updates password" do
retailer = FactoryGirl.create(:retailer)
retailer.update_from_params({name: 'hahah', password: 'abc'})
retailer.errors.any?.should be true
retailer.errors[:password].should eql ['is too short (minimum is 6 characters)']
retailer.update_from_params({name: 'hahah', password: 'abcdef'})
retailer.errors.any?.should be false
retailer.matches_password('abcdef').should be true
retailer = Retailer.find_by_id(retailer.id)
retailer.matches_password('abcdef').should be true
end
end

View File

@ -8,6 +8,8 @@ describe Sale do
let(:jam_track2) { FactoryGirl.create(:jam_track) }
let(:jam_track3) { FactoryGirl.create(:jam_track) }
let(:gift_card) { GiftCardType.jam_track_5 }
let(:posa_card) {FactoryGirl.create(:posa_card)}
let(:retailer) {FactoryGirl.create(:retailer)}
after(:each) {
Timecop.return
@ -27,6 +29,39 @@ describe Sale do
sale_line_item.product_id.should eq(jamtrack.id)
end
describe "posa_cards" do
it "works" do
posa_card.card_type.should eql PosaCard::JAM_TRACKS_5
result = Sale.posa_activate(posa_card, retailer)
posa_card.errors.any?.should be false
posa_card.activated_at.should_not be_nil
sale = result[:sale]
sale = Sale.find(sale.id)
sale.sale_line_items.count.should eql 1
sale_line_item = sale.sale_line_items.first
sale.retailer.should eql retailer
sale.sale_type.should eql Sale::POSA_SALE
sale_line_item.retailer.should eql retailer
sale_line_item.unit_price.should eql 9.99 #
sale_line_item.quantity.should eql 1
end
it "already activated" do
result = Sale.posa_activate(posa_card, retailer)
posa_card.activated_at.should_not be_nil
posa_card.errors.any?.should be false
sale = result[:sale]
result2 = Sale.posa_activate(posa_card, retailer)
posa_card.activated_at.should_not be_nil
posa_card.errors.any?.should be true
result2[:sale].should be_nil
posa_card.errors[:activated_at].should eq ["already activated. Please give card to customer. Thank you!"]
end
end
describe "index" do
it "empty" do
result = Sale.index(user)
@ -941,6 +976,7 @@ describe Sale do
r.voided.to_i.should eq(1)
end
end
end

View File

@ -210,6 +210,22 @@ describe "RenderMailers", :slow => true do
end
end
describe "Retailer emails" do
let(:retailer) {FactoryGirl.create(:retailer)}
before(:each) do
UserMailer.deliveries.clear
end
after(:each) do
UserMailer.deliveries.length.should == 1
# NOTE! we take the second email, because the act of creating the InvitedUser model
# sends an email too, before our it {} block runs. This is because we have an InvitedUserObserver
mail = UserMailer.deliveries[0]
save_emails_to_disk(mail, @filename)
end
it {@filename="retailer_customer_blast"; UserMailer.retailer_customer_blast('seth@jamkazam.com', retailer).deliver_now}
end
describe "InvitedUserMailer emails" do
let(:user2) { FactoryGirl.create(:user) }

View File

@ -294,6 +294,10 @@ def app_config
1
end
def jam_class_card_wait_period_year
1
end
def check_bounced_emails
false
end

View File

@ -80,6 +80,7 @@
affiliate_earnings: (userDetail.affiliate_earnings / 100).toFixed(2),
affiliate_referral_count: userDetail.affiliate_referral_count,
owns_school: !!userDetail.owned_school_id,
owns_retailer: !!userDetail.owned_retailer_id,
webcamName: webcamName
} , { variable: 'data' }));

View File

@ -2367,8 +2367,6 @@
})
}
function updateSchoolAvatar(options) {
var id = getId(options);
@ -2488,6 +2486,171 @@
});
}
function deleteSchoolTeacher(options) {
var id = getId(options);
return $.ajax({
type: "DELETE",
url: "/api/schools/" + id + '/teachers/' + options.teacher_id,
dataType: "json",
contentType: 'application/json'
});
}
function getRetailer(options) {
var id = getId(options);
return $.ajax({
type: "GET",
url: "/api/retailers/" + id,
dataType: "json",
contentType: 'application/json'
});
}
function updateRetailer(options) {
var id = getId(options);
return $.ajax({
type: "POST",
url: '/api/retailers/' + id,
dataType: "json",
contentType: 'application/json',
data: JSON.stringify(options)
})
}
function updateRetailerAvatar(options) {
var id = getId(options);
var original_fpfile = options['original_fpfile'];
var cropped_fpfile = options['cropped_fpfile'];
var cropped_large_fpfile = options['cropped_large_fpfile'];
var crop_selection = options['crop_selection'];
logger.debug(JSON.stringify({
original_fpfile : original_fpfile,
cropped_fpfile : cropped_fpfile,
cropped_large_fpfile : cropped_large_fpfile,
crop_selection : crop_selection
}));
var url = "/api/retailers/" + id + "/avatar";
return $.ajax({
type: "POST",
dataType: "json",
url: url,
contentType: 'application/json',
processData:false,
data: JSON.stringify({
original_fpfile : original_fpfile,
cropped_fpfile : cropped_fpfile,
cropped_large_fpfile : cropped_large_fpfile,
crop_selection : crop_selection
})
});
}
function deleteRetailerAvatar(options) {
var id = getId(options);
var url = "/api/retailers/" + id + "/avatar";
return $.ajax({
type: "DELETE",
dataType: "json",
url: url,
contentType: 'application/json',
processData:false
});
}
function generateRetailerFilePickerPolicy(options) {
var id = getId(options);
var handle = options && options["handle"];
var convert = options && options["convert"]
var url = "/api/retailers/" + id + "/filepicker_policy";
return $.ajax(url, {
data : { handle : handle, convert: convert },
dataType : 'json'
});
}
function listRetailerInvitations(options) {
var id = getId(options);
return $.ajax({
type: "GET",
url: "/api/retailers/" + id + '/invitations?' + $.param(options) ,
dataType: "json",
contentType: 'application/json'
});
}
function createRetailerInvitation(options) {
var id = getId(options);
return $.ajax({
type: "POST",
url: "/api/retailers/" + id + '/invitations?' + $.param(options) ,
dataType: "json",
contentType: 'application/json',
data: JSON.stringify(options)
});
}
function deleteRetailerInvitation(options) {
var id = getId(options);
return $.ajax({
type: "DELETE",
url: "/api/retailers/" + id + '/invitations/' + options.invitation_id,
dataType: "json",
contentType: 'application/json'
});
}
function resendRetailerInvitation(options) {
var id = getId(options);
return $.ajax({
type: "POST",
url: "/api/retaiers/" + id + '/invitations/' + options.invitation_id + '/resend',
dataType: "json",
contentType: 'application/json',
data: JSON.stringify(options)
});
}
function deleteRetailerStudent(options) {
var id = getId(options);
return $.ajax({
type: "DELETE",
url: "/api/retailers/" + id + '/students/' + options.student_id,
dataType: "json",
contentType: 'application/json'
});
}
function deleteRetailerTeacher(options) {
var id = getId(options);
return $.ajax({
type: "DELETE",
url: "/api/retailers/" + id + '/teachers/' + options.teacher_id,
dataType: "json",
contentType: 'application/json'
});
}
function listTeacherDistributions(options) {
if(!options) {
@ -2502,18 +2665,6 @@
});
}
function deleteSchoolTeacher(options) {
var id = getId(options);
return $.ajax({
type: "DELETE",
url: "/api/schools/" + id + '/teachers/' + options.teacher_id,
dataType: "json",
contentType: 'application/json'
});
}
function createReview(options) {
return $.ajax({
@ -2545,6 +2696,45 @@
})
}
function posaActivate(options) {
var slug = options.slug
delete options.slug
return $.ajax({
type: "POST",
url: '/api/posa/' + slug + '/activate',
dataType: "json",
contentType: 'application/json',
data: JSON.stringify(options),
})
}
function posaClaim(options) {
return $.ajax({
type: "POST",
url: '/api/posa/claim',
dataType: "json",
contentType: 'application/json',
data: JSON.stringify(options),
})
}
function sendRetailerCustomerEmail(options) {
options = options || {}
var retailerId = options.retailer
delete options.retailer
return $.ajax({
type: 'POST',
url: '/api/retailers/' + retailerId + '/customer_email',
dataType: 'json',
contentType: 'application/json',
data: JSON.stringify(options)
})
}
function initialize() {
return self;
}
@ -2768,11 +2958,25 @@
this.resendSchoolInvitation = resendSchoolInvitation;
this.deleteSchoolTeacher = deleteSchoolTeacher;
this.deleteSchoolStudent = deleteSchoolStudent;
this.getRetailer = getRetailer;
this.updateRetailer = updateRetailer;
this.updateRetailerAvatar = updateRetailerAvatar;
this.deleteRetailerAvatar = deleteRetailerAvatar;
this.generateRetailerFilePickerPolicy = generateRetailerFilePickerPolicy;
this.listRetailerInvitations = listRetailerInvitations;
this.createRetailerInvitation = createRetailerInvitation;
this.deleteRetailerInvitation = deleteRetailerInvitation;
this.resendRetailerInvitation = resendRetailerInvitation;
this.deleteRetailerTeacher = deleteRetailerTeacher;
this.deleteRetailerStudent = deleteRetailerStudent;
this.listTeacherDistributions = listTeacherDistributions;
this.lessonStartTime = lessonStartTime;
this.createReview = createReview;
this.askSearchHelp = askSearchHelp;
this.ratingDecision = ratingDecision;
this.posaActivate = posaActivate;
this.posaClaim = posaClaim;
this.sendRetailerCustomerEmail = sendRetailerCustomerEmail;
return this;
};
})(window,jQuery);

View File

@ -8,6 +8,7 @@
//= require ./react-components/stores/UserActivityStore
//= require ./react-components/stores/LessonTimerStore
//= require ./react-components/stores/SchoolStore
//= require ./react-components/stores/RetailerStore
//= require ./react-components/stores/JamBlasterStore
//= require ./react-components/stores/StripeStore
//= require ./react-components/stores/AvatarStore

View File

@ -0,0 +1,390 @@
context = window
rest = context.JK.Rest()
logger = context.JK.logger
AppStore = context.AppStore
SchoolActions = context.RetailerActions
SchoolStore = context.RetailerStore
UserStore = context.UserStore
profileUtils = context.JK.ProfileUtils
@AccountRetailerScreen = React.createClass({
mixins: [
ICheckMixin,
Reflux.listenTo(AppStore, "onAppInit"),
Reflux.listenTo(RetailerStore, "onRetailerChanged")
Reflux.listenTo(UserStore, "onUserChanged")
]
shownOnce: false
screenVisible: false
TILE_ACCOUNT: 'account'
TILE_TEACHERS: 'teachers'
TILE_SALES: 'sales'
TILE_AGREEMENT: 'agreement'
TILES: ['account', 'teachers', 'sales', 'agreement']
onAppInit: (@app) ->
@app.bindScreen('account/retailer', {beforeShow: @beforeShow, afterShow: @afterShow, beforeHide: @beforeHide})
onSchoolChanged: (retailerState) ->
@setState(retailerState)
onUserChanged: (userState) ->
@noRetailerCheck(userState?.user)
@setState({user: userState?.user})
componentDidMount: () ->
@checkboxes = [{selector: 'input.slot-decision', stateKey: 'slot-decision'}]
@root = $(@getDOMNode())
@iCheckify()
componentDidUpdate: () ->
@iCheckify()
checkboxChanged: (e) ->
checked = $(e.target).is(':checked')
value = $(e.target).val()
#@setState({userSchedulingComm: value})
beforeHide: (e) ->
#ProfileActions.viewTeacherProfileDone()
@screenVisible = false
beforeShow: (e) ->
noRetailerCheck: (user) ->
if user?.id? && @screenVisible
if !user.owned_retailer_id?
window.JK.Banner.showAlert("You are not the owner of a retailer in our systems. If you are, please contact support@jamkazam.com and we'll update your account.")
return false
else
if !@shownOnce
@shownOnce = true
RetailerActions.refresh(user.owned_retailer_id)
return true
else
return false
afterShow: (e) ->
@screenVisible = true
logger.debug("AccountRetailerScreen: afterShow")
logger.debug("after show", @state.user)
@noRetailerCheck(@state.user)
getInitialState: () ->
{
retailer: null,
user: null,
selected: 'account',
updateErrors: null,
retailerName: null,
teacherInvitations: null,
updating: false
}
nameValue: () ->
if this.state.retailerName?
this.state.retailerName
else
this.state.retailer.name
nameChanged: (e) ->
$target = $(e.target)
val = $target.val()
@setState({retailerName: val})
onCancel: (e) ->
e.preventDefault()
context.location.href = '/client#/account'
onUpdate: (e) ->
e.preventDefault()
if this.state.updating
return
name = @root.find('input[name="name"]').val()
@setState(updating: true)
rest.updateRetailer({
id: this.state.retailer.id,
name: name,
}).done((response) => @onUpdateDone(response)).fail((jqXHR) => @onUpdateFail(jqXHR))
onUpdateDone: (response) ->
@setState({retailer: response, retailerName: null, updateErrors: null, updating: false})
@app.layout.notify({title: "update success", text: "Your retailer information has been successfully updated"})
onUpdateFail: (jqXHR) ->
handled = false
@setState({updating: false})
if jqXHR.status == 422
errors = JSON.parse(jqXHR.responseText)
handled = true
@setState({updateErrors: errors})
if !handled
@app.ajaxError(jqXHR, null, null)
inviteTeacher: () ->
@app.layout.showDialog('invite-retailer-user', {d1: true})
resendInvitation: (id, e) ->
e.preventDefault()
rest.resendRetailerInvitation({
id: this.state.retailer.id, invitation_id: id
}).done((response) => @resendInvitationDone(response)).fail((jqXHR) => @resendInvitationFail(jqXHR))
resendInvitationDone: (response) ->
@app.layout.notify({title: 'invitation resent', text: 'Invitation was resent to ' + response.email})
resendInvitationFail: (jqXHR) ->
@app.ajaxError(jqXHR)
deleteInvitation: (id, e) ->
e.preventDefault()
rest.deleteRetailerInvitation({
id: this.state.retailer.id, invitation_id: id
}).done((response) => @deleteInvitationDone(id, response)).fail((jqXHR) => @deleteInvitationFail(jqXHR))
deleteInvitationDone: (id, response) ->
context.RetailerActions.deleteInvitation(id)
deleteInvitationFail: (jqXHR) ->
@app.ajaxError(jqXHR)
removeFromRetailer: (id, isTeacher, e) ->
if isTeacher
rest.deleteRetailerTeacher({id: this.state.retailer.id, teacher_id: id}).done((response) => @removeFromRetailerDone(response)).fail((jqXHR) => @removeFromSchoolFail(jqXHR))
removeFromRetailerDone: (retailer) ->
context.JK.Banner.showNotice("User removed", "User was removed from your retailer.")
context.RetailerActions.updateRetailer(retailer)
removeFromSchoolFail: (jqXHR) ->
@app.ajaxError(jqXHR)
renderUser: (user, isTeacher) ->
photo_url = user.photo_url
if !photo_url?
photo_url = '/assets/shared/avatar_generic.png'
`<div className="retailer-user">
<div className="avatar">
<img src={photo_url} />
</div>
<div className="usersname">
{user.name}
</div>
<div className="user-actions">
<a onClick={this.removeFromRetailer.bind(this, user.id, isTeacher)}>remove from retailer</a>
</div>
</div>`
renderInvitation: (invitation) ->
`<div key={invitation.id} className="retailer-invitation">
<table>
<tbody>
<td className="description">{invitation.first_name} {invitation.last_name}</td>
<td className="message">
<div className="detail-block">has not yet accepted invitation<br/>
<a className="resend" onClick={this.resendInvitation.bind(this, invitation.id)}>resend invitation</a>
<a className="delete" onClick={this.deleteInvitation.bind(this, invitation.id)}>delete</a>
</div>
</td>
</tbody>
</table>
</div>`
renderTeachers: () ->
teachers = []
if this.state.retailer.teachers? && this.state.retailer.teachers.length > 0
for teacher in this.state.retailer.teachers
teachers.push(@renderUser(teacher.user, true))
else
teachers = `<p>No teachers</p>`
teachers
renderTeacherInvitations: () ->
invitations = []
if this.state.teacherInvitations? && this.state.teacherInvitations.length > 0
for invitation in this.state.teacherInvitations
invitations.push(@renderInvitation(invitation))
else
invitations = `<p>No pending invitations</p>`
invitations
mainContent: () ->
if !@state.user? || !@state.retailer?
`<div className="loading">Loading...</div>`
else if @state.selected == @TILE_ACCOUNT
@account()
else if @state.selected == @TILE_TEACHERS
@teachers()
else if @state.selected == @TILE_SALES
@earnings()
else if @state.selected == @TILE_AGREEMENT
@agreement()
else
@account()
account: () ->
ownerEmail = this.state.school.owner.email
correspondenceEmail = this.state.school.correspondence_email
correspondenceDisabled = !@isSchoolManaged()
nameErrors = context.JK.reactSingleFieldErrors('name', @state.updateErrors)
correspondenceEmailErrors = context.JK.reactSingleFieldErrors('correspondence_email', @state.updateErrors)
nameClasses = classNames({name: true, error: nameErrors?, field: true})
correspondenceEmailClasses = classNames({
correspondence_email: true,
error: correspondenceEmailErrors?,
field: true
})
cancelClasses = { "button-grey": true, "cancel" : true, disabled: this.state.updating }
updateClasses = { "button-orange": true, "update" : true, disabled: this.state.updating }
`<div className="account-block info-block">
<div className={nameClasses}>
<label>School Name:</label>
<input type="text" name="name" value={this.nameValue()} onChange={this.nameChanged}/>
{nameErrors}
</div>
<div className="field logo">
<label>School Logo:</label>
<AvatarEditLink target={this.state.school} target_type="school"/>
</div>
<h4>Management Preference</h4>
<div className="field scheduling_communication">
<div className="scheduling_communication school">
<input type="radio" name="scheduling_communication" readOnly={true} value="school"
checked={this.isSchoolManaged()}/><label>School owner will manage scheduling of student lessons sourced
by JamKazam</label>
</div>
<div className="scheduling_communication teacher">
<input type="radio" name="scheduling_communication" readOnly={true} value="teacher"
checked={!this.isSchoolManaged()}/><label>Teacher will manage scheduling of lessons</label>
</div>
</div>
<div className={correspondenceEmailClasses}>
<label>Correspondence Email:</label>
<input type="text" name="correspondence_email" placeholder={ownerEmail} defaultValue={correspondenceEmail}
disabled={correspondenceDisabled}/>
<div className="hint">All emails relating to lesson scheduling will go to this email if school owner manages
scheduling.
</div>
{correspondenceEmailErrors}
</div>
<h4>Payments</h4>
<div className="field stripe-connect">
<StripeConnect purpose='school' user={this.state.user}/>
</div>
<div className="actions">
<a className={classNames(cancelClasses)} onClick={this.onCancel}>CANCEL</a>
<a className={classNames(updateClasses)} onClick={this.onUpdate}>UPDATE</a>
</div>
</div>`
teachers: () ->
teachers = @renderTeachers()
teacherInvitations = @renderTeacherInvitations()
`<div className="members-block info-block">
<div className="column column-left">
<div>
<h3>teachers:</h3>
<a onClick={this.inviteTeacher} className="button-orange invite-dialog">INVITE TEACHER</a>
<br className="clearall" />
</div>
<div className="teacher-invites">
{teacherInvitations}
</div>
<div className="teachers">
{teachers}
</div>
</div>
</div>`
earnings: () ->
`<div className="earnings-block info-block">
<p>Coming soon</p>
</div>`
agreement: () ->
`<div className="agreement-block info-block">
<p>The agreement between your music school and JamKazam is part of JamKazam's terms of service. You can find the
complete terms of service <a href="/corp/terms" target="_blank">here</a>. And you can find the section that is
most specific to the retailer terms <a href="/corp/terms" target="_blank">here</a>.</p>
</div>`
selectionMade: (selection, e) ->
e.preventDefault()
@setState({selected: selection})
createTileLink: (i, tile) ->
active = this.state.selected == tile
classes = classNames({last: i == @TILES.length - 1, activeTile: active})
return `<div key={i} className="profile-tile"><a className={classes}
onClick={this.selectionMade.bind(this, tile)}>{tile}</a></div>`
onCustomBack: (customBack, e) ->
e.preventDefault()
context.location = customBack
render: () ->
mainContent = @mainContent()
profileSelections = []
for tile, i in @TILES
profileSelections.push(@createTileLink(i, tile, profileSelections))
profileNav = `<div className="profile-nav">
{profileSelections}
</div>`
`<div className="content-body-scroller">
<div className="profile-header profile-head">
<div className="store-header">retailer:</div>
{profileNav}
<div className="clearall"></div>
</div>
<div className="profile-body">
<div className="profile-wrapper">
<div className="main-content">
{mainContent}
<br />
</div>
</div>
</div>
</div>`
})

View File

@ -44,9 +44,9 @@ context = window
rest.createSchoolInvitation({id: school.id, as_teacher: this.state.teacher, email: email, last_name: lastName, first_name: firstName }).done((response) => @createDone(response)).fail((jqXHR) => @createFail(jqXHR))
createDone:(response) ->
context.SchoolActions.addInvitation(@state.teacher, response)
context.SchoolActions.addInvitation(response)
context.JK.Banner.showNotice("invitation sent", "Your invitation has been sent!")
@app.layout.closeDialog('invite-school-user')
@app.layout.closeDialog('invite-retailer-user')
createFail: (jqXHR) ->

View File

@ -0,0 +1,9 @@
context = window
@RetailerActions = Reflux.createActions({
refresh: {},
addInvitation: {},
deleteInvitation: {}
updateRetailer: {}
})

View File

@ -0,0 +1,102 @@
context = window
rest = context.JK.Rest()
@JamClassRetailerLandingBottomPage = React.createClass({
render: () ->
`<div className="top-container">
<div className="row awesome jam-class teachers">
<h2 className="awesome">How Our Retail Partner Program Can Help Your Store</h2>
<p>By simply adding our free countertop display of music lesson and JamTracks gift cards to your store, you can
instantly be enabled to offer an amazing deal on music lessons to every customer who buys an instrument from
your store. Students can connect with amazing teachers anywhere in the country, avoid the time and hassle of
travel to/from lessons, and even record lessons to avoid forgetting what theyve learned. Even if your store
offers lessons through in-store teachers, often your customers may live too far away or may not want to deal
with travel to get back to your store for lessons, and this is a great service you can offer, while earning
about $100 per year per student for students who stick with their lessons, in addition to 30% on the initial
gift card sale.</p>
<p>And for more advanced musicians who frequently wander into your store just to look around because they have
the bug, JamTracks are a terrific product you can sell to both beginner and advanced musicians. JamTracks
are full multitrack recordings of more than 4,000 popular songs. Your customers can solo a part they want to
play to hear all its nuances, mute that part out to play with the rest of the band, slow it down to practice,
record themselves playing along and share it with their friends on YouTube, and more. Youll earn 30% margins
on JamTracks gift cards as well.</p>
<p>Watch the videos below that explain and show our JamClass online music lesson service and our JamTracks
products in more detail.</p>
<div className="testimonials jam-class retailer">
<h3>JamClass Kudos</h3>
<div className="testimonial">
<img src="/assets/landing/Julie Bonk - Jam Class - Speech Bubble.png"
className="testimonial-speech-bubble"/>
<img src="/assets/landing/Julie Bonk - Avatar.png" className="testimonial-avatar"/>
<h4><strong>Julie Bonk</strong></h4>
<div className="testiminal-background">
Oft-recorded pianist, teacher, mentor to Grammy winner Norah Jones and Scott Hoying of Pentatonix
</div>
</div>
<div className="testimonial">
<img src="/assets/landing/Carl Brown - Jam Class - Speech Bubble.png"
className="testimonial-speech-bubble"/>
<img src="/assets/landing/Carl Brown - Avatar.png" className="testimonial-avatar"/>
<a rel="external" href="https://www.youtube.com/channel/UCvnfBBzEizi1T5unOXNCxdQ"><img
src="/assets/landing/Carl Brown - YouTube.png" className="testimonial-youtube"/></a>
<h4><strong>Carl Brown</strong> of GuitarLessions365</h4>
</div>
<div className="testimonial">
<img src="/assets/landing/Justin Pierce - Jam Class - Speech Bubble.png"
className="testimonial-speech-bubble"/>
<img src="/assets/landing/Justin Pierce - Avatar.png" className="testimonial-avatar"/>
<h4><strong>Justin Pierce</strong></h4>
<div className="testiminal-background">
Masters degree in jazz studies, performer in multiple bands, saxophone instructor
</div>
</div>
<div className="testimonial">
<img src="/assets/landing/Dave Sebree - Jam Class - Speech Bubble.png"
className="testimonial-speech-bubble"/>
<img src="/assets/landing/Dave Sebree - Avatar.png" className="testimonial-avatar"/>
<h4><strong>Dave Sebree</strong></h4>
<div className="testiminal-background">
Founder of Austin School of Music, Gibson-endorsed guitarist, touring musician
</div>
</div>
<div className="testimonial">
<img src="/assets/landing/Sara Nelson - Jam Class - Speech Bubble.png"
className="testimonial-speech-bubble"/>
<img src="/assets/landing/Sara Nelson - Avatar.png" className="testimonial-avatar"/>
<h4><strong>Sara Nelson</strong></h4>
<div className="testiminal-background">
Cellist for Austin Lyric Opera, frequently recorded with major artists
</div>
</div>
</div>
</div>
<div className="video-section">
<div className="video-wrapper">
<div className="video-container">
<iframe src="https://www.youtube.com/v/Y9m16G_86oU" frameborder="0" allowfullscreen="allowfullscreen"/>
</div>
</div>
</div>
<div className="video-section second">
<div className="video-wrapper">
<div className="video-container">
<iframe src="https://www.youtube.com/v/-rHfJggbgqk" frameborder="0" allowfullscreen="allowfullscreen"/>
</div>
</div>
</div>
<br className="clearall"/>
</div>`
})

View File

@ -0,0 +1,171 @@
context = window
rest = context.JK.Rest()
@JamClassRetailerLandingPage = React.createClass({
render: () ->
loggedIn = context.JK.currentUserId?
if this.state.done
ctaButtonText = 'sending you in...'
else if this.state.processing
ctaButtonText = 'hold on...'
else
if loggedIn
ctaButtonText = "SIGN UP"
else
ctaButtonText = "SIGN UP"
if loggedIn
register = `<button className={classNames({'cta-button' : true, 'processing': this.state.processing})}
onClick={this.ctaClick}>{ctaButtonText}</button>`
else
if this.state.loginErrors?
for key, value of this.state.loginErrors
break
errorText = context.JK.getFullFirstError(key, this.state.loginErrors,
{email: 'Email', password: 'Password', 'terms_of_service': 'The terms of service'})
register = `<div className="register-area jam-class">
<div className={classNames({'errors': true, 'active': this.state.loginErrors})}>
{errorText}
</div>
<form className="jamtrack-signup-form">
<label>Email: </label><input type="text" name="email"/>
<label>Password: </label><input type="password" name="password"/>
<div className="clearall"/>
<input className="terms-checkbox" type="checkbox" name="terms"/><label className="terms-help">I have read and
agree to the JamKazam <a href="/corp/terms" onClick={this.termsClicked}>terms of service</a></label>
<div className="clearall"/>
<button className={classNames({'cta-button' : true, 'processing': this.state.processing})}
onClick={this.ctaClick}>{ctaButtonText}</button>
</form>
</div>`
`<div className="top-container">
<div className="full-row name-and-artist">
<div>
<img className="jam-class-teacher" width="375" height="215" src="/assets/landing/jam_class.png"
alt="teacher instructing a jam class"/>
<h1 className="jam-track-name">MANAGE A MUSIC INSTRUMENT STORE?</h1>
<h2 className="original-artist">Increase revenues without more inventory or space</h2>
<div className="clearall"/>
</div>
<JamClassPhone customClass="retailer"/>
<div className="preview-and-action-box jamclass retailer">
<img src="/assets/landing/arrow-1-student.png" className="arrow1-jamclass"/>
<div className="preview-jamtrack-header">
Sign Up Your Store
</div>
<div className={classNames({'preview-area': true, 'jam-class': true})}>
<p>Sign up to let us know youre interested in partnering, and well follow up to answer your questions.</p>
<p>If this is a good fit for your store, well help set up your JamKazam account and ship you a countertop
display with POSA cards.</p>
<p>We will not share your email. See our <a href="/corp/privacy" onClick={this.privacyPolicy}>privacy
policy</a></p>
{register}
<p>Learn how we can help you increase revenues without additional inventory or floor space.</p>
</div>
</div>
</div>
<div className="row summary-text">
<p className="top-summary">
Founded by a team that has built and sold companies to Google, eBay, GameStop and more, JamKazam has built
technology that lets musicians play together live in sync with studio quality audio over the Internet. Now
weve launched both an online music lesson marketplace, as well as a JamTracks marketplace that lets musicians
play along with their favorite bands and songs in compelling new ways. And weve created a simple, profitable
POSA card (point of sale activated gift card) program that retailers can use to sell these products without
any inventory investment or floor space.
</p>
</div>
</div>`
getInitialState: () ->
{loginErrors: null, processing: false}
privacyPolicy: (e) ->
e.preventDefault()
context.JK.popExternalLink('/corp/privacy')
termsClicked: (e) ->
e.preventDefault()
context.JK.popExternalLink('/corp/terms')
componentDidMount: () ->
$root = $(this.getDOMNode())
$checkbox = $root.find('.terms-checkbox')
context.JK.checkbox($checkbox)
# add item to cart, create the user if necessary, and then place the order to get the free JamTrack.
ctaClick: (e) ->
e.preventDefault()
return if @state.processing
@setState({loginErrors: null})
loggedIn = context.JK.currentUserId?
if loggedIn
@markTeacher()
else
@createUser()
@setState({processing: true})
markTeacher: () ->
rest.updateUser({school_interest: true})
.done((response) =>
this.setState({done: true})
context.location = '/client#/home'
)
.fail((jqXHR) =>
this.setState({processing: false})
context.JK.app.notifyServerError(jqXHR, "Unable to Mark As Interested in School")
)
createUser: () ->
$form = $('.jamtrack-signup-form')
email = $form.find('input[name="email"]').val()
password = $form.find('input[name="password"]').val()
terms = $form.find('input[name="terms"]').is(':checked')
rest.signup({
email: email,
password: password,
first_name: null,
last_name: null,
terms: terms,
school_interest: true
})
.done((response) =>
context.location = '/client#/home'
).fail((jqXHR) =>
@setState({processing: false})
if jqXHR.status == 422
response = JSON.parse(jqXHR.responseText)
if response.errors
@setState({loginErrors: response.errors})
else
context.JK.app.notify({title: 'Unknown Signup Error', text: jqXHR.responseText})
else
context.JK.app.notifyServerError(jqXHR, "Unable to Sign Up")
)
@setState({processing: true})
})

View File

@ -51,8 +51,8 @@ rest = context.JK.Rest()
<h2 className="original-artist">Do you own/operate a music school?</h2>
<div className="clearall"/>
</div>
<JamClassPhone/>
<div className="preview-and-action-box jamclass">
<JamClassPhone customClass="school"/>
<div className="preview-and-action-box jamclass school">
<img src="/assets/landing/arrow-1-student.png" className="arrow1-jamclass" />
<div className="preview-jamtrack-header">
Sign Up Your School

View File

@ -0,0 +1,196 @@
context = window
rest = context.JK.Rest()
@PosaActivationPage = React.createClass({
render: () ->
if this.props.retailer.large_photo_url?
logo = `<div className="retailer-logo">
<img src={this.props.retailer.large_photo_url}/>
</div>`
ctaButtonText = 'ACTIVATE'
if @state.processing
ctaButtonText = 'HOLD ON'
console.log("this.props.retailer", this.props.retailer, this.props.has_teachers)
if this.state.loginErrors?
for key, value of this.state.loginErrors
break
if this.state.emailErrors?
for errorKey, value of this.state.emailErrors
break
success = `<div className="success-message">
{this.state.success}
</div>`
emailSuccess = `<div className="success-message">
{this.state.emailSuccess}
</div>`
posaErrors = context.JK.getFullFirstError(key, this.state.loginErrors,
{code: 'POSA Card', activated_at: 'POSA Card', claimed_at: 'Claimed', user: 'User', retailer: 'Retailer'})
emailErrors = context.JK.getFullFirstError(errorKey, this.state.emailErrors,
{email: 'Email address', retailer: 'Retailer'})
register = `<div className="register-area jam-class">
<form className="retailer-signup-form" onSubmit={this.submit}>
<div className="field">
<label>POSA Card Code: </label><input type="text" name="code"/>
</div>
<a
className={classNames({'activate-btn': true, 'button-orange':true, 'cta-button' : true, 'processing': this.state.processing})}
onClick={this.activateClick}>{'ACTIVATE'}</a>
</form>
{success}
<div className={classNames({'errors': true, 'active': this.state.loginErrors})}>
{posaErrors}
</div>
</div>`
sendEmail = `<div className="send-email jam-class">
<form className="retailer-email-form" onSubmit={this.submitEmail}>
<div className="field">
<label>Customer Email: </label><input type="text" name="email"/>
</div>
<a
className={classNames({'activate-btn': true, 'button-orange':true, 'cta-button' : true, 'processing': this.state.processing})}
onClick={this.submitEmail}>{'SEND LINKS'}</a>
</form>
{emailSuccess}
<div className={classNames({'errors': true, 'active': this.state.emailErrors})}>
{emailErrors}
</div>
</div>`
leftColumnClasses = classNames({column: true, has_teachers: this.props.has_teachers})
rightColumnClasses = classNames({column: true, has_teachers: this.props.has_teachers})
`<div className="container">
<div className={leftColumnClasses}>
<div className="header-area">
<div className="header-content">
{logo}
<div className="headers">
<h1>ACTIVATE JAMKAZAM POSA CARD</h1>
</div>
<br className="clearall"/>
</div>
</div>
<div className="explain">
<p>
Please enter the 10-digit code from the back of the POSA card you have sold, and then click the Activate
Card button:
</p>
</div>
{register}
</div>
<div className={rightColumnClasses}>
<div className="header-area">
<div className="header-content">
<div className="headers">
<h1>SEND TEACHER LINKS TO STUDENT</h1>
</div>
<br className="clearall"/>
</div>
</div>
<div className="explain">
<p>
If you want to send links to your stores teachers to this customer, enter their email address below, and click the Send Links button.
</p>
</div>
{sendEmail}
</div>
<div className="clearall"/>
</div>`
submit: (e) ->
@activateClick(e)
getInitialState: () ->
{loginErrors: null, processing: false, emailErrors: null}
componentDidMount: () ->
# add item to cart, create the user if necessary, and then place the order to get the free JamTrack.
activateClick: (e) ->
e.preventDefault()
return if @state.processing
@setState({processing: true, success: null, emailSuccess:null, loginErrors: null})
@activateCode()
submitEmail: (e) ->
e.preventDefault()
return if @state.processing
$form = $('.retailer-email-form')
email = $form.find('input[name="email"]').val()
emailErrors = null
if(!email)
emailErrors = {"email": ['must be specified']}
processing = false
else
processing = true
rest.sendRetailerCustomerEmail({retailer: this.props.retailer.id, email: email})
.done((response) =>
@setState({processing: false, emailSuccess: "List of teachers sent to #{email}."})
$form.find('input[name="email"]').val('')
)
.fail((jqXHR) =>
@setState({processing: false})
if jqXHR.status == 404
@setState({emailErrors: {"retailer": ['is not valid']}})
else if jqXHR.status == 422
response = JSON.parse(jqXHR.responseText)
if response.errors
@setState({emailErrors: response.errors})
else
context.JK.app.notify({title: 'Unable to Send Email', text: jqXHR.responseText})
else
context.JK.app.notifyServerError(jqXHR, "Unable to Send Email")
)
@setState({processing: processing, success: null, emailSuccess: null, emailErrors: emailErrors})
activateCode: () ->
$form = $('.retailer-signup-form')
code = $form.find('input[name="code"]').val()
rest.posaActivate({
code: code,
slug: @props.retailer.slug
})
.done((response) =>
@setState({processing: false, success: 'Card successfully activated. Please give card to customer. Thank you!'})
).fail((jqXHR) =>
@setState({processing: false})
console.log("jqXHR.status", jqXHR.status)
if jqXHR.status == 404
@setState({loginErrors: {"code": ['is not valid']}})
else if jqXHR.status == 422
response = JSON.parse(jqXHR.responseText)
if response.errors
@setState({loginErrors: response.errors})
else
context.JK.app.notify({title: 'Unable to Activate POSA Card', text: jqXHR.responseText})
else
context.JK.app.notifyServerError(jqXHR, "Unable to Activate POSA Card")
)
})

View File

@ -2,6 +2,7 @@ context = window
rest = context.JK.Rest()
ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;
badCode = 'This is not a valid code. Please carefully re-enter the code and try again. If it still does not work, please email us at support@jamkazam.com to report this problem.'
@RedeemGiftCardPage = React.createClass({
render: () ->
@ -15,12 +16,21 @@ ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;
buttonClassnames = classNames({'redeem-giftcard': true, 'button-orange': true, disabled: @state.processing || @state.done })
if @state.done
button =
`<div key="done" className="done-action">
<div>You have {this.state.gifted_jamtracks} free JamTracks on your account!</div>
<div>You can now <a className="go-browse" href="/client#/jamtrack">browse our collection</a> and redeem them.</div>
</div>`
if this.state.gifted_jamtracks
button =
`<div key="done" className="done-action">
<div>You now have {this.state.gifted_jamtracks} JamTracks credits on your account!</div>
<div><a className="go-browse" href="/client#/jamtrack">go to JamTracks home page</a></div>
</div>`
else
button =
`<div key="done" className="done-action">
<div>You now have {this.state.gifted_jamclass}, 30-minute JamClass credits on your account!</div>
<div><a className="go-browse" href="/client#/jamclass/searchOptions">go to JamClass teacher search page</a></div>
</div>`
else
button = `<button key="button" className={buttonClassnames} onClick={this.action}>REDEEM GIFT CARD</button>`
@ -35,7 +45,7 @@ ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;
<label>Gift Card Code:</label><input type="text" name="code"/>
{action}
</form>`
instruments = `<p className="instructions">Enter the code from the back of your gift card to associate it with your account.</p>`
instruments = `<p className="instructions">Enter the 10-digit code from the back of your gift card and click the Redeem button below.</p>`
else
form =
`<form onSubmit={this.submit}>
@ -47,7 +57,7 @@ ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;
<div className="clearall"/>
{action}
</form>`
instruments = `<p className="instructions">Enter the code from the back of your gift card to associate it with your new JamKazam account.</p>`
instruments = `<p className="instructions">Enter the 10-digit code from the back of your gift card and click the Redeem button below.</p>`
classes = classNames({'redeem-container': true, 'not-logged-in': !context.JK.currentUserId?, 'logged-in': context.JK.currentUserId? })
@ -105,7 +115,7 @@ ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;
rest.redeemGiftCard({gift_card: code})
.done((response) =>
@setState({formErrors: null, processing:false, done: true, gifted_jamtracks: response.gifted_jamtracks})
@setState({formErrors: null, processing:false, done: true, gifted_jamtracks: response.gifted_jamtracks, gifted_jamclass: response.gifted_jamclass})
).fail((jqXHR) =>
@setState({processing:false})
@ -142,7 +152,7 @@ ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;
rest.signup({email: email, password: password, gift_card: code, terms: terms})
.done((response) =>
@setState({formErrors: null, processing:false, done: true, gifted_jamtracks: response.gifted_jamtracks})
@setState({formErrors: null, processing:false, done: true, gifted_jamtracks: response.gifted_jamtracks, gifted_jamclass: response.gifted_jamclass})
).fail((jqXHR) =>
@setState({processing:false})

View File

@ -0,0 +1,147 @@
context = window
rest = context.JK.Rest()
@RetailerTeacherLandingPage = React.createClass({
render: () ->
if this.props.retailer.large_photo_url?
logo = `<div className="retailer-logo">
<img src={this.props.retailer.large_photo_url}/>
</div>`
loggedIn = context.JK.currentUserId? && !this.props.preview
if this.state.done
ctaButtonText = 'sending you in...'
else if this.state.processing
ctaButtonText = 'hold on...'
else
if loggedIn
ctaButtonText = "GO TO JAMKAZAM"
else
ctaButtonText = "SIGN UP"
if loggedIn
register =
`<div className="register-area jam-class">
<button className={classNames({'cta-button' : true, 'processing': this.state.processing})}
onClick={this.ctaClick}>{ctaButtonText}</button>
</div>`
else
if this.state.loginErrors?
for key, value of this.state.loginErrors
break
errorText = context.JK.getFullFirstError(key, this.state.loginErrors,
{email: 'Email', password: 'Password', 'terms_of_service': 'The terms of service'})
register = `<div className="register-area jam-class">
<div className={classNames({'errors': true, 'active': this.state.loginErrors})}>
{errorText}
</div>
<form className="retailer-signup-form">
<div className="field">
<label>Email: </label><input type="text" defaultValue={this.props.defaultEmail} name="email"/>
</div>
<div className="field">
<label>Password: </label><input type="password" name="password"/>
</div>
<div className="clearall"/>
<input className="terms-checkbox" type="checkbox" name="terms"/><label className="terms-help">I have read and
agree to the JamKazam <a href="/corp/terms" target="_blank">terms of service</a></label>
<div className="clearall"/>
<button className={classNames({'cta-button' : true, 'processing': this.state.processing})}
onClick={this.ctaClick}>{ctaButtonText}</button>
</form>
<p className="privacy-policy">
We will not share your email.<br/>See our <a href="/corp/privacy" target="_blank">privacy policy</a>.
</p>
</div>`
`<div className="container">
<div className="header-area">
<div className="header-content">
{logo}
<div className="headers">
<h1>REGISTER AS A TEACHER</h1>
<h2>with {this.props.retailer.name}</h2>
</div>
<br className="clearall"/>
</div>
</div>
<div className="explain">
<p>
Please register here if you are currently a teacher with {this.props.retailer.name}, and if you plan to teach
online music lessons for students of {this.props.retailer.name} using the JamKazam service. When you have registered, we
will
email you instructions to set up your online teacher profile, and we'll schedule a brief online training session to make sure
you are comfortable using the service and ready to go with students in online lessons.
</p>
</div>
{register}
</div>`
getInitialState: () ->
{loginErrors: null, processing: false}
componentDidMount: () ->
$root = $(this.getDOMNode())
$checkbox = $root.find('.terms-checkbox')
context.JK.checkbox($checkbox)
# add item to cart, create the user if necessary, and then place the order to get the free JamTrack.
ctaClick: (e) ->
e.preventDefault()
return if @state.processing
@setState({loginErrors: null})
loggedIn = context.JK.currentUserId?
if loggedIn
#window.location.href = "/client#/jamclass"
window.location.href = "/client#/profile/#{context.JK.currentUserId}"
else
@createUser()
@setState({processing:true})
createUser: () ->
$form = $('.retailer-signup-form')
email = $form.find('input[name="email"]').val()
password = $form.find('input[name="password"]').val()
terms = $form.find('input[name="terms"]').is(':checked')
rest.signup({
email: email,
password: password,
first_name: null,
last_name: null,
terms: terms,
teacher: true,
retailer_invitation_code: this.props.invitation_code,
retailer_id: this.props.retailer.id
})
.done((response) =>
@setState({done: true})
#window.location.href = "/client#/jamclass"
window.location.href = "/client#/profile/#{response.id}"
).fail((jqXHR) =>
@setState({processing: false})
if jqXHR.status == 422
response = JSON.parse(jqXHR.responseText)
if response.errors
@setState({loginErrors: response.errors})
else
context.JK.app.notify({title: 'Unknown Signup Error', text: jqXHR.responseText})
else
context.JK.app.notifyServerError(jqXHR, "Unable to Sign Up")
)
})

View File

@ -0,0 +1,65 @@
$ = jQuery
context = window
logger = context.JK.logger
rest = new context.JK.Rest()
@RetailerStore = Reflux.createStore(
{
retailer: null,
teacherInvitations: null
listenables: @RetailerActions
init: ->
this.listenTo(context.AppStore, this.onAppInit)
onAppInit: (@app) ->
onLoaded: (response) ->
@retailer = response
if @retailer.photo_url?
@retailer.photo_url = @retailer.photo_url + '?cache-bust=' + new Date().getTime()
if @retailer.large_photo_url?
@retailer.large_photo_url = @retailer.large_photo_url + '?cache-bust=' + new Date().getTime()
@changed()
rest.listRetailerInvitations({id:@retailer.id}).done((response) => @onLoadedTeacherInvitations(response)).fail((jqXHR) => @onRetailerInvitationFail(jqXHR))
onLoadedTeacherInvitations: (response) ->
@teacherInvitations = response.entries
@changed()
onAddInvitation: (invitation) ->
@teacherInvitations.push(invitation)
@changed()
onDeleteInvitation: (id) ->
if @teacherInvitations?
@teacherInvitations = @teacherInvitations.filter (invitation) -> invitation.id isnt id
@changed()
onRefresh: (retailerId) ->
if !retailerId?
retailerId = @retailer?.id
rest.getRetailer({id: retailerId}).done((response) => @onLoaded(response)).fail((jqXHR) => @onRetailerFail(jqXHR))
onUpdateRetailer: (retailer) ->
@retailer = retailer
@changed()
onRetailerFail:(jqXHR) ->
@app.layout.notify({title: 'Unable to Request Retailer Info', text: "We recommend you refresh the page."})
onRetailerInvitationFail:(jqXHR) ->
@app.layout.notify({title: 'Unable to Request Retailer Invitation Info', text: "We recommend you refresh the page."})
changed:() ->
@trigger(@getState())
getState:() ->
{retailer: @retailer, teacherInvitations: @teacherInvitations}
}
)

View File

@ -11,4 +11,5 @@
*= require users/signin
*= require dialogs/dialog
*= require icheck/minimal/minimal
*= require landings/posa_activation
*/

View File

@ -77,12 +77,37 @@ body.web.individual_jamtrack {
}
}
.video-section {
height:460px;
width:661px;
.video-wrapper {
height:100%;
width:100%;
.video-container {
height:100%;
width:100%;
}
}
margin-bottom:40px;
&.second {
margin-bottom:400px;
}
}
.jamclass-phone {
position: relative;
top: -150px;
right: 107px;
background-color: black;
&.school {
top:8px;
}
&.retailer {
top:8px;
}
&.student {
top: -13px;
}
@ -141,6 +166,10 @@ body.web.individual_jamtrack {
&.teachers {
padding-top: 100px;
}
&.retailers {
}
}
width: 1050px;
p {
@ -714,6 +743,13 @@ body.web.individual_jamtrack {
&.jamclass {
top: 209px;
&.school {
top:356px;
}
&.retailer {
top:356px;
}
&.student {
top: 541px;

View File

@ -0,0 +1,62 @@
@import "client/common.scss";
body.landing_page.full.posa_activation .landing-content {
font-size:1em;
h1 {
font-size:1.5rem;
}
h2 {
font-size:1.5rem;
}
.column {
float:left;
width:50%;
@include border_box_sizing;
&:nth-child(1) {
width:100%;
&.has_teachers {
width:50%;
}
}
&:nth-child(2) {
display:none;
&.has_teachers {
width:50%;
display:block;
}
}
}
.explain {
margin:2rem 0;
}
label {
display:inline-block;
margin-right:1rem;
}
input[name="code"] {
margin-bottom:1rem;
}
.activate-btn {
margin: 0 1rem 0 1rem;
height: 1rem;
line-height: 1rem;
top: -1px;
position: relative;
}
.field {
display:inline-block;
}
.success-message {
font-weight:bold;
font-size:1rem;
margin-top:1rem;
}
.errors {
font-weight:bold;
font-size:1rem;
margin-top:1rem;
color:red;
}
}

View File

@ -0,0 +1,204 @@
@import "client/common";
$fluid-break: 1100px;
$copy-color-on-dark: #b9b9b9;
$cta-color: #e03d04;
@mixin layout-small {
@media (max-width: #{$fluid-break - 1px}) {
@content;
}
}
@mixin layout-normal {
@media (min-width: #{$fluid-break}) {
@content;
}
}
body.web.retailer_register {
h1.web-tagline {
@include layout-small {
display: none;
}
}
.header-area {
padding-top:30px;
text-align:center;
margin-bottom:40px;
}
.explain {
margin-bottom:40px;
text-align:center;
p {
display:inline-block;
text-align: left;
width:600px;
}
}
.field {
margin-top:1px;
}
.header-content {
display:inline-block;
}
.retailer-logo {
margin-right:60px;
float:left;
img {
max-width:225px;
max-height:225px;
}
}
.headers {
float:left;
text-align:left;
padding-top:64px;
h1 {
margin-bottom:10px;
}
h2 {
font-size:16px;
}
}
.register-area {
text-align:center;
width:400px;
margin:0 auto;
input {
background-color: $copy-color-on-dark;
color: black;
font-size: 16px;
@include layout-small {
font-size:30pt;
}
&[name="terms"] {
width:auto;
line-height:24px;
vertical-align:middle;
@include layout-small {
line-height:125%;
}
}
}
.checkbox-wrap {
float: left;
margin-top: 6px;
margin-left:64px;
@include border_box_sizing;
text-align:right;
input {
height:auto;
@include layout-small {
height: 30pt !important;
width: 30pt !important;
}
}
@include layout-small {
width:40%;
margin-left:0;
.icheckbox_minimal {
right: -18px;
}
}
.icheckbox_minimal {
}
}
.cta-button {
font-size: 24px;
color: white;
background-color: $cta-color;
text-align: center;
padding: 10px;
display: block;
width: 100%;
border: 1px outset buttonface;
font-family: Raleway, Arial, Helvetica, sans-serif;
@include layout-small {
font-size:30pt;
}
}
.privacy-policy {
margin-top:10px;
line-height:125%;
}
form {
display:inline-block;
}
.errors {
font-size:12px;
height:20px;
margin:0;
visibility: hidden;
text-align: center;
color: red;
font-weight: bold;
&.active {
visibility: visible;
}
@include layout-small {
font-size:20pt;
height:32pt;
}
}
label {
text-align:left;
width:100px;
display: inline-block;
height: 36px;
vertical-align: middle;
line-height: 36px;
margin-bottom: 15px;
@include border-box_sizing;
&.terms-help {
color:$ColorTextTypical;
width:205px;
height:28px;
line-height:14px;
float:right;
@include layout-small {
line-height:125%;
}
}
@include layout-small {
height:40pt;
font-size:30pt;
}
}
input {
width: 206px;
height: 36px;
float: right;
margin-bottom: 15px;
@include border-box_sizing;
@include layout-small {
height:40pt;
font-size:30pt;
}
}
}
}

View File

@ -0,0 +1,35 @@
require 'sanitize'
class ApiPosaCardsController < ApiController
before_filter :api_signed_in_user, :only => [:claim]
before_filter :posa_http_basic_auth, :only => [:activate]
#before_filter :lookup_review_summary, :only => [:details]
#before_filter :lookup_review, :only => [:update, :delete, :show]
respond_to :json
# Create a review:
def activate
@posa_card = PosaCard.find_by_code!(params[:code])
PosaCard.activate(@posa_card, @retailer)
if @posa_card.errors.any?
respond_with_model(@posa_card)
return
end
end
def claim
@posa_card = PosaCard.find_by_code!(params[:code])
@posa_card.claim(current_user)
if @posa_card.errors.any?
respond_with_model(@posa_card)
return
end
end
end

View File

@ -0,0 +1,61 @@
class ApiRetailerInvitationsController < ApiController
before_filter :api_signed_in_user
before_filter :lookup_retailer, :only => [:index, :create]
before_filter :auth_retailer, :only => [:index, :create]
before_filter :lookup_retailer_invitation, :only => [:delete, :resend]
before_filter :auth_retailer_invitation, :only => [:delete, :resend]
respond_to :json
def index
data = RetailerInvitation.index(@retailer, params)
@retailer_invitations = data[:query]
@next = data[:next_page]
render "api_retailer_invitations/index", :layout => nil
end
def create
@retailer_invitation = RetailerInvitation.create(current_user, @retailer, params)
if @retailer_invitation.errors.any?
respond_with @retailer_invitation, status: :unprocessable_entity
return
end
end
def delete
@retailer_invitation.destroy
respond_with responder: ApiResponder, :status => 204
end
def resend
@retailer_invitation.resend
end
private
def lookup_retailer_invitation
@retailer_invitation = RetailerInvitation.find_by_id(params[:invitation_id])
raise ActiveRecord::RecordNotFound, "Can't find retailer invitation" if @retailer_invitation.nil?
end
def auth_retailer_invitation
if current_user.id != @retailer_invitation.retailer.owner.id && current_user.id != @retailer_invitation.retailer.owner.id
raise JamPermissionError, "You do not have access to this retailer"
end
end
def lookup_retailer
@retailer = Retailer.find_by_id(params[:id])
raise ActiveRecord::RecordNotFound, "Can't find retailer" if @retailer.nil?
end
def auth_retailer
if current_user.id != @retailer.owner.id && current_user.id != @retailer.owner.id
raise JamPermissionError, "You do not have access to this retailer"
end
end
end

View File

@ -0,0 +1,112 @@
class ApiRetailersController < ApiController
before_filter :api_signed_in_user, :except => [:customer_email]
before_filter :lookup_retailer, :only => [:show, :update, :update_avatar, :delete_avatar, :generate_filepicker_policy, :remove_student, :remove_teacher, :customer_email]
before_filter :auth_retailer, :only => [:show, :update, :update_avatar, :delete_avatar, :generate_filepicker_policy, :remove_student, :remove_teacher]
respond_to :json
def show
end
def update
@retailer.update_from_params(params)
respond_with_model(@retailer)
end
def update_avatar
original_fpfile = params[:original_fpfile]
cropped_fpfile = params[:cropped_fpfile]
cropped_large_fpfile = params[:cropped_large_fpfile]
crop_selection = params[:crop_selection]
# public bucket to allow images to be available to public
@retailer.update_avatar(original_fpfile, cropped_fpfile, cropped_large_fpfile, crop_selection, Rails.application.config.aws_bucket_public)
if @retailer.errors.any?
respond_with @retailer, status: :unprocessable_entity
return
end
end
def delete_avatar
@retailer.delete_avatar(Rails.application.config.aws_bucket_public)
if @retailer.errors.any?
respond_with @retailer, status: :unprocessable_entity
return
end
end
def generate_filepicker_policy
# generates a soon-expiring filepicker policy so that a user can only upload to their own folder in their bucket
handle = params[:handle]
call = 'pick,convert,store'
policy = { :expiry => (DateTime.now + 5.minutes).to_i(),
:call => call,
#:path => 'avatars/' + @user.id + '/.*jpg'
}
# if the caller specifies a handle, add it to the hash
unless handle.nil?
start = handle.rindex('/') + 1
policy[:handle] = handle[start..-1]
end
policy = Base64.urlsafe_encode64( policy.to_json )
digest = OpenSSL::Digest::Digest.new('sha256')
signature = OpenSSL::HMAC.hexdigest(digest, Rails.application.config.fp_secret, policy)
render :json => {
:signature => signature,
:policy => policy
}, :status => :ok
end
def remove_student
user = User.find(params[:user_id])
user.retailer_id = nil
if !user.save
respond_with user, status: :unprocessable_entity
return
end
end
def remove_teacher
teacher = User.find(params[:teacher_id])
teacher.teacher.retailer_id = nil
if !teacher.teacher.save
respond_with teacher.teacher, status: :unprocessable_entity
return
end
end
def customer_email
if !User::VALID_EMAIL_REGEX.match(params[:email])
raise JamRuby::JamArgumentError.new('is not valid', :email)
end
UserMailer.retailer_customer_blast(params[:email], @retailer).deliver_now
render :json => {}, status: 200
end
private
def lookup_retailer
@retailer = Retailer.find_by_id(params[:id])
raise ActiveRecord::RecordNotFound, "Can't find retailer" if @retailer.nil?
end
def auth_retailer
if current_user.id != @retailer.owner.id && current_user.id != @retailer.owner.id
raise JamPermissionError, "You do not have access to this retailer"
end
end
end

View File

@ -3,17 +3,17 @@ class ApiUsersController < ApiController
before_filter :api_signed_in_user, :except => [:create, :calendar, :show, :signup_confirm, :auth_session_create, :complete, :finalize_update_email, :isp_scoring, :add_play, :crash_dump, :validate_data, :google_auth, :user_event]
before_filter :auth_user, :only => [:session_settings_show, :session_history_index, :session_user_history_index, :update, :delete, :authorizations, :test_drive_status,
:liking_create, :liking_destroy, # likes
:following_create, :following_show, :following_destroy, # followings
:recording_update, :recording_destroy, # recordings
:favorite_create, :favorite_destroy, # favorites
:friend_request_index, :friend_request_show, :friend_request_create, :friend_request_update, # friend requests
:friend_show, :friend_destroy, # friends
:notification_index, :notification_destroy, # notifications
:band_invitation_index, :band_invitation_show, :band_invitation_update, # band invitations
:set_password, :begin_update_email, :update_avatar, :delete_avatar, :generate_filepicker_policy,
:share_session, :share_recording,
:affiliate_report, :audio_latency, :broadcast_notification, :redeem_giftcard]
:liking_create, :liking_destroy, # likes
:following_create, :following_show, :following_destroy, # followings
:recording_update, :recording_destroy, # recordings
:favorite_create, :favorite_destroy, # favorites
:friend_request_index, :friend_request_show, :friend_request_create, :friend_request_update, # friend requests
:friend_show, :friend_destroy, # friends
:notification_index, :notification_destroy, # notifications
:band_invitation_index, :band_invitation_show, :band_invitation_update, # band invitations
:set_password, :begin_update_email, :update_avatar, :delete_avatar, :generate_filepicker_policy,
:share_session, :share_recording,
:affiliate_report, :audio_latency, :broadcast_notification, :redeem_giftcard]
before_filter :ip_blacklist, :only => [:create, :redeem_giftcard]
respond_to :json, :except => :calendar
@ -60,10 +60,10 @@ class ApiUsersController < ApiController
def profile_show
@profile = User.includes([{musician_instruments: :instrument},
{band_musicians: :user},
{genre_players: :genre},
:bands, :instruments, :genres,
:online_presences, :performance_samples])
{band_musicians: :user},
{genre_players: :genre},
:bands, :instruments, :genres,
:online_presences, :performance_samples])
.find(params[:id])
@show_teacher_profile = params[:show_teacher]
@ -215,20 +215,20 @@ class ApiUsersController < ApiController
user.updating_password = true
user.easy_save(
params[:first_name],
params[:last_name],
nil, # email can't be edited at this phase. We need to get them into the site, and they can edit on profile page if they really want
params[:password],
params[:password_confirmation],
true, # musician
params[:gender],
params[:birth_date],
params[:isp],
params[:city],
params[:state],
params[:country],
params[:instruments],
params[:photo_url])
params[:first_name],
params[:last_name],
nil, # email can't be edited at this phase. We need to get them into the site, and they can edit on profile page if they really want
params[:password],
params[:password_confirmation],
true, # musician
params[:gender],
params[:birth_date],
params[:isp],
params[:city],
params[:state],
params[:country],
params[:instruments],
params[:photo_url])
if user.errors.any?
render :json => user.errors.full_messages(), :status => :unprocessable_entity
@ -273,7 +273,7 @@ class ApiUsersController < ApiController
begin
User.reset_password(params[:email], ApplicationHelper.base_uri(request))
rescue JamRuby::JamArgumentError
render :json => { :message => ValidationMessages::EMAIL_NOT_FOUND }, :status => 403
render :json => {:message => ValidationMessages::EMAIL_NOT_FOUND}, :status => 403
end
respond_with responder: ApiResponder, :status => 204
end
@ -284,7 +284,7 @@ class ApiUsersController < ApiController
rescue JamRuby::JamArgumentError
# FIXME
# There are some other errors that can happen here, besides just EMAIL_NOT_FOUND
render :json => { :message => ValidationMessages::EMAIL_NOT_FOUND }, :status => 403
render :json => {:message => ValidationMessages::EMAIL_NOT_FOUND}, :status => 403
end
set_remember_token(@user)
respond_with responder: ApiResponder, :status => 204
@ -295,16 +295,16 @@ class ApiUsersController < ApiController
@user = User.authenticate(params[:email], params[:password])
if @user.nil?
render :json => { :success => false }, :status => 404
render :json => {:success => false}, :status => 404
else
sign_in @user
render :json => { :success => true }, :status => 200
render :json => {:success => true}, :status => 200
end
end
def auth_session_delete
sign_out
render :json => { :success => true }, :status => 200
render :json => {:success => true}, :status => 200
end
###################### SESSION SETTINGS ###################
@ -443,7 +443,7 @@ class ApiUsersController < ApiController
def friend_destroy
if current_user.id != params[:id] && current_user.id != params[:friend_id]
render :json => { :message => "You are not allowed to delete this friendship." }, :status => 403
render :json => {:message => "You are not allowed to delete this friendship."}, :status => 403
end
# clean up both records representing this "friendship"
JamRuby::Friendship.delete_all "(user_id = '#{params[:id]}' AND friend_id = '#{params[:friend_id]}') OR (user_id = '#{params[:friend_id]}' AND friend_id = '#{params[:id]}')"
@ -455,9 +455,9 @@ class ApiUsersController < ApiController
if params[:type] == 'TEXT_MESSAGE'
# you can ask for just text_message notifications
raise JamArgumentError.new('can\'t be blank', 'receiver') if params[:receiver].blank?
raise JamArgumentError.new('can\'t be blank', 'limit') if params[:limit].blank?
raise JamArgumentError.new('can\'t be blank', 'offset') if params[:offset].blank?
raise JamArgumentError.new('can\'t be blank', 'receiver') if params[:receiver].blank?
raise JamArgumentError.new('can\'t be blank', 'limit') if params[:limit].blank?
raise JamArgumentError.new('can\'t be blank', 'offset') if params[:offset].blank?
receiver_id = params[:receiver]
limit = params[:limit].to_i
@ -499,7 +499,7 @@ class ApiUsersController < ApiController
respond_with @invitation, responder: ApiResponder, :status => 200
rescue ActiveRecord::RecordNotFound
render :json => { :message => ValidationMessages::BAND_INVITATION_NOT_FOUND }, :status => 404
render :json => {:message => ValidationMessages::BAND_INVITATION_NOT_FOUND}, :status => 404
end
end
@ -514,7 +514,7 @@ class ApiUsersController < ApiController
respond_with @invitation, responder: ApiResponder, :status => 200
rescue ActiveRecord::RecordNotFound
render :json => { :message => ValidationMessages::BAND_INVITATION_NOT_FOUND }, :status => 404
render :json => {:message => ValidationMessages::BAND_INVITATION_NOT_FOUND}, :status => 404
end
end
@ -550,7 +550,7 @@ class ApiUsersController < ApiController
if score.save
render :text => 'scoring recorded'
else
render :text => "score invalid: #{score.errors.inspect}", status:422
render :text => "score invalid: #{score.errors.inspect}", status: 422
end
end
@ -589,9 +589,9 @@ class ApiUsersController < ApiController
call = 'pick,convert,store'
policy = { :expiry => (DateTime.now + 5.minutes).to_i(),
:call => call,
#:path => 'avatars/' + @user.id + '/.*jpg'
policy = {:expiry => (DateTime.now + 5.minutes).to_i(),
:call => call,
#:path => 'avatars/' + @user.id + '/.*jpg'
}
# if the caller specifies a handle, add it to the hash
@ -600,14 +600,14 @@ class ApiUsersController < ApiController
policy[:handle] = handle[start..-1]
end
policy = Base64.urlsafe_encode64( policy.to_json )
digest = OpenSSL::Digest.new('sha256')
policy = Base64.urlsafe_encode64(policy.to_json)
digest = OpenSSL::Digest.new('sha256')
signature = OpenSSL::HMAC.hexdigest(digest, Rails.application.config.fp_secret, policy)
render :json => {
:signature => signature,
:policy => policy
}, :status => :ok
:signature => signature,
:policy => policy
}, :status => :ok
end
@ -678,13 +678,13 @@ class ApiUsersController < ApiController
end
logger.debug("sending crash email with subject#{subject}")
AdminMailer.crash_alert(subject: subject, body:body).deliver_now
AdminMailer.crash_alert(subject: subject, body: body).deliver_now
redirect_to write_url, status: 307
else
# we should store it here to aid in development, but we don't have to until someone wants the feature
# so... just return 200
render :json => { :id => @dump.id }, :status => 200
render :json => {:id => @dump.id}, :status => 200
end
end
@ -695,13 +695,14 @@ class ApiUsersController < ApiController
@user = current_user
@user.update_progression_field(:first_downloaded_client_at)
if @user.errors.any?
respond_with @user, :status => :unprocessable_entity
return
end
if @user.errors.any?
respond_with @user, :status => :unprocessable_entity
return
end
render :json => {}, :status => 200
end
# user progression tracking
def qualified_gear
@user = current_user
@ -741,7 +742,7 @@ class ApiUsersController < ApiController
end
def opened_jamtrack_web_player
User.where(id: current_user.id).update_all(first_opened_jamtrack_web_player: Time.now)
User.where(id: current_user.id).update_all(first_opened_jamtrack_web_player: Time.now)
render :json => {}, :status => 200
end
@ -764,21 +765,21 @@ class ApiUsersController < ApiController
if provider == 'facebook'
render json: {
description: view_context.description_for_music_session(history),
title: view_context.title_for_music_session(history, current_user),
photo_url: view_context.facebook_image_for_music_session(history),
url: share_token_url(history.share_token.token),
caption: 'www.jamkazam.com'
}, status: 200
description: view_context.description_for_music_session(history),
title: view_context.title_for_music_session(history, current_user),
photo_url: view_context.facebook_image_for_music_session(history),
url: share_token_url(history.share_token.token),
caption: 'www.jamkazam.com'
}, status: 200
elsif provider == 'twitter'
render json: {
message: view_context.title_for_music_session(history, current_user)
}, status: 200
message: view_context.title_for_music_session(history, current_user)
}, status: 200
else
render :json => { :errors => {:provider => ['not valid']} }, :status => 422
render :json => {:errors => {:provider => ['not valid']}}, :status => 422
end
end
@ -791,22 +792,22 @@ class ApiUsersController < ApiController
if provider == 'facebook'
render json: {
description: view_context.description_for_claimed_recording(claimed_recording),
title: view_context.title_for_claimed_recording(claimed_recording, current_user),
photo_url: view_context.facebook_image_for_claimed_recording(claimed_recording),
url: share_token_url(claimed_recording.share_token.token),
caption: 'www.jamkazam.com'
}, status: 200
description: view_context.description_for_claimed_recording(claimed_recording),
title: view_context.title_for_claimed_recording(claimed_recording, current_user),
photo_url: view_context.facebook_image_for_claimed_recording(claimed_recording),
url: share_token_url(claimed_recording.share_token.token),
caption: 'www.jamkazam.com'
}, status: 200
elsif provider == 'twitter'
render json: {
message: view_context.title_for_claimed_recording(history, current_user) + " at " + request.host_with_port
}, status: 200
message: view_context.title_for_claimed_recording(history, current_user) + " at " + request.host_with_port
}, status: 200
else
render :json => { :errors => {:provider => ['not valid']} }, :status => 422
render :json => {:errors => {:provider => ['not valid']}}, :status => 422
end
end
@ -821,53 +822,53 @@ class ApiUsersController < ApiController
elsif request.get?
result = {}
result['account'] = {
'address' => oo.address.clone,
'tax_identifier' => oo.tax_identifier,
'entity_type' => oo.entity_type,
'partner_name' => oo.partner_name,
'partner_id' => oo.partner_user_id,
'id' => oo.id
'address' => oo.address.clone,
'tax_identifier' => oo.tax_identifier,
'entity_type' => oo.entity_type,
'partner_name' => oo.partner_name,
'partner_id' => oo.partner_user_id,
'id' => oo.id
}
if txt = oo.affiliate_legalese.try(:legalese)
txt = ControllerHelp.instance.simple_format(txt)
end
result['agreement'] = {
'legalese' => txt,
'signed_at' => oo.signed_at
'legalese' => txt,
'signed_at' => oo.signed_at
}
#result['signups'] = oo.referrals_by_date
#result['earnings'] = [['April 2015', '1000 units', '$100']]
render json: result.to_json, status: 200
end
else
render :json => { :message => 'user not affiliate partner' }, :status => 400
render :json => {:message => 'user not affiliate partner'}, :status => 400
end
end
def affiliate_report
begin
affiliate = User
.where(:id => params[:id])
.includes(:affiliate_partner)
.limit(1)
.first
.affiliate_partner
.where(:id => params[:id])
.includes(:affiliate_partner)
.limit(1)
.first
.affiliate_partner
referrals_by_date = affiliate.referrals_by_date do |by_date|
by_date.inject([]) { |rr, key| rr << key }
end
result = {
:total_count => affiliate.referral_user_count,
:by_date => referrals_by_date
:total_count => affiliate.referral_user_count,
:by_date => referrals_by_date
}
render json: result.to_json, status: 200
rescue
render :json => { :message => $!.to_s }, :status => 400
render :json => {:message => $!.to_s}, :status => 400
end
end
def add_play
if params[:id].blank?
render :json => { :message => "Playable ID is required" }, :status => 400
render :json => {:message => "Playable ID is required"}, :status => 400
return
end
@ -880,7 +881,7 @@ class ApiUsersController < ApiController
play.save
if play.errors.any?
render :json => { :errors => play.errors }, :status => 422
render :json => {:errors => play.errors}, :status => 422
else
render :json => {}, :status => 201
end
@ -914,7 +915,7 @@ class ApiUsersController < ApiController
def validate_data
unless (data = params[:data]).present?
render(json: { message: "blank data #{data}" }, status: :unprocessable_entity) && return
render(json: {message: "blank data #{data}"}, status: :unprocessable_entity) && return
end
url = nil
site = params[:sitetype]
@ -923,10 +924,10 @@ class ApiUsersController < ApiController
elsif Utils.recording_source?(site)
rec_data = Utils.extract_recording_data(site, data)
if rec_data
render json: { message: 'Valid Site', recording_id: rec_data["id"], recording_title: rec_data["title"], data: data }, status: 200
render json: {message: 'Valid Site', recording_id: rec_data["id"], recording_title: rec_data["title"], data: data}, status: 200
return
else
render json: { message: 'Invalid Site', data: data, errors: { site: ["Could not detect recording identifier"] } }, status: 200
render json: {message: 'Invalid Site', data: data, errors: {site: ["Could not detect recording identifier"]}}, status: 200
return
end
else
@ -934,12 +935,12 @@ class ApiUsersController < ApiController
end
unless url.blank?
if errmsg = Utils.site_validator(url, site)
render json: { message: 'Invalid Site', data: data, errors: { site: [errmsg] } }, status: 200
render json: {message: 'Invalid Site', data: data, errors: {site: [errmsg]}}, status: 200
else
render json: { message: 'Valid Site', data: data }, status: 200
render json: {message: 'Valid Site', data: data}, status: 200
end
else
render json: { message: "unknown validation for data '#{params[:data]}', site '#{params[:site]}'" }, status: :unprocessable_entity
render json: {message: "unknown validation for data '#{params[:data]}', site '#{params[:site]}'"}, status: :unprocessable_entity
end
end
@ -951,7 +952,7 @@ class ApiUsersController < ApiController
@broadcast.did_view(current_user)
respond_with_model(@broadcast)
else
render json: { }, status: 200
render json: {}, status: 200
end
end
@ -964,39 +965,66 @@ class ApiUsersController < ApiController
@broadcast.save
end
render json: { }, status: 200
render json: {}, status: 200
end
def lookup_user
User.includes([{musician_instruments: :instrument},
{band_musicians: :user},
{genre_players: :genre},
:bands, :instruments, :genres, :jam_track_rights,
:affiliate_partner, :reviews, :review_summary, :recordings,
{band_musicians: :user},
{genre_players: :genre},
:bands, :instruments, :genres, :jam_track_rights,
:affiliate_partner, :reviews, :review_summary, :recordings,
:teacher => [:subjects, :instruments, :languages, :genres, :teachers_languages, :experiences_teaching, :experiences_award, :experiences_education, :reviews, :review_summary]])
.find(params[:id])
.find(params[:id])
end
def redeem_giftcard
def try_posa_card
@posa_card = PosaCard.find_by_code(params[:gift_card])
if @posa_card.nil?
return false
end
@posa_card.claim(current_user)
if @posa_card.errors.any?
respond_with_model(@posa_card)
else
if @posa_card.card_type == PosaCard::JAM_CLASS_4
render json: {gifted_jamclass: 4}, status: 200
elsif @posa_card.card_type == PosaCard::JAM_TRACKS_10
render json: {gifted_jamtracks: 10}, status: 200
elsif @posa_card.card_type == PosaCard::JAM_TRACKS_5
render json: {gifted_jamtracks: 5}, status: 200
else
raise 'unknown card_type ' + @posa_card.card_type
end
end
return true
end
def try_gift_card
@gift_card = GiftCard.find_by_code(params[:gift_card])
if @gift_card.nil?
render json: {errors:{gift_card: ['does not exist']}}, status: 422
render json: {errors: {gift_card: ['does not exist']}}, status: 422
return
end
if current_user.gift_cards.count >= 5
render json: {errors:{gift_card: ['has too many on account']}}, status: 422
render json: {errors: {gift_card: ['has too many on account']}}, status: 422
return
end
if @gift_card.user
if @gift_card.user == current_user
render json: {errors:{gift_card: ['already redeemed by you']}}, status: 422
render json: {errors: {gift_card: ['already redeemed by you']}}, status: 422
return
else
render json: {errors:{gift_card: ['already redeemed by another']}}, status: 422
render json: {errors: {gift_card: ['already redeemed by another']}}, status: 422
return
end
end
@ -1012,10 +1040,19 @@ class ApiUsersController < ApiController
# apply gift card items to everything in shopping cart
current_user.reload
ShoppingCart.apply_gifted_jamtracks(current_user)
render json: {gifted_jamtracks:current_user.gifted_jamtracks}, status: 200
render json: {gifted_jamtracks: current_user.gifted_jamtracks}, status: 200
end
end
def redeem_giftcard
# first, try to find posa_card
rendered = try_posa_card
try_gift_card if !rendered
end
def test_drive_status
@user = current_user
@teacher = User.find(params[:teacher_id])

View File

@ -4,6 +4,8 @@ class LandingsController < ApplicationController
respond_to :html
before_filter :posa_http_basic_auth, only: [:posa_activation]
def watch_bands
@promo_buzz = PromoBuzz.active
@ -138,6 +140,14 @@ class LandingsController < ApplicationController
render 'jam_class_schools', layout: 'web'
end
def jam_class_retailers
enable_olark
@no_landing_tag = true
@landing_tag_play_learn_earn = true
@show_after_black_bar_border = true
render 'jam_class_retailers', layout: 'web'
end
def individual_jamtrack
enable_olark
@ -367,5 +377,47 @@ class LandingsController < ApplicationController
@page_data = {school: @school, invitation_code: params[:invitation_code], defaultEmail: defaultEmail, preview: @preview}
render 'school_teacher_register', layout: 'web'
end
def retailer_teacher_register
@no_landing_tag = true
@landing_tag_play_learn_earn = true
@retailer = Retailer.find_by_id(params[:id])
if @retailer.nil?
redirect_to '/signup'
return
end
@title = 'Become a teacher with ' + @retailer.name
@description = "Using JamKazam, teach online lessons with " + @retailer.name
@preview = !params[:preview].nil?
@invitation = RetailerInvitation.find_by_invitation_code(params[:invitation_code]) if params[:invitation_code]
defaultEmail = ''
if @invitation
defaultEmail = @invitation.email
end
@page_data = {retailer: @retailer, invitation_code: params[:invitation_code], defaultEmail: defaultEmail, preview: @preview}
render 'retailer_teacher_register', layout: 'web'
end
def posa_activation
@no_landing_tag = true
@landing_tag_play_learn_earn = true
@retailer = Retailer.find_by_slug(params[:slug])
if @retailer.nil?
redirect_to '/signup'
return
end
@page_data = {retailer: @retailer, has_teachers: @retailer.teachers.count > 0}
render 'posa_activation', layout: 'web'
end
end

View File

@ -145,6 +145,20 @@ module SessionsHelper
end
end
def posa_http_basic_auth
@retailer = Retailer.find_by_slug(params[:slug])
if @retailer.nil?
redirect_to signin_url, notice: "Please use the correct url for retailers in."
return
end
authenticate_or_request_with_http_basic('Administration') do |username, password|
@retailer.matches_password(password)
end
end
def ip_blacklist
if current_user && current_user.admin
return

View File

@ -0,0 +1,3 @@
object @posa_card
extends "api_posa_cards/show"

View File

@ -0,0 +1,3 @@
object @posa_card
extends "api_posa_cards/show"

View File

@ -0,0 +1,3 @@
@posa_card
attributes :id

View File

@ -0,0 +1,3 @@
object @retailer_invitation
extends "api_retailer_invitations/show"

View File

@ -0,0 +1,11 @@
node :next do |page|
@next
end
node :entries do |page|
partial "api_retailer_invitations/show", object: @retailer_invitations
end
node :total_entries do |page|
@retailer_invitations.total_entries
end

View File

@ -0,0 +1,3 @@
object @retailer_invitation
extends "api_retailer_invitations/show"

View File

@ -0,0 +1,7 @@
object @retailer_invitation
attributes :id, :user_id, :retailer_id, :invitation_code, :note, :email, :first_name, :last_name, :accepted
child(:user => :user) do |user|
partial "api_users/show_minimal", object: user
end

View File

@ -0,0 +1,3 @@
object @retailer
extends "api_retailers/show"

View File

@ -0,0 +1,3 @@
object @retailer
extends "api_retailers/show"

View File

@ -0,0 +1,16 @@
object @retailer
attributes :id, :user_id, :name, :enabled, :original_fpfile, :cropped_fpfile, :crop_selection, :photo_url
child :owner => :owner do
attributes :id, :email, :photo_url, :name, :first_name, :last_name
end
child :teachers => :teachers do |teacher|
attributes :id
child :user => :user do
attributes :id, :name, :first_name, :last_name, :photo_url
end
end

View File

@ -0,0 +1,3 @@
object @retailer
extends "api_retailers/show"

View File

@ -0,0 +1,3 @@
object @retailer
extends "api_retailers/show"

View File

@ -34,6 +34,10 @@ if current_user && @user == current_user
user.owned_school.id if user.owned_school
end
node :owned_retailer_id do |user|
user.owned_retailer.id if user.owned_retailer
end
child :user_authorizations => :user_authorizations do |auth|
attributes :uid, :provider, :token_expiration
end

View File

@ -0,0 +1,9 @@
#account-retailer.screen.secondary layout="screen" layout-id="account/retailer"
.content-head
.content-icon
= image_tag "content/icon_account.png", :size => "27x20"
h1
| jamclass
= render "screen_navigation"
.content-body
= react_component 'AccountRetailerScreen', {}

View File

@ -86,6 +86,7 @@
<%= render "account_session_properties" %>
<%= render "account_payment_history" %>
<%= render "account_school" %>
<%= render "account_retailer" %>
<%= render "inviteMusicians" %>
<%= render "hoverBand" %>
<%= render "hoverFan" %>

View File

@ -0,0 +1,14 @@
- provide(:page_name, 'landing_page full individual_jamtrack')
- provide(:description, @description)
- provide(:title, @title)
= react_component 'JamClassRetailerLandingPage', @page_data.to_json
- content_for :after_black_bar do
.row.cta-row
h2 SIGN UP YOUR STORE NOW!
p Start generating more revenues, while helping your customers better engage with their instruments.
p.cta-text Not sure if our retail partner program is for you? Scroll down to learn more.
- content_for :white_bar do
= react_component 'JamClassRetailerLandingBottomPage', @page_data.to_json

View File

@ -0,0 +1,5 @@
- provide(:page_name, 'landing_page full posa_activation')
- provide(:description, @description)
- provide(:title, @title)
= react_component 'PosaActivationPage', @page_data.to_json

View File

@ -0,0 +1,5 @@
- provide(:page_name, 'landing_page full retailer_register teacher')
- provide(:description, @description)
- provide(:title, @title)
= react_component 'RetailerTeacherLandingPage', @page_data.to_json

View File

@ -49,6 +49,7 @@ Rails.application.routes.draw do
get '/landing/jamclass/teachers', to: 'landings#jam_class_teachers', as: 'jamclass_teacher_signup'
get '/landing/jamclass/affiliates', to: 'landings#jam_class_affiliates'
get '/landing/jamclass/schools', to: 'landings#jam_class_schools'
get '/landing/jamclass/retailers', to: 'landings#jam_class_retailers'
get '/affiliateProgram', to: 'landings#affiliate_program', as: 'affiliate_program'
@ -57,6 +58,9 @@ Rails.application.routes.draw do
match '/school/:id/student', to: 'landings#school_student_register', via: :get, as: 'school_student_register'
match '/school/:id/teacher', to: 'landings#school_teacher_register', via: :get, as: 'school_teacher_register'
match '/retailer/:id/teacher', to: 'landings#retailer_teacher_register', via: :get, as: 'retailer_teacher_register'
match '/posa/:slug', to: 'landings#posa_activation', via: :get, as: 'posa_activation'
# redirect /jamtracks to jamtracks browse page
get '/jamtracks', to: redirect('/client#/jamtrack/search')
@ -727,6 +731,21 @@ Rails.application.routes.draw do
match '/schools/:id/students/:user_id' => 'api_schools#remove_student', :via => :delete
match '/schools/:id/teachers/:teacher_id' => 'api_schools#remove_teacher', :via => :delete
match '/retailers/:id' => 'api_retailers#show', :via => :get
match '/retailers/:id' => 'api_retailers#update', :via => :post
match '/retailers/:id/avatar' => 'api_retailers#update_avatar', :via => :post
match '/retailers/:id/avatar' => 'api_retailers#delete_avatar', :via => :delete
match '/retailers/:id/filepicker_policy' => 'api_retailers#generate_filepicker_policy', :via => :get
match '/retailers/:id/invitations' => 'api_retailer_invitations#index', :via => :get
match '/retailers/:id/invitations' => 'api_retailer_invitations#create', :via => :post
match '/retailers/:id/invitations/:invitation_id/resend' => 'api_retailer_invitations#resend', :via => :post
match '/retailers/:id/invitations/:invitation_id' => 'api_retailer_invitations#delete', :via => :delete
match '/retailers/:id/teachers/:teacher_id' => 'api_retailers#remove_teacher', :via => :delete
match '/retailers/:id/customer_email' => 'api_retailers#customer_email', :via => :post
match '/posa/:slug/activate' => 'api_posa_cards#activate', via: :post
match '/posa/claim' => 'api_posa_cards#claim', via: :post
match '/teacher_distributions' => 'api_teacher_distributions#index', :via => :get
match '/stripe' => 'api_stripe#store', :via => :post

View File

@ -52,7 +52,7 @@ module JamRuby
# https://developers.google.com/youtube/v3/docs/videos/insert
# https://developers.google.com/youtube/v3/guides/using_resumable_upload_protocol
def sign_youtube_upload(user, filename, length)
def sign_youtube_upload(user, filename, length)
raise ArgumentError, "Length is required and should be > 0" if length.to_i.zero?
# Something like this:

View File

@ -0,0 +1,44 @@
require 'spec_helper'
describe ApiPosaCardsController, type: :controller do
render_views
let (:password) {'abcdef'}
let (:posa_card) {FactoryGirl.create(:posa_card)}
let (:owner) {FactoryGirl.create(:user)}
let (:user) {FactoryGirl.create(:user)}
let (:retailer) {FactoryGirl.create(:retailer, user: owner)}
let (:authorization) { 'Basic ' + Base64::encode64("#{password}:#{password}") }
before(:each) do
retailer.update_from_params({password:password})
end
describe "activate" do
it "works" do
request.headers['HTTP_AUTHORIZATION'] = authorization
get :activate, slug: retailer.slug, code: posa_card.code
response.should be_success
JSON.parse(response.body)['id'].should eql posa_card.id
posa_card.reload
posa_card.activated_at.should_not be_nil
posa_card.retailer.should eql retailer
end
end
describe "claim" do
it "works" do
controller.current_user = user
posa_card.activate(retailer)
get :claim, code: posa_card.code
response.should be_success
JSON.parse(response.body)['id'].should eql posa_card.id
posa_card.reload
posa_card.claimed_at.should_not be_nil
posa_card.user.should eql user
end
end
end

View File

@ -0,0 +1,54 @@
require 'spec_helper'
describe ApiRetailerInvitationsController, type: :controller do
render_views
let (:owner) {FactoryGirl.create(:user)}
let (:retailer) {FactoryGirl.create(:retailer, user: owner)}
let (:retailer_invitation_teacher) {FactoryGirl.create(:retailer_invitation, retailer: retailer)}
before(:each) do
controller.current_user = owner
end
describe "index" do
it "works" do
get :index, id: retailer.id
response.should be_success
JSON.parse(response.body)['total_entries'].should eql 0
retailer_invitation_teacher.touch
get :index, id: retailer.id
response.should be_success
JSON.parse(response.body)['total_entries'].should eql 1
end
end
describe "create" do
it "works" do
UserMailer.deliveries.clear
post :create, id: retailer.id, first_name: "Seth", last_name: "Call", email: "seth@jamkazam.com", :format => 'json'
response.should be_success
UserMailer.deliveries.length.should eql 1
JSON.parse(response.body)['id'].should eql RetailerInvitation.find_by_email("seth@jamkazam.com").id
end
end
describe "resend" do
it "works" do
UserMailer.deliveries.clear
post :resend, id: retailer.id, invitation_id: retailer_invitation_teacher.id, :format => 'json'
UserMailer.deliveries.length.should eql 1
response.should be_success
end
end
describe "delete" do
it "works" do
delete :delete, id: retailer.id, invitation_id: retailer_invitation_teacher.id, :format => 'json'
response.should be_success
end
end
end

View File

@ -0,0 +1,32 @@
require 'spec_helper'
describe ApiRetailersController, type: :controller do
render_views
let (:owner) {FactoryGirl.create(:user)}
let (:retailer) {FactoryGirl.create(:retailer, user: owner)}
before(:each) do
controller.current_user = owner
end
describe "show" do
it "works" do
get :show, id: retailer.id
response.should be_success
JSON.parse(response.body)['id'].should eql retailer.id
end
end
describe "update" do
it "works" do
post :update, id: retailer.id, name: "Hardy har", format: 'json'
response.should be_success
json = JSON.parse(response.body)
json['name'].should eql "Hardy har"
end
end
end

View File

@ -867,6 +867,19 @@ FactoryGirl.define do
card_type GiftCard::JAM_TRACKS_5
end
factory :posa_card, class: 'JamRuby::PosaCard' do
sequence(:code) { |n| n.to_s }
card_type JamRuby::PosaCardType::JAM_TRACKS_5
end
factory :posa_card_type, class: 'JamRuby::PosaCardType' do
card_type JamRuby::PosaCardType::JAM_TRACKS_5
end
factory :posa_card_purchase, class: 'JamRuby::PosaCardPurchase' do
association :user, factory: :user
end
factory :jamblaster, class: 'JamRuby::Jamblaster' do
association :user, factory: :user
@ -901,6 +914,22 @@ FactoryGirl.define do
accepted false
end
factory :retailer, class: 'JamRuby::Retailer' do
association :user, factory: :user
sequence(:name) {|n| "Dat Retailer"}
sequence(:slug) { |n| "retailer-#{n}" }
enabled true
end
factory :retailer_invitation, class: 'JamRuby::RetailerInvitation' do
association :retailer, factory: :retailer
note "hey come in in"
sequence(:email) { |n| "retail_person#{n}@example.com" }
sequence(:first_name) {|n| "FirstName"}
sequence(:last_name) {|n| "LastName"}
accepted false
end
factory :lesson_booking_slot, class: 'JamRuby::LessonBookingSlot' do
factory :lesson_booking_slot_single do
slot_type 'single'

View File

@ -151,9 +151,14 @@ def web_config
def email_partners_alias
"partner-dev@jamkazam.com"
end
def test_drive_wait_period_year
1
end
def jam_class_card_wait_period_year
1
end
end
klass.new
end