* merged
This commit is contained in:
commit
37417708b3
|
|
@ -20,6 +20,5 @@ class EmailController < ApplicationController
|
|||
if params[:any_jam_track]
|
||||
@users = @users.select('DISTINCT users.id, email, first_name, last_name').joins(:sales => :sale_line_items).where("sale_line_items.product_type = 'JamTrack'")
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
|
@ -321,4 +321,8 @@ jam_track_sessions.sql
|
|||
jam_track_sessions_v2.sql
|
||||
email_screening.sql
|
||||
bounced_email_cleanup.sql
|
||||
news.sql
|
||||
news.sql
|
||||
profile_teacher.sql
|
||||
populate_languages.sql
|
||||
populate_subjects.sql
|
||||
reviews.sql
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
insert into languages(description, id) values ('English','EN');
|
||||
insert into languages(description, id) values ('Afrikanns','AF');
|
||||
insert into languages(description, id) values ('Albanian','SQ');
|
||||
insert into languages(description, id) values ('Arabic','AR');
|
||||
insert into languages(description, id) values ('Armenian','HY');
|
||||
insert into languages(description, id) values ('Basque','EU');
|
||||
insert into languages(description, id) values ('Bengali','BN');
|
||||
insert into languages(description, id) values ('Bulgarian','BG');
|
||||
insert into languages(description, id) values ('Catalan','CA');
|
||||
insert into languages(description, id) values ('Cambodian','KM');
|
||||
insert into languages(description, id) values ('Chinese (Mandarin)','ZH');
|
||||
insert into languages(description, id) values ('Croation','HR');
|
||||
insert into languages(description, id) values ('Czech','CS');
|
||||
insert into languages(description, id) values ('Danish','DA');
|
||||
insert into languages(description, id) values ('Dutch','NL');
|
||||
insert into languages(description, id) values ('Estonian','ET');
|
||||
insert into languages(description, id) values ('Fiji','FJ');
|
||||
insert into languages(description, id) values ('Finnish','FI');
|
||||
insert into languages(description, id) values ('French','FR');
|
||||
insert into languages(description, id) values ('Georgian','KA');
|
||||
insert into languages(description, id) values ('German','DE');
|
||||
insert into languages(description, id) values ('Greek','EL');
|
||||
insert into languages(description, id) values ('Gujarati','GU');
|
||||
insert into languages(description, id) values ('Hebrew','HE');
|
||||
insert into languages(description, id) values ('Hindi','HI');
|
||||
insert into languages(description, id) values ('Hungarian','HU');
|
||||
insert into languages(description, id) values ('Icelandic','IS');
|
||||
insert into languages(description, id) values ('Indonesian','ID');
|
||||
insert into languages(description, id) values ('Irish','GA');
|
||||
insert into languages(description, id) values ('Italian','IT');
|
||||
insert into languages(description, id) values ('Japanese','JA');
|
||||
insert into languages(description, id) values ('Javanese','JW');
|
||||
insert into languages(description, id) values ('Korean','KO');
|
||||
insert into languages(description, id) values ('Latin','LA');
|
||||
insert into languages(description, id) values ('Latvian','LV');
|
||||
insert into languages(description, id) values ('Lithuanian','LT');
|
||||
insert into languages(description, id) values ('Macedonian','MK');
|
||||
insert into languages(description, id) values ('Malay','MS');
|
||||
insert into languages(description, id) values ('Malayalam','ML');
|
||||
insert into languages(description, id) values ('Maltese','MT');
|
||||
insert into languages(description, id) values ('Maori','MI');
|
||||
insert into languages(description, id) values ('Marathi','MR');
|
||||
insert into languages(description, id) values ('Mongolian','MN');
|
||||
insert into languages(description, id) values ('Nepali','NE');
|
||||
insert into languages(description, id) values ('Norwegian','NO');
|
||||
insert into languages(description, id) values ('Persian','FA');
|
||||
insert into languages(description, id) values ('Polish','PL');
|
||||
insert into languages(description, id) values ('Portuguese','PT');
|
||||
insert into languages(description, id) values ('Punjabi','PA');
|
||||
insert into languages(description, id) values ('Quechua','QU');
|
||||
insert into languages(description, id) values ('Romanian','RO');
|
||||
insert into languages(description, id) values ('Russian','RU');
|
||||
insert into languages(description, id) values ('Samoan','SM');
|
||||
insert into languages(description, id) values ('Serbian','SR');
|
||||
insert into languages(description, id) values ('Slovak','SK');
|
||||
insert into languages(description, id) values ('Slovenian','SL');
|
||||
insert into languages(description, id) values ('Spanish','ES');
|
||||
insert into languages(description, id) values ('Swahili','SW');
|
||||
insert into languages(description, id) values ('Swedish ','SV');
|
||||
insert into languages(description, id) values ('Tamil','TA');
|
||||
insert into languages(description, id) values ('Tatar','TT');
|
||||
insert into languages(description, id) values ('Telugu','TE');
|
||||
insert into languages(description, id) values ('Thai','TH');
|
||||
insert into languages(description, id) values ('Tibetan','BO');
|
||||
insert into languages(description, id) values ('Tonga','TO');
|
||||
insert into languages(description, id) values ('Turkish','TR');
|
||||
insert into languages(description, id) values ('Ukranian','UK');
|
||||
insert into languages(description, id) values ('Urdu','UR');
|
||||
insert into languages(description, id) values ('Uzbek','UZ');
|
||||
insert into languages(description, id) values ('Vietnamese','VI');
|
||||
insert into languages(description, id) values ('Welsh','CY');
|
||||
insert into languages(description, id) values ('Xhosa','XH');
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
insert into subjects(id, description) values ('arranging', 'Arranging');
|
||||
insert into subjects(id, description) values ('composing', 'Composing');
|
||||
insert into subjects(id, description) values ('music-business', 'Music Business');
|
||||
insert into subjects(id, description) values ('music-theory', 'Music Theory');
|
||||
insert into subjects(id, description) values ('recording', 'Recording');
|
||||
insert into subjects(id, description) values ('site-reading', 'Site Reading');
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
CREATE TABLE teachers (
|
||||
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
introductory_video VARCHAR(1024) NULL,
|
||||
years_teaching SMALLINT NOT NULL DEFAULT 0,
|
||||
years_playing SMALLINT NOT NULL DEFAULT 0,
|
||||
teaches_age_lower SMALLINT NOT NULL DEFAULT 0,
|
||||
teaches_age_upper SMALLINT NOT NULL DEFAULT 0,
|
||||
teaches_beginner BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
teaches_intermediate BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
teaches_advanced BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
website VARCHAR(1024) NULL,
|
||||
biography VARCHAR(4096) NULL,
|
||||
prices_per_lesson BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
prices_per_month BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
lesson_duration_30 BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
lesson_duration_45 BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
lesson_duration_60 BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
lesson_duration_90 BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
lesson_duration_120 BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
price_per_lesson_30_cents INT NULL,
|
||||
price_per_lesson_45_cents INT NULL,
|
||||
price_per_lesson_60_cents INT NULL,
|
||||
price_per_lesson_90_cents INT NULL,
|
||||
price_per_lesson_120_cents INT NULL,
|
||||
price_per_month_30_cents INT NULL,
|
||||
price_per_month_45_cents INT NULL,
|
||||
price_per_month_60_cents INT NULL,
|
||||
price_per_month_90_cents INT NULL,
|
||||
price_per_month_120_cents INT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
ALTER TABLE users ADD COLUMN teacher_id VARCHAR(64) REFERENCES teachers(id) ON DELETE SET NULL;
|
||||
|
||||
CREATE TABLE subjects(
|
||||
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
description VARCHAR(1024) NULL
|
||||
);
|
||||
|
||||
CREATE TABLE languages(
|
||||
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
description VARCHAR(1024) NULL
|
||||
);
|
||||
|
||||
-- Has many:
|
||||
CREATE TABLE teacher_experiences(
|
||||
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
teacher_id VARCHAR(64) REFERENCES teachers(id) ON DELETE CASCADE,
|
||||
-- experience type: teaching, education, award:
|
||||
experience_type VARCHAR(32) NOT NULL,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
organization VARCHAR(200) NOT NULL,
|
||||
start_year SMALLINT NOT NULL DEFAULT 0,
|
||||
end_year SMALLINT NULL
|
||||
);
|
||||
|
||||
-- Has many/through tables:
|
||||
CREATE TABLE teachers_genres(
|
||||
teacher_id VARCHAR(64) REFERENCES teachers(id) ON DELETE CASCADE,
|
||||
genre_id VARCHAR(64) REFERENCES genres(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE TABLE teachers_instruments(
|
||||
teacher_id VARCHAR(64) REFERENCES teachers(id) ON DELETE CASCADE,
|
||||
instrument_id VARCHAR(64) REFERENCES instruments(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE TABLE teachers_subjects(
|
||||
teacher_id VARCHAR(64) REFERENCES teachers(id) ON DELETE CASCADE,
|
||||
subject_id VARCHAR(64) REFERENCES subjects(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE TABLE teachers_languages(
|
||||
teacher_id VARCHAR(64) REFERENCES teachers(id) ON DELETE CASCADE,
|
||||
language_id VARCHAR(64) REFERENCES languages(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
CREATE TABLE reviews (
|
||||
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL,
|
||||
user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
target_id VARCHAR(64) NOT NULL,
|
||||
target_type VARCHAR(32) NOT NULL,
|
||||
description VARCHAR,
|
||||
rating INT NOT NULL,
|
||||
deleted_by_user_id VARCHAR(64) REFERENCES users(id) ON DELETE SET NULL,
|
||||
deleted_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NULL,
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL,
|
||||
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE review_summaries (
|
||||
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL,
|
||||
target_id VARCHAR(64) NOT NULL,
|
||||
target_type VARCHAR(32) NOT NULL,
|
||||
avg_rating FLOAT NOT NULL,
|
||||
wilson_score FLOAT NOT NULL,
|
||||
review_count INT NOT NULL,
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL,
|
||||
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL
|
||||
);
|
||||
|
|
@ -116,6 +116,8 @@ require "jam_ruby/models/ip_blacklist"
|
|||
require "jam_ruby/models/user_blacklist"
|
||||
require "jam_ruby/models/fraud_alert"
|
||||
require "jam_ruby/models/fingerprint_whitelist"
|
||||
require "jam_ruby/models/review"
|
||||
require "jam_ruby/models/review_summary"
|
||||
require "jam_ruby/models/rsvp_request"
|
||||
require "jam_ruby/models/rsvp_slot"
|
||||
require "jam_ruby/models/rsvp_request_rsvp_slot"
|
||||
|
|
@ -251,6 +253,10 @@ require "jam_ruby/models/online_presence"
|
|||
require "jam_ruby/models/json_store"
|
||||
require "jam_ruby/models/base_search"
|
||||
require "jam_ruby/models/musician_search"
|
||||
require "jam_ruby/models/teacher"
|
||||
require "jam_ruby/models/teacher_experience"
|
||||
require "jam_ruby/models/language"
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ module JamRuby
|
|||
# genres
|
||||
has_and_belongs_to_many :recordings, :class_name => "JamRuby::Recording", :join_table => "recordings_genres"
|
||||
|
||||
# teachers
|
||||
has_and_belongs_to_many :teachers, :class_name => "JamRuby::Teacher", :join_table => "teachers_genres"
|
||||
|
||||
# jam tracks
|
||||
has_many :genres_jam_tracks, :class_name => "JamRuby::GenreJamTrack", :foreign_key => "genre_id"
|
||||
has_many :jam_tracks, :through => :genres_jam_tracks, :class_name => "JamRuby::JamTrack", :source => :genre
|
||||
|
|
|
|||
|
|
@ -43,6 +43,9 @@ module JamRuby
|
|||
# music sessions
|
||||
has_and_belongs_to_many :music_sessions, :class_name => "JamRuby::ActiveMusicSession", :join_table => "genres_music_sessions"
|
||||
|
||||
# teachers
|
||||
has_and_belongs_to_many :teachers, :class_name => "JamRuby::Teacher", :join_table => "teachers_instruments"
|
||||
|
||||
def self.standard_list
|
||||
return Instrument.where('instruments.popularity > 0').order('instruments.popularity DESC, instruments.description ASC')
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
module JamRuby
|
||||
class Language < ActiveRecord::Base
|
||||
include HtmlSanitize
|
||||
html_sanitize strict: [:name, :description]
|
||||
has_and_belongs_to_many :teachers, :class_name => "JamRuby::Teacher", :join_table => "teachers_languages"
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
module JamRuby
|
||||
class Review < ActiveRecord::Base
|
||||
include HtmlSanitize
|
||||
html_sanitize strict: [:description]
|
||||
|
||||
attr_accessible :target, :rating, :description, :user, :user_id, :target_id, :target_type
|
||||
belongs_to :target, polymorphic: true
|
||||
belongs_to :user, foreign_key: 'user_id', class_name: "JamRuby::User"
|
||||
belongs_to :deleted_by_user, foreign_key: 'deleted_by_user_id', class_name: "JamRuby::User"
|
||||
|
||||
scope :available, -> { where("deleted_at iS NULL") }
|
||||
scope :all, -> { select("*") }
|
||||
|
||||
validates :description, length: {maximum: 16000}, no_profanity: true, :allow_blank => true
|
||||
validates :rating, presence: true, numericality: {only_integer: true, minimum: 1, maximum: 5}
|
||||
|
||||
validates :target, presence: true
|
||||
validates :user_id, presence: true
|
||||
validates :target_id, uniqueness: {scope: :user_id, message: "There is already a review for this User and Target."}
|
||||
|
||||
after_save :reduce
|
||||
|
||||
def self.index(options={})
|
||||
if options.key?(:include_deleted)
|
||||
arel = Review.all
|
||||
else
|
||||
arel = Review.available
|
||||
end
|
||||
|
||||
if options.key?(:target_id)
|
||||
arel = arel.where("target_id=?", options[:target_id])
|
||||
end
|
||||
|
||||
if options.key?(:user_id)
|
||||
arel = arel.where("user_id=?", options[:user_id])
|
||||
end
|
||||
|
||||
arel
|
||||
end
|
||||
|
||||
# Create review_summary records by grouping reviews
|
||||
def self.reduce_all
|
||||
ReviewSummary.transaction do
|
||||
ReviewSummary.destroy_all
|
||||
Review.select("target_id, target_type AS target_type, AVG(rating) as avg_rating, count(*) as review_count, SUM(CASE WHEN rating>=3.0 THEN 1 ELSE 0 END) AS pos_count")
|
||||
.where("deleted_at IS NULL")
|
||||
.group("target_type, target_id")
|
||||
.each do |r|
|
||||
wilson_score = ci_lower_bound(r.pos_count, r.review_count)
|
||||
ReviewSummary.create!(
|
||||
target_id: r.target_id,
|
||||
target_type: r.target_type,
|
||||
avg_rating: r.avg_rating,
|
||||
wilson_score: wilson_score,
|
||||
review_count: r.review_count
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# http://www.evanmiller.org/how-not-to-sort-by-average-rating.html
|
||||
def self.ci_lower_bound(pos, n, confidence=0.95)
|
||||
pos=pos.to_f
|
||||
n=n.to_f
|
||||
return 0 if n == 0
|
||||
z = 1.96 # Statistics2.pnormaldist(1-(1-confidence)/2)
|
||||
phat = 1.0*pos/n
|
||||
(phat + z*z/(2*n) - z * Math.sqrt((phat*(1-phat)+z*z/(4*n))/n))/(1+z*z/n)
|
||||
end
|
||||
|
||||
def reduce
|
||||
ReviewSummary.transaction do
|
||||
ReviewSummary.where(target_type: target_type, target_id: target_id).destroy_all
|
||||
|
||||
Review.select("target_id, target_type AS target_type, AVG(rating) as avg_rating, count(*) as review_count, SUM(CASE WHEN rating>=3.0 THEN 1 ELSE 0 END) AS pos_count")
|
||||
.where("deleted_at IS NULL")
|
||||
.where(target_type: target_type, target_id: target_id)
|
||||
.group("target_type, target_id")
|
||||
.each do |r|
|
||||
wilson_score = Review.ci_lower_bound(r.pos_count, r.review_count)
|
||||
ReviewSummary.create!(
|
||||
target_id: r.target_id,
|
||||
target_type: r.target_type,
|
||||
avg_rating: r.avg_rating,
|
||||
wilson_score: wilson_score,
|
||||
review_count: r.review_count
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
module JamRuby
|
||||
class ReviewSummary < ActiveRecord::Base
|
||||
attr_accessible :target, :target_id, :target_type, :avg_rating, :wilson_score, :review_count
|
||||
belongs_to :target, polymorphic: true
|
||||
|
||||
validates :avg_rating, presence:true, numericality: true
|
||||
validates :review_count, presence:true, numericality: {only_integer: true}
|
||||
validates :wilson_score, presence:true, numericality: {greater_than:0, less_than:1}
|
||||
validates :target_id, presence:true, uniqueness:true
|
||||
|
||||
class << self
|
||||
|
||||
# Query review_summaries using target type, id, and minimum review count
|
||||
# * target_type: Only return review summaries for given target type
|
||||
# * target_id: Only return review summary for given target type
|
||||
# * minimum_reviews: Only return review summary made up of at least this many reviews
|
||||
# * arel: start with pre-queried reviews (arel object)
|
||||
# sorts by wilson score
|
||||
def index(options={})
|
||||
options ||= {}
|
||||
if (options.key?(:arel))
|
||||
arel = options[:arel].order("wilson_score DESC")
|
||||
else
|
||||
arel = ReviewSummary.order("wilson_score DESC")
|
||||
end
|
||||
|
||||
if (options.key?(:target_type))
|
||||
arel = arel.where("target_type=?", options[:target_type])
|
||||
end
|
||||
|
||||
if (options.key?(:target_id))
|
||||
arel = arel.where("target_id=?", options[:target_id])
|
||||
end
|
||||
|
||||
if (options.key?(:minimum_reviews))
|
||||
arel = arel.where("review_count>=?", options[:minimum_reviews])
|
||||
end
|
||||
|
||||
arel
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
module JamRuby
|
||||
class Subject < ActiveRecord::Base
|
||||
include HtmlSanitize
|
||||
html_sanitize strict: [:name, :description]
|
||||
has_and_belongs_to_many :teachers, :class_name => "JamRuby::Teacher", :join_table => "teachers_subjects"
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
module JamRuby
|
||||
class Teacher < ActiveRecord::Base
|
||||
include HtmlSanitize
|
||||
html_sanitize strict: [:biography, :website]
|
||||
attr_accessor :validate_introduction, :validate_basics, :validate_pricing
|
||||
attr_accessible :genres, :teacher_experiences, :experiences_teaching, :experiences_education, :experiences_award
|
||||
has_and_belongs_to_many :genres, :class_name => "JamRuby::Genre", :join_table => "teachers_genres", :order=>"description"
|
||||
has_and_belongs_to_many :instruments, :class_name => "JamRuby::Instrument", :join_table => "teachers_instruments", :order=>"description"
|
||||
has_and_belongs_to_many :subjects, :class_name => "JamRuby::Subject", :join_table => "teachers_subjects", :order=>"description"
|
||||
has_and_belongs_to_many :languages, :class_name => "JamRuby::Language", :join_table => "teachers_languages", :order=>"description"
|
||||
has_many :teacher_experiences, :class_name => "JamRuby::TeacherExperience"
|
||||
has_many :experiences_teaching, :class_name => "JamRuby::TeacherExperience", conditions: {experience_type: 'teaching'}
|
||||
has_many :experiences_education, :class_name => "JamRuby::TeacherExperience", conditions: {experience_type: 'education'}
|
||||
has_many :experiences_award, :class_name => "JamRuby::TeacherExperience", conditions: {experience_type: 'award'}
|
||||
has_many :reviews, :class_name => "JamRuby::Review", as: :target
|
||||
has_one :review_summary, :class_name => "JamRuby::ReviewSummary", as: :target
|
||||
has_one :user, :class_name => 'JamRuby::User'
|
||||
|
||||
validates :user, :presence => true
|
||||
validates :biography, length: {minimum: 5, maximum: 4096}, :if => :validate_introduction
|
||||
validates :introductory_video, :format=> {:with=> /^(?:https?:\/\/)?(?:www\.)?youtu(?:\.be|be\.com)\/(?:watch\?v=)?([\w-]{10,})/, message: "is not a valid youtube URL"}, :allow_blank => true, :if => :validate_introduction
|
||||
validates :years_teaching, :presence => true, :if => :validate_introduction
|
||||
validates :years_playing, :presence => true, :if => :validate_introduction
|
||||
|
||||
validates :instruments, :length => { minimum:1, message:"At least one instrument or subject is required"}, if: :validate_basics, unless: ->(teacher){teacher.subjects.length>0}
|
||||
validates :subjects, :length => { minimum:1, message:"At least one instrument or subject is required"}, if: :validate_basics, unless: ->(teacher){teacher.instruments.length>0}
|
||||
validates :genres, :length => { minimum:1, message:"At least one genre is required"}, if: :validate_basics
|
||||
validates :languages, :length => { minimum:1, message:"At least one language is required"}, if: :validate_basics
|
||||
|
||||
validate :offer_pricing, :if => :validate_pricing
|
||||
validate :offer_duration, :if => :validate_pricing
|
||||
validate :teaches_ages, :if => :validate_basics
|
||||
|
||||
default_scope { includes(:genres) }
|
||||
|
||||
class << self
|
||||
def save_teacher(user, params)
|
||||
teacher = build_teacher(user, params)
|
||||
teacher.save
|
||||
teacher
|
||||
end
|
||||
|
||||
def build_teacher(user, params)
|
||||
# ensure person creating this Teacher is a Musician
|
||||
unless user && user.musician?
|
||||
raise JamPermissionError, "must be a musician"
|
||||
end
|
||||
|
||||
teacher = user.teacher
|
||||
teacher ||= user.build_teacher()
|
||||
teacher.user = user
|
||||
|
||||
teacher.website = params[:website] if params.key?(:website)
|
||||
teacher.biography = params[:biography] if params.key?(:biography)
|
||||
teacher.introductory_video = params[:introductory_video] if params.key?(:introductory_video)
|
||||
|
||||
teacher.introductory_video = params[:introductory_video] if params.key?(:introductory_video)
|
||||
teacher.years_teaching = params[:years_teaching] if params.key?(:years_teaching)
|
||||
teacher.years_playing = params[:years_playing] if params.key?(:years_playing)
|
||||
teacher.teaches_age_lower = params[:teaches_age_lower] if params.key?(:teaches_age_lower)
|
||||
teacher.teaches_age_upper = params[:teaches_age_upper] if params.key?(:teaches_age_upper)
|
||||
teacher.website = params[:website] if params.key?(:website)
|
||||
teacher.biography = params[:biography] if params.key?(:biography)
|
||||
teacher.teaches_beginner = params[:teaches_beginner] if params.key?(:teaches_beginner)
|
||||
teacher.teaches_intermediate = params[:teaches_intermediate] if params.key?(:teaches_intermediate)
|
||||
teacher.teaches_advanced = params[:teaches_advanced] if params.key?(:teaches_advanced)
|
||||
teacher.prices_per_lesson = params[:prices_per_lesson] if params.key?(:prices_per_lesson)
|
||||
teacher.prices_per_month = params[:prices_per_month] if params.key?(:prices_per_month)
|
||||
teacher.lesson_duration_30 = params[:lesson_duration_30] if params.key?(:lesson_duration_30)
|
||||
teacher.lesson_duration_45 = params[:lesson_duration_45] if params.key?(:lesson_duration_45)
|
||||
teacher.lesson_duration_60 = params[:lesson_duration_60] if params.key?(:lesson_duration_60)
|
||||
teacher.lesson_duration_90 = params[:lesson_duration_90] if params.key?(:lesson_duration_90)
|
||||
teacher.lesson_duration_120 = params[:lesson_duration_120] if params.key?(:lesson_duration_120)
|
||||
teacher.price_per_lesson_30_cents = params[:price_per_lesson_30_cents] if params.key?(:price_per_lesson_30_cents)
|
||||
teacher.price_per_lesson_45_cents = params[:price_per_lesson_45_cents] if params.key?(:price_per_lesson_45_cents)
|
||||
teacher.price_per_lesson_60_cents = params[:price_per_lesson_60_cents] if params.key?(:price_per_lesson_60_cents)
|
||||
teacher.price_per_lesson_90_cents = params[:price_per_lesson_90_cents] if params.key?(:price_per_lesson_90_cents)
|
||||
teacher.price_per_lesson_120_cents = params[:price_per_lesson_120_cents] if params.key?(:price_per_lesson_120_cents)
|
||||
teacher.price_per_month_30_cents = params[:price_per_month_30_cents] if params.key?(:price_per_month_30_cents)
|
||||
teacher.price_per_month_45_cents = params[:price_per_month_45_cents] if params.key?(:price_per_month_45_cents)
|
||||
teacher.price_per_month_60_cents = params[:price_per_month_60_cents] if params.key?(:price_per_month_60_cents)
|
||||
teacher.price_per_month_90_cents = params[:price_per_month_90_cents] if params.key?(:price_per_month_90_cents)
|
||||
teacher.price_per_month_120_cents = params[:price_per_month_120_cents] if params.key?(:price_per_month_120_cents)
|
||||
|
||||
# Many-to-many relations:
|
||||
if params.key?(:genres)
|
||||
genres = params[:genres]
|
||||
genres = [] if genres.nil?
|
||||
teacher.genres = genres.collect{|genre_id| Genre.find(genre_id)}
|
||||
end
|
||||
if params.key?(:instruments)
|
||||
instruments = params[:instruments]
|
||||
instruments = [] if instruments.nil?
|
||||
teacher.instruments = instruments.collect{|instrument_id| Instrument.find(instrument_id)}
|
||||
end
|
||||
if params.key?(:subjects)
|
||||
subjects = params[:subjects]
|
||||
subjects = [] if subjects.nil?
|
||||
teacher.subjects = subjects.collect{|subject_id| Subject.find(subject_id)}
|
||||
end
|
||||
if params.key?(:languages)
|
||||
languages = params[:languages]
|
||||
languages = [] if languages.nil?
|
||||
teacher.languages = languages.collect{|language_id| Language.find(language_id)}
|
||||
end
|
||||
|
||||
# Experience:
|
||||
[:teaching, :education, :award].each do |experience_type|
|
||||
key = "experiences_#{experience_type}".to_sym
|
||||
if params.key?(key)
|
||||
list = params[key]
|
||||
list = [] if list.nil?
|
||||
experiences = list.collect do |exp|
|
||||
TeacherExperience.new(
|
||||
name: exp[:name],
|
||||
experience_type: experience_type,
|
||||
organization: exp[:organization],
|
||||
start_year: exp[:start_year],
|
||||
end_year: exp[:end_year]
|
||||
)
|
||||
end # collect
|
||||
|
||||
# we blindly destroy/recreate on every resubmit
|
||||
previous = teacher.send("#{key.to_s}")
|
||||
previous.destroy_all
|
||||
|
||||
# Dynamically call the appropriate method (just setting the
|
||||
# value doesn't result in the behavior we need)
|
||||
teacher.send("#{key.to_s}=", experiences)
|
||||
end # if
|
||||
end # do
|
||||
|
||||
# How to validate:
|
||||
teacher.validate_introduction = !!params[:validate_introduction]
|
||||
teacher.validate_basics = !!params[:validate_basics]
|
||||
teacher.validate_pricing = !!params[:validate_pricing]
|
||||
|
||||
teacher
|
||||
end
|
||||
end
|
||||
|
||||
def offer_pricing
|
||||
unless prices_per_lesson.present? || prices_per_month.present?
|
||||
errors.add(:offer_pricing, "Must choose to price per lesson or per month")
|
||||
end
|
||||
end
|
||||
|
||||
def offer_duration
|
||||
unless lesson_duration_30.present? || lesson_duration_45.present? || lesson_duration_60.present? || lesson_duration_90.present? || lesson_duration_120.present?
|
||||
errors.add(:offer_duration, "Must offer at least one duration")
|
||||
end
|
||||
end
|
||||
|
||||
def teaches_ages
|
||||
if teaches_age_lower > 0 && teaches_age_upper > 0 && (teaches_age_upper < teaches_age_lower)
|
||||
errors.add(:ages_taught, "Age range is backwards")
|
||||
end
|
||||
end
|
||||
|
||||
def recent_reviews
|
||||
reviews.order('created_at desc').limit(20)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
module JamRuby
|
||||
class TeacherExperience < ActiveRecord::Base
|
||||
include HtmlSanitize
|
||||
html_sanitize strict: [:name, :organization]
|
||||
belongs_to :teacher, :class_name => "JamRuby::Teacher"
|
||||
attr_accessible :name, :experience_type, :organization, :start_year, :end_year
|
||||
|
||||
scope :teaching, where(experience_type: 'teaching')
|
||||
scope :education, where(experience_type: 'education')
|
||||
scope :awards, where(experience_type: 'award')
|
||||
end
|
||||
end
|
||||
|
|
@ -50,6 +50,9 @@ module JamRuby
|
|||
# authorizations (for facebook, etc -- omniauth)
|
||||
has_many :user_authorizations, :class_name => "JamRuby::UserAuthorization"
|
||||
|
||||
has_many :reviews, :class_name => "JamRuby::Review"
|
||||
has_one :review_summary, :class_name => "JamRuby::ReviewSummary"
|
||||
|
||||
# calendars (for scheduling NOT in music_session)
|
||||
has_many :calendars, :class_name => "JamRuby::Calendar"
|
||||
|
||||
|
|
@ -67,7 +70,8 @@ module JamRuby
|
|||
# bands
|
||||
has_many :band_musicians, :class_name => "JamRuby::BandMusician"
|
||||
has_many :bands, :through => :band_musicians, :class_name => "JamRuby::Band"
|
||||
|
||||
has_one :teacher, :class_name => "JamRuby::Teacher"
|
||||
|
||||
# genres
|
||||
has_many :genre_players, as: :player, class_name: "JamRuby::GenrePlayer", dependent: :destroy
|
||||
has_many :genres, through: :genre_players, class_name: "JamRuby::Genre"
|
||||
|
|
@ -183,6 +187,7 @@ module JamRuby
|
|||
|
||||
has_one :musician_search, :class_name => 'JamRuby::MusicianSearch'
|
||||
has_one :band_search, :class_name => 'JamRuby::BandSearch'
|
||||
belongs_to :teacher, :class_name => 'JamRuby::Teacher'
|
||||
|
||||
has_many :jam_track_session, :class_name => "JamRuby::JamTrackSession"
|
||||
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ FactoryGirl.define do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
factory :music_session, :class => JamRuby::MusicSession do
|
||||
sequence(:name) { |n| "Music Session #{n}" }
|
||||
sequence(:description) { |n| "Music Session Description #{n}" }
|
||||
|
|
@ -225,6 +225,16 @@ FactoryGirl.define do
|
|||
description { |n| "Genre #{n}" }
|
||||
end
|
||||
|
||||
factory :language, :class => JamRuby::Language do
|
||||
id { |n| "Language #{n}" }
|
||||
description { |n| "Language #{n}" }
|
||||
end
|
||||
|
||||
factory :subject, :class => JamRuby::Subject do
|
||||
id { |n| "Subject #{n}" }
|
||||
description { |n| "Subject #{n}" }
|
||||
end
|
||||
|
||||
factory :join_request, :class => JamRuby::JoinRequest do
|
||||
text 'let me in to the session!'
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,169 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Review do
|
||||
|
||||
shared_examples_for :review do |target, target_type|
|
||||
before(:each) do
|
||||
Review.delete_all
|
||||
User.delete_all
|
||||
@user = FactoryGirl.create(:user)
|
||||
end
|
||||
|
||||
after(:all) do
|
||||
Review.delete_all
|
||||
User.delete_all
|
||||
end
|
||||
|
||||
context "validates review" do
|
||||
it "blank target" do
|
||||
review = Review.create()
|
||||
review.valid?.should be_false
|
||||
review.errors[:target].should == ["can't be blank"]
|
||||
end
|
||||
|
||||
it "no rating" do
|
||||
review = Review.create(target:target)
|
||||
review.valid?.should be_false
|
||||
review.errors[:rating].should include("can't be blank")
|
||||
review.errors[:rating].should include("is not a number")
|
||||
end
|
||||
|
||||
it "no user" do
|
||||
review = Review.create(target:target, rating:3)
|
||||
review.valid?.should be_false
|
||||
review.errors[:user_id].should include("can't be blank")
|
||||
end
|
||||
|
||||
it "complete" do
|
||||
review = Review.create(target:target, rating:3, user:@user)
|
||||
review.valid?.should be_true
|
||||
end
|
||||
|
||||
it "unique" do
|
||||
review = Review.create(target:target, rating:3, user:@user)
|
||||
review.valid?.should be_true
|
||||
|
||||
review2 = Review.create(target:target, rating:3, user:@user)
|
||||
review2.valid?.should be_false
|
||||
end
|
||||
|
||||
it "reduces" do
|
||||
review = Review.create(target:target, rating:3, user:@user)
|
||||
review.valid?.should be_true
|
||||
|
||||
review2 = Review.create(target:target, rating:5, user:FactoryGirl.create(:user))
|
||||
review2.valid?.should be_true
|
||||
Review.should have(2).items
|
||||
Review.index.should have(2).items
|
||||
|
||||
# Reduce and check:
|
||||
ReviewSummary.should have(1).items
|
||||
ReviewSummary.first.avg_rating.should eq(4.0)
|
||||
|
||||
ws_orig = ReviewSummary.first.wilson_score
|
||||
avg_orig = ReviewSummary.first.avg_rating
|
||||
|
||||
# Create some more and verify:
|
||||
5.times {Review.create(target:target, rating:5, user:FactoryGirl.create(:user))}
|
||||
Review.index.should have(7).items
|
||||
ReviewSummary.should have(1).items
|
||||
|
||||
ReviewSummary.first.wilson_score.should > ws_orig
|
||||
ReviewSummary.first.avg_rating.should > avg_orig
|
||||
|
||||
end
|
||||
end # context
|
||||
|
||||
context "validates review summary" do
|
||||
it "blank target" do
|
||||
review_summary = ReviewSummary.create()
|
||||
review_summary.valid?.should be_false
|
||||
review_summary.errors[:target_id].should == ["can't be blank"]
|
||||
end
|
||||
|
||||
it "no rating" do
|
||||
review_summary = ReviewSummary.create(target:target)
|
||||
review_summary.valid?.should be_false
|
||||
review_summary.errors[:target].should be_empty
|
||||
review_summary.errors[:avg_rating].should include("can't be blank")
|
||||
review_summary.errors[:avg_rating].should include("is not a number")
|
||||
end
|
||||
|
||||
it "no score" do
|
||||
review_summary = ReviewSummary.create(target:target, avg_rating:3.2)
|
||||
review_summary.valid?.should be_false
|
||||
review_summary.errors[:target].should be_empty
|
||||
review_summary.errors[:avg_rating].should be_empty
|
||||
review_summary.errors[:wilson_score].should include("can't be blank")
|
||||
review_summary.errors[:wilson_score].should include("is not a number")
|
||||
end
|
||||
|
||||
it "no count" do
|
||||
review_summary = ReviewSummary.create(target:target, avg_rating:3.2, wilson_score:0.95)
|
||||
review_summary.valid?.should be_false
|
||||
review_summary.errors[:review_count].should include("can't be blank")
|
||||
end
|
||||
|
||||
it "complete" do
|
||||
review_summary = ReviewSummary.create(target:target, avg_rating:3.2, wilson_score:0.95, review_count: 15)
|
||||
review_summary.valid?.should be_true
|
||||
end
|
||||
|
||||
it "unique" do
|
||||
review = ReviewSummary.create(target:target, avg_rating:3, wilson_score:0.82, review_count:14)
|
||||
review.valid?.should be_true
|
||||
|
||||
review2 = ReviewSummary.create(target:target, avg_rating:3.22, wilson_score:0.91, review_count:12)
|
||||
review2.valid?.should be_false
|
||||
end
|
||||
|
||||
it "reduces and queries" do
|
||||
review = Review.create(target:target, rating:3, user:@user)
|
||||
review.valid?.should be_true
|
||||
review2 = Review.create(target:target, rating:5, user:FactoryGirl.create(:user))
|
||||
review2.valid?.should be_true
|
||||
Review.should have(2).items
|
||||
|
||||
ReviewSummary.should have(1).items
|
||||
ReviewSummary.first.avg_rating.should eq(4.0)
|
||||
|
||||
ws_orig = ReviewSummary.first.wilson_score
|
||||
avg_orig = ReviewSummary.first.avg_rating
|
||||
|
||||
|
||||
# Create some more and verify:
|
||||
5.times {Review.create(target:target, rating:5, user:FactoryGirl.create(:user))}
|
||||
ReviewSummary.should have(1).items
|
||||
ReviewSummary.first.wilson_score.should > ws_orig
|
||||
ReviewSummary.first.avg_rating.should > avg_orig
|
||||
|
||||
|
||||
# Create some more with a different target and verify:
|
||||
target2=FactoryGirl.create(:jam_track)
|
||||
5.times {Review.create(target:target2, rating:5, user:FactoryGirl.create(:user))}
|
||||
Review.index.should have(12).items
|
||||
Review.index(target_id: target2).should have(5).items
|
||||
summaries = ReviewSummary.index()
|
||||
summaries.should have(2).items
|
||||
summaries[0].wilson_score.should > summaries[1].wilson_score
|
||||
|
||||
summaries = ReviewSummary.index(target_id: target2)
|
||||
summaries.should have(1).items
|
||||
summaries[0].target_id.should eq(target2.id)
|
||||
|
||||
summaries = ReviewSummary.index(target_type: "JamRuby::JamTrack")
|
||||
summaries.should have(2).items
|
||||
|
||||
summaries = ReviewSummary.index(minimum_reviews: 6)
|
||||
summaries.should have(1).items
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "with a jamtrack" do
|
||||
@jam_track = FactoryGirl.create(:jam_track)
|
||||
it_behaves_like :review, @jam_track, "jam_track"
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
|
|
@ -0,0 +1,285 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Teacher do
|
||||
|
||||
let(:user) { FactoryGirl.create(:user) }
|
||||
let(:genre1) { FactoryGirl.create(:genre) }
|
||||
let(:genre2) { FactoryGirl.create(:genre) }
|
||||
let(:subject1) { FactoryGirl.create(:subject) }
|
||||
let(:subject2) { FactoryGirl.create(:subject) }
|
||||
let(:language1) { FactoryGirl.create(:language) }
|
||||
let(:language2) { FactoryGirl.create(:language) }
|
||||
let(:instrument1) { FactoryGirl.create(:instrument, :description => 'a great instrument')}
|
||||
let(:instrument2) { FactoryGirl.create(:instrument, :description => 'an ok instrument')}
|
||||
|
||||
BIO = "Once a man learned a guitar."
|
||||
GOOD_YOUTUBE_URL = "http://youtube.com/watch?v=1234567890"
|
||||
describe "can create" do
|
||||
it "a simple teacher" do
|
||||
teacher = Teacher.new
|
||||
teacher.user = user;
|
||||
teacher.biography = BIO
|
||||
teacher.introductory_video = GOOD_YOUTUBE_URL
|
||||
teacher.save.should be_true
|
||||
t = Teacher.find(teacher.id)
|
||||
t.biography.should == BIO
|
||||
t.introductory_video.should == GOOD_YOUTUBE_URL
|
||||
end
|
||||
|
||||
|
||||
it "with instruments" do
|
||||
teacher = Teacher.build_teacher(user, {})
|
||||
teacher.instruments << instrument1
|
||||
teacher.instruments << instrument2
|
||||
teacher.save.should be_true
|
||||
t = Teacher.find(teacher.id)
|
||||
t.instruments.should have(2).items
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe "using save_teacher can create" do
|
||||
it "introduction" do
|
||||
teacher = Teacher.save_teacher(
|
||||
user,
|
||||
biography: BIO,
|
||||
introductory_video: GOOD_YOUTUBE_URL,
|
||||
years_teaching: 21,
|
||||
years_playing: 12
|
||||
)
|
||||
teacher.should_not be_nil
|
||||
teacher.errors.should be_empty
|
||||
teacher.id.should_not be_nil
|
||||
t = Teacher.find(teacher.id)
|
||||
t.biography.should == BIO
|
||||
t.introductory_video.should == GOOD_YOUTUBE_URL
|
||||
t.years_teaching.should == 21
|
||||
t.years_playing.should == 12
|
||||
end
|
||||
|
||||
it "basics" do
|
||||
teacher = Teacher.save_teacher(
|
||||
user,
|
||||
instruments: [instrument1, instrument2],
|
||||
subjects: [subject1, subject2],
|
||||
genres: [genre1, genre2],
|
||||
languages: [language1, language2],
|
||||
teaches_age_lower: 10,
|
||||
teaches_age_upper: 20,
|
||||
teaches_beginner: true,
|
||||
teaches_intermediate: false,
|
||||
teaches_advanced: true
|
||||
)
|
||||
|
||||
teacher.should_not be_nil
|
||||
teacher.errors.should be_empty
|
||||
|
||||
t = Teacher.find(teacher.id)
|
||||
|
||||
# Instruments
|
||||
t.instruments.should have(2).items
|
||||
|
||||
# Genres
|
||||
t.genres.should have(2).items
|
||||
|
||||
# Subjects
|
||||
t.subjects.should have(2).items
|
||||
|
||||
# Languages
|
||||
t.languages.should have(2).items
|
||||
|
||||
t.teaches_age_lower.should == 10
|
||||
t.teaches_age_upper.should == 20
|
||||
t.teaches_beginner.should be_true
|
||||
t.teaches_intermediate.should be_false
|
||||
t.teaches_advanced.should be_true
|
||||
|
||||
end
|
||||
|
||||
it "experience" do
|
||||
experience = [{
|
||||
name: "Professor",
|
||||
organization: "SHSU",
|
||||
start_year: 1994,
|
||||
end_year: 2004
|
||||
}
|
||||
]
|
||||
|
||||
teacher = Teacher.save_teacher(user, experiences_teaching: experience)
|
||||
teacher.should_not be_nil
|
||||
teacher.errors.should be_empty
|
||||
|
||||
|
||||
t = Teacher.find(teacher.id)
|
||||
t.should_not be_nil
|
||||
|
||||
|
||||
t.teacher_experiences.should have(1).items
|
||||
t.experiences_teaching.should have(1).items
|
||||
t.experiences_education.should have(0).items
|
||||
t.experiences_award.should have(0).items
|
||||
|
||||
# Save some awards and re-check teacher object:
|
||||
teacher = Teacher.save_teacher(user, experiences_award: experience)
|
||||
teacher.should_not be_nil
|
||||
teacher.errors.should be_empty
|
||||
|
||||
t.reload
|
||||
|
||||
t.teacher_experiences.should have(2).items
|
||||
t.experiences_teaching.should have(1).items
|
||||
t.experiences_education.should have(0).items
|
||||
t.experiences_award.should have(1).items
|
||||
|
||||
|
||||
end
|
||||
|
||||
it "lesson pricing" do
|
||||
teacher = Teacher.save_teacher(
|
||||
user,
|
||||
prices_per_lesson: true,
|
||||
prices_per_month: true,
|
||||
lesson_duration_30: true,
|
||||
lesson_duration_45: true,
|
||||
lesson_duration_60: true,
|
||||
lesson_duration_90: true,
|
||||
lesson_duration_120: true,
|
||||
price_per_lesson_30_cents: 3000,
|
||||
price_per_lesson_45_cents: 3000,
|
||||
price_per_lesson_60_cents: 3000,
|
||||
price_per_lesson_90_cents: 3000,
|
||||
price_per_lesson_120_cents: 3000,
|
||||
price_per_month_30_cents: 5000,
|
||||
price_per_month_45_cents: 5000,
|
||||
price_per_month_60_cents: 5000,
|
||||
price_per_month_90_cents: 5000,
|
||||
price_per_month_120_cents: 5000
|
||||
)
|
||||
|
||||
teacher.should_not be_nil
|
||||
teacher.id.should_not be_nil
|
||||
teacher.errors.should be_empty
|
||||
|
||||
t = Teacher.find(teacher.id)
|
||||
t.prices_per_lesson.should be_true
|
||||
t.prices_per_month.should be_true
|
||||
t.lesson_duration_30.should be_true
|
||||
t.lesson_duration_45.should be_true
|
||||
t.lesson_duration_60.should be_true
|
||||
t.lesson_duration_90.should be_true
|
||||
t.lesson_duration_120.should be_true
|
||||
t.price_per_lesson_30_cents.should == 3000
|
||||
t.price_per_lesson_45_cents.should == 3000
|
||||
t.price_per_lesson_60_cents.should == 3000
|
||||
t.price_per_lesson_90_cents.should == 3000
|
||||
t.price_per_lesson_120_cents.should == 3000
|
||||
t.price_per_month_30_cents.should == 5000
|
||||
t.price_per_month_45_cents.should == 5000
|
||||
t.price_per_month_60_cents.should == 5000
|
||||
t.price_per_month_90_cents.should == 5000
|
||||
t.price_per_month_120_cents.should == 5000
|
||||
end
|
||||
end
|
||||
|
||||
describe "validates" do
|
||||
it "introduction" do
|
||||
teacher = Teacher.save_teacher(
|
||||
user,
|
||||
years_teaching: 21,
|
||||
validate_introduction: true
|
||||
)
|
||||
|
||||
teacher.should_not be_nil
|
||||
teacher.id.should be_nil
|
||||
teacher.errors.should_not be_empty
|
||||
|
||||
teacher.errors.should have_key(:biography)
|
||||
end
|
||||
|
||||
it "introductory video" do
|
||||
teacher = Teacher.save_teacher(
|
||||
user,
|
||||
biography: BIO,
|
||||
introductory_video: "fubar.com/nothing",
|
||||
validate_introduction: true
|
||||
)
|
||||
|
||||
teacher.should_not be_nil
|
||||
teacher.id.should be_nil
|
||||
teacher.errors.should_not be_empty
|
||||
|
||||
teacher.errors.should have_key(:introductory_video)
|
||||
|
||||
teacher = Teacher.save_teacher(
|
||||
user,
|
||||
biography: BIO,
|
||||
introductory_video: GOOD_YOUTUBE_URL,
|
||||
validate_introduction: true
|
||||
)
|
||||
|
||||
teacher.should_not be_nil
|
||||
teacher.id.should_not be_nil
|
||||
teacher.errors.should be_empty
|
||||
|
||||
end
|
||||
|
||||
it "basics" do
|
||||
teacher = Teacher.save_teacher(
|
||||
user,
|
||||
# instruments: [instrument1, instrument2],
|
||||
# subjects: [subject1, subject2],
|
||||
# genres: [genre1, genre2],
|
||||
# languages: [language1, language2],
|
||||
teaches_age_lower: 10,
|
||||
teaches_beginner: true,
|
||||
teaches_intermediate: false,
|
||||
teaches_advanced: true,
|
||||
validate_basics: true
|
||||
)
|
||||
|
||||
teacher.should_not be_nil
|
||||
teacher.id.should be_nil
|
||||
teacher.errors.should have_key(:instruments)
|
||||
teacher.errors.should have_key(:subjects)
|
||||
teacher.errors.should have_key(:genres)
|
||||
teacher.errors.should have_key(:languages)
|
||||
end
|
||||
|
||||
it "pricing" do
|
||||
teacher = Teacher.save_teacher(
|
||||
user,
|
||||
prices_per_lesson: false,
|
||||
prices_per_month: false,
|
||||
lesson_duration_30: false,
|
||||
lesson_duration_45: false,
|
||||
lesson_duration_60: false,
|
||||
lesson_duration_90: false,
|
||||
lesson_duration_120: false,
|
||||
#price_per_lesson_30_cents: 3000,
|
||||
price_per_lesson_45_cents: 3000,
|
||||
#price_per_lesson_60_cents: 3000,
|
||||
#price_per_lesson_90_cents: 3000,
|
||||
price_per_lesson_120_cents: 3000,
|
||||
validate_pricing:true
|
||||
)
|
||||
|
||||
teacher.should_not be_nil
|
||||
teacher.id.should be_nil
|
||||
teacher.errors.should_not be_empty
|
||||
teacher.errors.should have_key(:offer_pricing)
|
||||
teacher.errors.should have_key(:offer_duration)
|
||||
|
||||
teacher = Teacher.save_teacher(
|
||||
user,
|
||||
prices_per_month: true,
|
||||
lesson_duration_45: true,
|
||||
validate_pricing:true
|
||||
)
|
||||
|
||||
teacher.should_not be_nil
|
||||
teacher.id.should_not be_nil
|
||||
teacher.errors.should be_empty
|
||||
|
||||
end # pricing
|
||||
end # validates
|
||||
end # spec
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
Jasmine Javascript Unit Tests
|
||||
=============================
|
||||
|
||||
Open browser to localhost:3000/teaspoon
|
||||
|
||||
Open browser to localhost:3000/teaspoon
|
||||
|
|
@ -170,11 +170,11 @@
|
|||
|
||||
function navToEditProfile() {
|
||||
resetForm()
|
||||
window.location = '/client#/account/profile'
|
||||
window.ProfileActions.startProfileEdit(null, false)
|
||||
}
|
||||
|
||||
function navToEditSubscriptions() {
|
||||
window.location = '/client#/account/profile'
|
||||
window.ProfileActions.startProfileEdit(null, false)
|
||||
}
|
||||
|
||||
function navToEditPayments() {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
(function(context,$) {
|
||||
(function (context, $) {
|
||||
|
||||
"use strict";
|
||||
|
||||
context.JK = context.JK || {};
|
||||
context.JK.AccountProfileScreen = function(app) {
|
||||
context.JK.AccountProfileScreen = function (app) {
|
||||
var $document = $(document);
|
||||
var logger = context.JK.logger;
|
||||
var EVENTS = context.JK.EVENTS;
|
||||
|
|
@ -32,12 +32,18 @@
|
|||
var $btnSubmit = $screen.find('.account-edit-profile-submit');
|
||||
|
||||
function beforeShow(data) {
|
||||
userId = data.id;
|
||||
userId = data.id;
|
||||
}
|
||||
|
||||
function afterShow(data) {
|
||||
resetForm();
|
||||
renderAccountProfile();
|
||||
if (window.ProfileStore.solo) {
|
||||
$btnSubmit.text('SAVE & RETURN TO PROFILE');
|
||||
}
|
||||
else {
|
||||
$btnSubmit.text('SAVE & NEXT');
|
||||
}
|
||||
resetForm();
|
||||
renderAccountProfile();
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
|
|
@ -64,7 +70,7 @@
|
|||
var content_root = $('#account-profile-content-scroller');
|
||||
|
||||
// set birth_date
|
||||
if(userDetail.birth_date) {
|
||||
if (userDetail.birth_date) {
|
||||
var birthDateFields = userDetail.birth_date.split('-')
|
||||
var birthDateYear = birthDateFields[0];
|
||||
var birthDateMonth = birthDateFields[1];
|
||||
|
|
@ -79,8 +85,8 @@
|
|||
}
|
||||
|
||||
function populateAccountProfileLocation(userDetail, regions, cities) {
|
||||
populateRegions(regions, userDetail.state);
|
||||
populateCities(cities, userDetail.city);
|
||||
populateRegions(regions, userDetail.state);
|
||||
populateCities(cities, userDetail.city);
|
||||
}
|
||||
|
||||
function populateCountries(countries, userCountry) {
|
||||
|
|
@ -94,21 +100,21 @@
|
|||
nilOption.text(nilOptionText);
|
||||
countrySelect.append(nilOption);
|
||||
|
||||
$.each(countries, function(index, country) {
|
||||
if(!country) return;
|
||||
$.each(countries, function (index, country) {
|
||||
if (!country) return;
|
||||
|
||||
var option = $(nilOptionStr);
|
||||
option.text(country);
|
||||
option.attr("value", country);
|
||||
|
||||
if(country == userCountry) {
|
||||
if (country == userCountry) {
|
||||
foundCountry = true;
|
||||
}
|
||||
|
||||
countrySelect.append(option);
|
||||
});
|
||||
|
||||
if(!foundCountry) {
|
||||
if (!foundCountry) {
|
||||
// in this case, the user has a country that is not in the database
|
||||
// this can happen in a development/test scenario, but let's assume it can
|
||||
// happen in production too.
|
||||
|
|
@ -137,21 +143,21 @@
|
|||
nilOption.text(nilOptionText);
|
||||
countrySelect.append(nilOption);
|
||||
|
||||
$.each(countriesx, function(index, countryx) {
|
||||
$.each(countriesx, function (index, countryx) {
|
||||
if (!countryx.countrycode) return;
|
||||
|
||||
var option = $(nilOptionStr);
|
||||
option.text(countryx.countryname);
|
||||
option.attr("value", countryx.countrycode);
|
||||
|
||||
if(countryx.countrycode == userCountry) {
|
||||
if (countryx.countrycode == userCountry) {
|
||||
foundCountry = true;
|
||||
}
|
||||
|
||||
countrySelect.append(option);
|
||||
});
|
||||
|
||||
if(!foundCountry) {
|
||||
if (!foundCountry) {
|
||||
// in this case, the user has a country that is not in the database
|
||||
// this can happen in a development/test scenario, but let's assume it can
|
||||
// happen in production too.
|
||||
|
|
@ -175,8 +181,8 @@
|
|||
nilOption.text(nilOptionText);
|
||||
regionSelect.append(nilOption);
|
||||
|
||||
$.each(regions, function(index, region) {
|
||||
if(!region) return;
|
||||
$.each(regions, function (index, region) {
|
||||
if (!region) return;
|
||||
|
||||
var option = $(nilOptionStr);
|
||||
option.text(region['name']);
|
||||
|
|
@ -199,8 +205,8 @@
|
|||
nilOption.text(nilOptionText);
|
||||
citySelect.append(nilOption);
|
||||
|
||||
$.each(cities, function(index, city) {
|
||||
if(!city) return;
|
||||
$.each(cities, function (index, city) {
|
||||
if (!city) return;
|
||||
|
||||
var option = $(nilOptionStr);
|
||||
option.text(city);
|
||||
|
|
@ -218,27 +224,32 @@
|
|||
/****************** MAIN PORTION OF SCREEN *****************/
|
||||
// events for main screen
|
||||
function events() {
|
||||
$btnCancel.click(function(evt) {
|
||||
$btnCancel.click(function (evt) {
|
||||
evt.stopPropagation();
|
||||
navToAccount();
|
||||
window.ProfileActions.cancelProfileEdit()
|
||||
return false;
|
||||
});
|
||||
|
||||
$('#account-profile-content-scroller').on('click', '#account-change-avatar', function (evt) {
|
||||
evt.stopPropagation();
|
||||
navToAvatar();
|
||||
return false;
|
||||
});
|
||||
|
||||
$('#account-profile-content-scroller').on('click', '#account-change-avatar', function(evt) { evt.stopPropagation(); navToAvatar(); return false; } );
|
||||
|
||||
enableSubmits();
|
||||
}
|
||||
|
||||
function renderAccountProfile() {
|
||||
|
||||
$.when(api.getUserProfile())
|
||||
.done(function(userDetail) {
|
||||
.done(function (userDetail) {
|
||||
recentUserDetail = userDetail;
|
||||
populateAccountProfile(userDetail);
|
||||
|
||||
selectLocation = new context.JK.SelectLocation(getCountryElement(), getRegionElement(), getCityElement(), app);
|
||||
selectLocation.load(userDetail.country, userDetail.state, userDetail.city)
|
||||
});
|
||||
|
||||
|
||||
context.JK.dropdown($('select'));
|
||||
}
|
||||
|
||||
|
|
@ -277,20 +288,24 @@
|
|||
biography: biography,
|
||||
subscribe_email: subscribeEmail
|
||||
})
|
||||
.done(postUpdateProfileSuccess)
|
||||
.fail(postUpdateProfileFailure)
|
||||
.always(enableSubmits)
|
||||
.done(postUpdateProfileSuccess)
|
||||
.fail(postUpdateProfileFailure)
|
||||
.always(enableSubmits)
|
||||
}
|
||||
|
||||
function enableSubmits() {
|
||||
$btnSubmit.click(function(evt) {
|
||||
$btnSubmit.click(function (evt) {
|
||||
evt.stopPropagation();
|
||||
handleUpdateProfile();
|
||||
return false;
|
||||
});
|
||||
$btnSubmit.removeClass("disabled");
|
||||
$('#account-profile-content-scroller').on('submit', '#account-edit-email-form', function(evt) { evt.stopPropagation(); handleUpdateProfile(); return false; } );
|
||||
$("#account-edit-email-form").removeClass("disabled");
|
||||
$btnSubmit.removeClass("disabled");
|
||||
$('#account-profile-content-scroller').on('submit', '#account-edit-email-form', function (evt) {
|
||||
evt.stopPropagation();
|
||||
handleUpdateProfile();
|
||||
return false;
|
||||
});
|
||||
$("#account-edit-email-form").removeClass("disabled");
|
||||
}
|
||||
|
||||
function disableSubmits() {
|
||||
|
|
@ -302,14 +317,14 @@
|
|||
|
||||
function postUpdateProfileSuccess(response) {
|
||||
$document.triggerHandler(EVENTS.USER_UPDATED, response);
|
||||
context.location = "/client#/account/profile/experience";
|
||||
window.ProfileActions.editProfileNext('experience');
|
||||
}
|
||||
|
||||
function postUpdateProfileFailure(xhr, textStatus, errorMessage) {
|
||||
|
||||
var errors = JSON.parse(xhr.responseText)
|
||||
|
||||
if(xhr.status == 422) {
|
||||
if (xhr.status == 422) {
|
||||
var first_name = context.JK.format_errors("first_name", errors);
|
||||
var last_name = context.JK.format_errors("last_name", errors);
|
||||
var country = context.JK.format_errors("country", errors);
|
||||
|
|
@ -320,41 +335,41 @@
|
|||
var subscribeEmail = context.JK.format_errors("subscribe_email", errors);
|
||||
var biography = context.JK.format_errors("biography", errors);
|
||||
|
||||
if(first_name != null) {
|
||||
if (first_name != null) {
|
||||
getFirstNameElement().closest('div.field').addClass('error').end().after(first_name);
|
||||
}
|
||||
|
||||
if(last_name != null) {
|
||||
if (last_name != null) {
|
||||
getLastNameElement().closest('div.field').addClass('error').end().after(last_name);
|
||||
}
|
||||
|
||||
if(country != null) {
|
||||
if (country != null) {
|
||||
getCountryElement().closest('div.field').addClass('error').end().after(country);
|
||||
}
|
||||
|
||||
if(state != null) {
|
||||
if (state != null) {
|
||||
getRegionElement().closest('div.field').addClass('error').end().after(state);
|
||||
}
|
||||
|
||||
if(city != null) {
|
||||
if (city != null) {
|
||||
getCityElement().closest('div.field').addClass('error').end().after(city);
|
||||
}
|
||||
|
||||
if(birth_date != null) {
|
||||
if (birth_date != null) {
|
||||
getYearElement().closest('div.field').addClass('error').end().after(birth_date);
|
||||
}
|
||||
|
||||
if(subscribeEmail != null) {
|
||||
if (subscribeEmail != null) {
|
||||
getSubscribeEmail().closest('div.field').addClass('error').end().after(subscribeEmail);
|
||||
}
|
||||
|
||||
if(gender != null) {
|
||||
if (gender != null) {
|
||||
getGenderElement().closest('div.field').addClass('error').end().after(gender);
|
||||
}
|
||||
}
|
||||
else {
|
||||
app.ajaxError(xhr, textStatus, errorMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleCountryChanged() {
|
||||
|
|
@ -376,9 +391,9 @@
|
|||
regionElement.children().remove()
|
||||
regionElement.append($(nilOptionStr).text('loading...'))
|
||||
|
||||
api.getRegions({ country: selectedCountry })
|
||||
api.getRegions({country: selectedCountry})
|
||||
.done(getRegionsDone)
|
||||
.error(function(err) {
|
||||
.error(function (err) {
|
||||
regionElement.children().remove()
|
||||
regionElement.append($(nilOptionStr).text(nilOptionText))
|
||||
})
|
||||
|
|
@ -404,14 +419,14 @@
|
|||
cityElement.children().remove();
|
||||
cityElement.append($(nilOptionStr).text('loading...'));
|
||||
|
||||
api.getCities({ country: selectedCountry, region: selectedRegion })
|
||||
api.getCities({country: selectedCountry, region: selectedRegion})
|
||||
.done(getCitiesDone)
|
||||
.error(function(err) {
|
||||
cityElement.children().remove();
|
||||
cityElement.append($(nilOptionStr).text(nilOptionText));
|
||||
.error(function (err) {
|
||||
cityElement.children().remove();
|
||||
cityElement.append($(nilOptionStr).text(nilOptionText));
|
||||
})
|
||||
.always(function () {
|
||||
loadingCitiesData = false;
|
||||
loadingCitiesData = false;
|
||||
});
|
||||
}
|
||||
else {
|
||||
|
|
@ -483,7 +498,7 @@
|
|||
var day = getDayElement().val()
|
||||
var year = getYearElement().val()
|
||||
|
||||
if(month != null && month.length > 0 && day != null && day.length > 0 && year != null && year.length > 0) {
|
||||
if (month != null && month.length > 0 && day != null && day.length > 0 && year != null && year.length > 0) {
|
||||
return month + "-" + day + "-" + year;
|
||||
}
|
||||
else {
|
||||
|
|
@ -506,4 +521,4 @@
|
|||
return this;
|
||||
};
|
||||
|
||||
})(window,jQuery);
|
||||
})(window, jQuery);
|
||||
|
|
@ -21,6 +21,16 @@
|
|||
}
|
||||
|
||||
function afterShow(data) {
|
||||
|
||||
if (window.ProfileStore.solo) {
|
||||
$btnBack.hide()
|
||||
$btnSubmit.text('SAVE & RETURN TO PROFILE');
|
||||
}
|
||||
else {
|
||||
$btnBack.show()
|
||||
$btnSubmit.text('SAVE & NEXT');
|
||||
}
|
||||
|
||||
resetForm();
|
||||
renderExperience();
|
||||
}
|
||||
|
|
@ -115,7 +125,7 @@
|
|||
function events() {
|
||||
$btnCancel.click(function(evt) {
|
||||
evt.stopPropagation();
|
||||
navigateTo('/client#/profile/' + context.JK.currentUserId);
|
||||
window.ProfileActions.cancelProfileEdit()
|
||||
return false;
|
||||
});
|
||||
|
||||
|
|
@ -179,7 +189,7 @@
|
|||
|
||||
function postUpdateProfileSuccess(response) {
|
||||
$document.triggerHandler(EVENTS.USER_UPDATED, response);
|
||||
context.location = "/client#/account/profile/interests";
|
||||
ProfileActions.editProfileNext('interests')
|
||||
}
|
||||
|
||||
function postUpdateProfileFailure(xhr, textStatus, errorMessage) {
|
||||
|
|
|
|||
|
|
@ -70,7 +70,16 @@
|
|||
}
|
||||
|
||||
function afterShow(data) {
|
||||
renderInterests()
|
||||
if (window.ProfileStore.solo) {
|
||||
$btnBack.hide()
|
||||
$btnSubmit.text('SAVE & RETURN TO PROFILE');
|
||||
}
|
||||
else {
|
||||
$btnBack.show()
|
||||
$btnSubmit.text('SAVE & NEXT');
|
||||
}
|
||||
|
||||
renderInterests()
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
|
|
@ -187,7 +196,7 @@
|
|||
|
||||
$btnCancel.click(function(e) {
|
||||
e.stopPropagation()
|
||||
navigateTo('/client#/profile/' + context.JK.currentUserId)
|
||||
window.ProfileActions.cancelProfileEdit()
|
||||
return false
|
||||
})
|
||||
|
||||
|
|
@ -310,7 +319,7 @@
|
|||
|
||||
function postUpdateProfileSuccess(response) {
|
||||
$document.triggerHandler(EVENTS.USER_UPDATED, response)
|
||||
context.location = "/client#/account/profile/samples"
|
||||
ProfileActions.editProfileNext('samples')
|
||||
}
|
||||
|
||||
function postUpdateProfileFailure(xhr, textStatus, errorMessage) {
|
||||
|
|
|
|||
|
|
@ -43,7 +43,6 @@
|
|||
var $btnBack = parent.find('.account-edit-profile-back')
|
||||
var $btnSubmit = parent.find('.account-edit-profile-submit')
|
||||
|
||||
|
||||
var urlValidator=null
|
||||
var soundCloudValidator=null
|
||||
var reverbNationValidator=null
|
||||
|
|
@ -59,9 +58,18 @@
|
|||
}
|
||||
|
||||
function afterShow(data) {
|
||||
if (window.ProfileStore.solo) {
|
||||
$btnBack.hide()
|
||||
$btnSubmit.text('SAVE & RETURN TO PROFILE');
|
||||
}
|
||||
else {
|
||||
$btnBack.show()
|
||||
$btnSubmit.text('SAVE & FINISH');
|
||||
}
|
||||
|
||||
$.when(loadFn())
|
||||
.done(function(targetPlayer) {
|
||||
if (targetPlayer && targetPlayer.keys && targetPlayer.keys.length > 0) {
|
||||
if (targetPlayer) {
|
||||
renderPlayer(targetPlayer)
|
||||
}
|
||||
})
|
||||
|
|
@ -161,6 +169,10 @@
|
|||
}
|
||||
|
||||
function buildNonJamKazamEntry($sampleList, type, source) {
|
||||
|
||||
// remove anything that matches
|
||||
$sampleList.find('[data-recording-id=' + source.recording_id + ']').remove();
|
||||
|
||||
// TODO: this code is repeated in HTML file
|
||||
var recordingIdAttr = ' data-recording-id="' + source.recording_id + '" ';
|
||||
var recordingUrlAttr = ' data-recording-url="' + source.url + '" ';
|
||||
|
|
@ -207,7 +219,9 @@
|
|||
|
||||
$btnCancel.click(function(evt) {
|
||||
evt.stopPropagation();
|
||||
navigateTo('/client#/profile/' + context.JK.currentUserId);
|
||||
|
||||
window.ProfileActions.cancelProfileEdit()
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
|
|
@ -334,7 +348,7 @@
|
|||
|
||||
function postUpdateProfileSuccess(response) {
|
||||
$document.triggerHandler(EVENTS.USER_UPDATED, response);
|
||||
context.location = "/client#/profile/" + context.JK.currentUserId;
|
||||
ProfileActions.doneProfileEdit()
|
||||
}
|
||||
|
||||
function postUpdateProfileFailure(xhr, textStatus, errorMessage) {
|
||||
|
|
@ -387,7 +401,7 @@
|
|||
|
||||
|
||||
setTimeout(function() {
|
||||
urlValidator = new JK.SiteValidator('url', userNameSuccessCallback, userNameFailCallback, parent)
|
||||
urlValidator = new JK.SiteValidator('url', websiteSuccessCallback, userNameFailCallback, parent)
|
||||
urlValidator.init()
|
||||
|
||||
soundCloudValidator = new JK.SiteValidator('soundcloud', userNameSuccessCallback, userNameFailCallback, parent)
|
||||
|
|
@ -429,6 +443,13 @@
|
|||
$inputDiv.append("<span class='error-text'>Invalid username</span>").show();
|
||||
}
|
||||
|
||||
function websiteSuccessCallback($inputDiv) {
|
||||
$inputDiv.addClass('error');
|
||||
$inputDiv.find('.error-text').remove();
|
||||
$inputDiv.append("<span class='error-text'>Invalid URL</span>").show();
|
||||
}
|
||||
|
||||
|
||||
function soundCloudSuccessCallback($inputDiv) {
|
||||
siteSuccessCallback($inputDiv, soundCloudRecordingValidator, $soundCloudSampleList, 'soundcloud');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@
|
|||
//= require jquery.exists
|
||||
//= require jquery.payment
|
||||
//= require jquery.visible
|
||||
//= require jquery.jstarbox
|
||||
//= require classnames
|
||||
//= require reflux
|
||||
//= require howler.core.js
|
||||
|
|
@ -51,6 +52,7 @@
|
|||
//= require ga
|
||||
//= require utils
|
||||
//= require subscription_utils
|
||||
//= require profile_utils
|
||||
//= require custom_controls
|
||||
//= require react
|
||||
//= require react_ujs
|
||||
|
|
|
|||
|
|
@ -476,6 +476,58 @@
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
function getTeacher(options) {
|
||||
// var url = '/api/teacher/detail'
|
||||
|
||||
// if(options && _.size(options) > 0) {
|
||||
// console.log("WTF");
|
||||
// url += "?" + $.param(options)
|
||||
// }
|
||||
|
||||
// console.log("THE URL", url)
|
||||
return $.ajax({
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
url: '/api/teachers/detail?'+ $.param(options),
|
||||
contentType: 'application/json',
|
||||
processData: false
|
||||
});
|
||||
}
|
||||
|
||||
function deleteTeacher(teacherId) {
|
||||
var url = "/api/teachers/" + teacherId;
|
||||
return $.ajax({
|
||||
type: "DELETE",
|
||||
dataType: "json",
|
||||
url: url,
|
||||
contentType: 'application/json',
|
||||
processData:false
|
||||
});
|
||||
}
|
||||
|
||||
function updateTeacher(teacher) {
|
||||
console.log("Updating teacher", teacher)
|
||||
var id = teacher && teacher["id"]
|
||||
var url
|
||||
if (id != null && typeof(id) != 'undefined') {
|
||||
url = '/api/teachers/' + teacher.id
|
||||
} else {
|
||||
url = '/api/teachers'
|
||||
}
|
||||
|
||||
var deferred = $.ajax({
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
url: url,
|
||||
contentType: 'application/json',
|
||||
processData: false,
|
||||
data: JSON.stringify(teacher)
|
||||
})
|
||||
|
||||
return deferred
|
||||
}
|
||||
|
||||
function getSession(id) {
|
||||
var url = "/api/sessions/" + id;
|
||||
return $.ajax({
|
||||
|
|
@ -530,13 +582,16 @@
|
|||
}
|
||||
|
||||
function getUserDetail(options) {
|
||||
if(!options) {
|
||||
options = {}
|
||||
}
|
||||
var id = getId(options);
|
||||
var detail = null;
|
||||
if (id != null && typeof(id) != 'undefined') {
|
||||
detail = $.ajax({
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
url: "/api/users/" + id,
|
||||
url: "/api/users/" + id + '?' + $.param(options),
|
||||
processData: false
|
||||
});
|
||||
}
|
||||
|
|
@ -549,11 +604,14 @@
|
|||
}
|
||||
|
||||
function getUserProfile(options) {
|
||||
if (!options) {
|
||||
options = {}
|
||||
}
|
||||
var id = getId(options);
|
||||
return $.ajax({
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
url: "/api/users/" + id + "/profile",
|
||||
url: "/api/users/" + id + "/profile" + '?' + $.param(options),
|
||||
processData: false
|
||||
});
|
||||
}
|
||||
|
|
@ -677,6 +735,20 @@
|
|||
});
|
||||
}
|
||||
|
||||
function getSubjects(options) {
|
||||
return $.ajax('/api/subjects', {
|
||||
data: { },
|
||||
dataType: 'json'
|
||||
});
|
||||
}
|
||||
|
||||
function getLanguages(options) {
|
||||
return $.ajax('/api/languages', {
|
||||
data: { },
|
||||
dataType: 'json'
|
||||
});
|
||||
}
|
||||
|
||||
function updateUdpReachable(options) {
|
||||
var id = getId(options);
|
||||
|
||||
|
|
@ -1016,7 +1088,7 @@
|
|||
type: 'GET',
|
||||
dataType: "json",
|
||||
url: "/api/feeds?" + $.param(options),
|
||||
processData:false
|
||||
processData:false
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -2077,6 +2149,8 @@
|
|||
this.getResolvedLocation = getResolvedLocation;
|
||||
this.getInstruments = getInstruments;
|
||||
this.getGenres = getGenres;
|
||||
this.getSubjects = getSubjects;
|
||||
this.getLanguages = getLanguages;
|
||||
this.updateUdpReachable = updateUdpReachable;
|
||||
this.updateNetworkTesting = updateNetworkTesting;
|
||||
this.updateAvatar = updateAvatar;
|
||||
|
|
@ -2169,6 +2243,9 @@
|
|||
this.getBandPhotoFilepickerPolicy = getBandPhotoFilepickerPolicy;
|
||||
this.getBand = getBand;
|
||||
this.validateBand = validateBand;
|
||||
this.getTeacher = getTeacher;
|
||||
this.updateTeacher = updateTeacher;
|
||||
this.deleteTeacher = deleteTeacher;
|
||||
this.updateFavorite = updateFavorite;
|
||||
this.createBandInvitation = createBandInvitation;
|
||||
this.updateBandInvitation = updateBandInvitation;
|
||||
|
|
|
|||
|
|
@ -96,6 +96,8 @@
|
|||
|
||||
// buttons
|
||||
var $btnEdit = $screen.find('.edit-profile-btn');
|
||||
var $btnTeacherProfileEdit = $screen.find('.edit-teacher-profile-btn');
|
||||
var $btnTeacherProfileView = $screen.find('.view-teacher-profile-btn');
|
||||
var $btnAddFriend = $screen.find('#btn-add-friend');
|
||||
var $btnFollowUser = $screen.find('#btn-follow-user');
|
||||
var $btnMessageUser = $screen.find('#btn-message-user');
|
||||
|
|
@ -103,6 +105,7 @@
|
|||
var $btnAddRecordings = $screen.find('.add-recordings');
|
||||
var $btnAddSites = $screen.find('.add-sites');
|
||||
var $btnAddInterests = $screen.find('.add-interests');
|
||||
var $btnAddExperiences = $screen.find('.add-experiences')
|
||||
|
||||
// social
|
||||
var $socialLeft = $screen.find('.profile-social-left');
|
||||
|
|
@ -144,7 +147,7 @@
|
|||
user = null;
|
||||
decrementedFriendCountOnce = false;
|
||||
sentFriendRequest = false;
|
||||
userDefer = rest.getUserProfile({id: userId})
|
||||
userDefer = rest.getUserProfile({id: userId, show_teacher:true})
|
||||
.done(function (response) {
|
||||
user = response;
|
||||
configureUserType();
|
||||
|
|
@ -168,6 +171,10 @@
|
|||
return user.musician;
|
||||
}
|
||||
|
||||
function isTeacher() {
|
||||
return user.teacher;
|
||||
}
|
||||
|
||||
function isCurrentUser() {
|
||||
return userId === context.JK.currentUserId;
|
||||
}
|
||||
|
|
@ -199,6 +206,13 @@
|
|||
|
||||
if (isCurrentUser()) {
|
||||
$btnEdit.show();
|
||||
$btnTeacherProfileEdit.show();
|
||||
if(isTeacher()) {
|
||||
$btnTeacherProfileView.show();
|
||||
}
|
||||
else {
|
||||
$btnTeacherProfileView.hide();
|
||||
}
|
||||
$btnAddFriend.hide();
|
||||
$btnFollowUser.hide();
|
||||
$btnMessageUser.hide();
|
||||
|
|
@ -206,6 +220,8 @@
|
|||
configureFriendFollowersControls();
|
||||
|
||||
$btnEdit.hide();
|
||||
$btnTeacherProfileEdit.hide();
|
||||
$btnTeacherProfileView.show();
|
||||
$btnAddFriend.show();
|
||||
$btnFollowUser.show();
|
||||
$btnMessageUser.show();
|
||||
|
|
@ -251,6 +267,47 @@
|
|||
|
||||
// Hook up soundcloud player:
|
||||
$soundCloudSamples.off("click", "a.sound-cloud-playable") .on("click", "a.sound-cloud-playable", playSoundCloudFile)
|
||||
|
||||
$btnEdit.click(function(e) {
|
||||
e.preventDefault()
|
||||
window.ProfileActions.startProfileEdit(null, false)
|
||||
return false;
|
||||
})
|
||||
$btnTeacherProfileEdit.click(function(e) {
|
||||
e.preventDefault()
|
||||
window.ProfileActions.startTeacherEdit(null, false)
|
||||
return false;
|
||||
})
|
||||
$btnTeacherProfileView.click(function(e) {
|
||||
e.preventDefault()
|
||||
context.location = '/client#/profile/teacher/' + context.JK.currentUserId;
|
||||
return false;
|
||||
})
|
||||
$btnEditBio.click(function(e) {
|
||||
e.preventDefault()
|
||||
window.ProfileActions.startProfileEdit(null, true)
|
||||
return false;
|
||||
})
|
||||
$btnAddRecordings.click(function(e) {
|
||||
e.preventDefault()
|
||||
window.ProfileActions.startProfileEdit('samples', true)
|
||||
return false;
|
||||
})
|
||||
$btnAddSites.click(function(e) {
|
||||
e.preventDefault()
|
||||
window.ProfileActions.startProfileEdit('samples', true)
|
||||
return false;
|
||||
})
|
||||
$btnAddInterests.click(function(e) {
|
||||
e.preventDefault()
|
||||
window.ProfileActions.startProfileEdit('interests', true)
|
||||
return false;
|
||||
});
|
||||
$btnAddExperiences.click(function(e) {
|
||||
e.preventDefault()
|
||||
window.ProfileActions.startProfileEdit('experience', true)
|
||||
return false;
|
||||
})
|
||||
}
|
||||
|
||||
function playSoundCloudFile(e) {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@
|
|||
|
||||
var NOT_SPECIFIED_TEXT = 'Not specified';
|
||||
|
||||
profileUtils.NOT_SPECIFIED_TEXT = NOT_SPECIFIED_TEXT
|
||||
|
||||
var proficiencyDescriptionMap = {
|
||||
"1": "BEGINNER",
|
||||
"2": "INTERMEDIATE",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,11 @@
|
|||
//= require_directory ./react-components/helpers
|
||||
//= require_directory ./react-components/actions
|
||||
//= require ./react-components/stores/AppStore
|
||||
//= require ./react-components/stores/InstrumentStore
|
||||
//= require ./react-components/stores/LanguageStore
|
||||
//= require ./react-components/stores/GenreStore
|
||||
//= require ./react-components/stores/SubjectStore
|
||||
//= require ./react-components/stores/ProfileStore
|
||||
//= require ./react-components/stores/PlatformStore
|
||||
//= require ./react-components/stores/BrowserMediaStore
|
||||
//= require ./react-components/stores/RecordingStore
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
context = window
|
||||
rest = window.JK.Rest()
|
||||
logger = context.JK.logger
|
||||
|
||||
@AgeRangeList = React.createClass({
|
||||
ages: [0, 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100]
|
||||
|
||||
componentDidMount: () ->
|
||||
@agesJsx = []
|
||||
for age in @ages
|
||||
@agesJsx.push(`<option value={age}>{age == 0 ? 'Any' : age}</option>`)
|
||||
|
||||
getInitialState: () ->
|
||||
{selectedAge:@props.selectedAge}
|
||||
|
||||
componentWillReceiveProps: (nextProps) ->
|
||||
@setState({selectedAge: nextProps.selectedAge})
|
||||
|
||||
onChanged: (e) ->
|
||||
val = $(e.target).val()
|
||||
@setState({selectedAge: val })
|
||||
this.props.onItemChanged(this.props.objectName, val)
|
||||
|
||||
render: () ->
|
||||
`<select className="AgeRangeList react-component" onChange={this.onChanged} value={this.state.selectedAge}>
|
||||
{this.agesJsx}
|
||||
</select>`
|
||||
})
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
context = window
|
||||
rest = window.JK.Rest()
|
||||
logger = context.JK.logger
|
||||
|
||||
@CheckBoxList = React.createClass({
|
||||
objects: []
|
||||
|
||||
onItemChanged: (e) ->
|
||||
# e.preventDefault()
|
||||
|
||||
selectedObjects = @selectedObjects()
|
||||
@setState({selectedObjects: selectedObjects})
|
||||
this.props.onItemChanged(this.props.objectName, selectedObjects)
|
||||
|
||||
selectedObjects: ->
|
||||
selected=[]
|
||||
@root = jQuery(this.getDOMNode())
|
||||
$(".checkItem input[type=checkbox]:checked", @root).each ->
|
||||
selected.push $(this).data("object-id")
|
||||
selected
|
||||
|
||||
render: () ->
|
||||
object_options = []
|
||||
|
||||
for object in this.props.sourceObjects
|
||||
nm = "check_#{object.id}"
|
||||
checked = @isChecked(object.id)
|
||||
object_options.push `<div className='checkItem'><input type='checkbox' key={object.id} name={nm} data-object-id={object.id} onChange={this.onItemChanged} checked={checked}></input><label htmlFor={nm}>{object.description}</label></div>`
|
||||
|
||||
`<div className="CheckBoxList react-component">
|
||||
<div className="checkbox-scroller left">
|
||||
{object_options}
|
||||
</div>
|
||||
</div>`
|
||||
|
||||
isChecked: (id) ->
|
||||
this.state.selectedObjects? && id in this.state.selectedObjects
|
||||
|
||||
|
||||
getInitialState: () ->
|
||||
{selectedObjects:@props.selectedObjects}
|
||||
|
||||
componentWillReceiveProps: (nextProps) ->
|
||||
@setState({selectedObjects: nextProps.selectedObjects})
|
||||
|
||||
})
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
context = window
|
||||
rest = window.JK.Rest()
|
||||
logger = context.JK.logger
|
||||
|
||||
@EditableList = React.createClass({
|
||||
objects: []
|
||||
|
||||
listObjects: ->
|
||||
objs=[]
|
||||
@root = jQuery(this.getDOMNode())
|
||||
$(".list-item", @root).each ->
|
||||
objs.push $(this).data("object-id")
|
||||
objs
|
||||
|
||||
deleteItem: (i, e) ->
|
||||
e.preventDefault()
|
||||
|
||||
this.props.listItems.splice(i,1)
|
||||
this.props.onItemChanged(this.props.objectName, this.props.listItems)
|
||||
|
||||
render: () ->
|
||||
object_options = []
|
||||
|
||||
logger.debug("Rendering EditableList", this.props, this.props.listItems)
|
||||
|
||||
if this.props.listItems? && this.props.listItems.length > 0
|
||||
for object,i in this.props.listItems
|
||||
nm = "item_#{i}"
|
||||
displayValue = this.props.formatListItem(object)
|
||||
object_options.push `<div className='list-item'>
|
||||
<div className='display-value left'>{displayValue}</div>
|
||||
<div className='actions'>
|
||||
<a className='delete-list-item right' onClick={this.deleteItem.bind(this, i)} >X</a>
|
||||
</div>
|
||||
</div>`
|
||||
else
|
||||
object_options.push `<div className='display-value'><em>None</em></div>`
|
||||
|
||||
`<div className="EditableList react-component">
|
||||
<div className="editable-scroller left">
|
||||
{object_options}
|
||||
</div>
|
||||
</div>`
|
||||
|
||||
})
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
context = window
|
||||
rest = window.JK.Rest()
|
||||
logger = context.JK.logger
|
||||
|
||||
@GenreCheckBoxList = React.createClass({
|
||||
|
||||
mixins: [Reflux.listenTo(@GenreStore,"onGenresChanged")]
|
||||
|
||||
getInitialState:() ->
|
||||
{genres: []}
|
||||
|
||||
onGenresChanged: (genres) ->
|
||||
@setState({genres: genres})
|
||||
|
||||
|
||||
render: () ->
|
||||
`<div className="GenreCheckBoxList react-component">
|
||||
<CheckBoxList objectName='genres' onItemChanged={this.props.onItemChanged} sourceObjects={this.state.genres} selectedObjects={this.props.selectedGenres}/>
|
||||
</div>`
|
||||
})
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
context = window
|
||||
rest = window.JK.Rest()
|
||||
logger = context.JK.logger
|
||||
|
||||
@InstrumentCheckBoxList = React.createClass({
|
||||
|
||||
mixins: [Reflux.listenTo(@InstrumentStore,"onInstrumentsChanged")]
|
||||
|
||||
getInitialState: () ->
|
||||
{instruments: []}
|
||||
|
||||
onInstrumentsChanged: (instruments) ->
|
||||
@setState({instruments: instruments})
|
||||
|
||||
render: () ->
|
||||
`<div className="InstrumentCheckBoxList react-component">
|
||||
<CheckBoxList objectName='instruments' onItemChanged={this.props.onItemChanged} sourceObjects={this.state.instruments} selectedObjects={this.props.selectedInstruments}/>
|
||||
</div>`
|
||||
})
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
context = window
|
||||
rest = window.JK.Rest()
|
||||
logger = context.JK.logger
|
||||
|
||||
@LanguageCheckBoxList = React.createClass({
|
||||
|
||||
mixins: [Reflux.listenTo(@LanguageStore,"onLanguagesChanged")]
|
||||
|
||||
getInitialState: () ->
|
||||
{languages: []}
|
||||
|
||||
onLanguagesChanged: (languages) ->
|
||||
@setState({languages: languages})
|
||||
|
||||
render: () ->
|
||||
`<div className="LanguageCheckBoxList react-component">
|
||||
<CheckBoxList objectName='languages' onItemChanged={this.props.onItemChanged} sourceObjects={this.state.languages} selectedObjects={this.props.selectedLanguages}/>
|
||||
</div>`
|
||||
})
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
context = window
|
||||
rest = window.JK.Rest()
|
||||
logger = context.JK.logger
|
||||
|
||||
@SubjectCheckBoxList = React.createClass({
|
||||
|
||||
mixins: [Reflux.listenTo(@SubjectStore,"onSubjectsChanged")]
|
||||
|
||||
getInitialState:() ->
|
||||
{subjects: []}
|
||||
|
||||
onSubjectsChanged: (subjects) ->
|
||||
@setState({subjects: subjects})
|
||||
|
||||
render: () ->
|
||||
`<div className="SubjectCheckBoxList react-component">
|
||||
<CheckBoxList objectName='subjects' onItemChanged={this.props.onItemChanged} sourceObjects={this.state.subjects} selectedObjects={this.props.selectedSubjects}/>
|
||||
</div>`
|
||||
})
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
context = window
|
||||
rest = window.JK.Rest()
|
||||
logger = context.JK.logger
|
||||
|
||||
@TeacherExperienceEditableList = React.createClass({
|
||||
componentDidUnmount: () ->
|
||||
@root.off("submit", ".teacher-experience-teaching-form")
|
||||
|
||||
componentDidMount: () ->
|
||||
@root = jQuery(this.getDOMNode())
|
||||
@root.off("submit", ".teacher-experience-teaching-form").on("submit", ".teacher-experience-teaching-form", @addExperience)
|
||||
|
||||
formatListItem: (obj) ->
|
||||
t = "#{obj.name}/#{obj.organization} (#{obj.start_year}"
|
||||
t += "-#{obj.end_year}" if this.props.showEndDate
|
||||
t += ")"
|
||||
|
||||
getInitialProps: () ->
|
||||
{listItems: []}
|
||||
|
||||
sortListItems: () ->
|
||||
this.props.listItems ||= []
|
||||
this.props.listItems = _.sortBy(this.props.listItems, 'start_year')
|
||||
|
||||
addExperience: (e) ->
|
||||
e.preventDefault()
|
||||
logger.debug("addExperience", this.props.listItems, this.props)
|
||||
$form = e.target
|
||||
|
||||
start_year = $("[name='start_year']", $form).val()
|
||||
end_year = $("[name='end_year']", $form).val()
|
||||
|
||||
if this.props.showEndDate && start_year > end_year
|
||||
this.setState({errors: ["End year must be greater than start year"]})
|
||||
else
|
||||
this.props.listItems.push {
|
||||
name: $("[name='title_input']", $form).val()
|
||||
organization: $("[name='organization_input']", $form).val()
|
||||
start_year: start_year
|
||||
end_year: end_year
|
||||
}
|
||||
logger.debug("addExperience", this.props.listItems)
|
||||
this.props.onItemChanged(this.props.experienceType, this.props.listItems)
|
||||
#$form.reset()
|
||||
this.setState({errors: null})
|
||||
false
|
||||
|
||||
getInitialState: () ->
|
||||
{errors:null}
|
||||
|
||||
onItemChanged: (listName, listObjects) ->
|
||||
this.setState({errors: null})
|
||||
this.props.onItemChanged(listName, listObjects)
|
||||
|
||||
render: () ->
|
||||
endDate = []
|
||||
if this.props.showEndDate
|
||||
endDate.push `<span><label htmlFor="end-year">to</label>
|
||||
<YearSelect name="end_year"></YearSelect></span>`
|
||||
dtLabel = "Start & End"
|
||||
else
|
||||
dtLabel = "Date"
|
||||
|
||||
titleLabel = this.props.titleLabel
|
||||
orgLabel = this.props.orgLabel
|
||||
|
||||
titleLabel ||= "Title"
|
||||
orgLabel ||= "School/Org"
|
||||
|
||||
listItems= _.sortBy(this.props.listItems, 'start_year')
|
||||
errorClasses = classNames({hidden: !@state.error?, "error-text": true})
|
||||
|
||||
errors = []
|
||||
if this.state.errors?
|
||||
for error in this.state.errors
|
||||
errors.push(error)
|
||||
|
||||
`<div className="TeacherExperienceEditableList react-component">
|
||||
<form className="teacher-experience-teaching-form">
|
||||
<div className="form-table">
|
||||
<div className="teacher-field title">
|
||||
<label htmlFor="title-input">{titleLabel}:</label>
|
||||
<input type="text" name="title_input" required="required"> </input>
|
||||
</div>
|
||||
<div className="teacher-field organization">
|
||||
<label htmlFor="organization-input">{orgLabel}:</label>
|
||||
<input type="text" name="organization_input" required="required"> </input>
|
||||
</div>
|
||||
<div className="teacher-field date">
|
||||
<label htmlFor="start-year">{dtLabel}:</label>
|
||||
<span className="year-range-cell">
|
||||
<YearSelect name="start_year"> </YearSelect>
|
||||
{endDate}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button className="add-experience-btn button-grey right" type="submit">ADD</button>
|
||||
</form>
|
||||
<EditableList objectName={this.props.experienceType} onItemChanged={this.onItemChanged} listItems={listItems} formatListItem={this.formatListItem}/>
|
||||
<div className={errorClasses}>
|
||||
{errors}
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
|
|
@ -0,0 +1,668 @@
|
|||
context = window
|
||||
MIX_MODES = context.JK.MIX_MODES
|
||||
rest = context.JK.Rest()
|
||||
logger = context.JK.logger
|
||||
|
||||
SubjectStore = context.SubjectStore
|
||||
InstrumentStore = context.InstrumentStore
|
||||
LanguageStore = context.LanguageStore
|
||||
GenreStore = context.GenreStore
|
||||
UserStore = context.UserStore
|
||||
AppStore = context.AppStore
|
||||
|
||||
profileUtils = context.JK.ProfileUtils
|
||||
|
||||
proficiencyCssMap = {
|
||||
"1": "proficiency-beginner",
|
||||
"2": "proficiency-intermediate",
|
||||
"3": "proficiency-expert"
|
||||
};
|
||||
|
||||
proficiencyDescriptionMap = {
|
||||
"1": "BEGINNER",
|
||||
"2": "INTERMEDIATE",
|
||||
"3": "EXPERT"
|
||||
};
|
||||
|
||||
@TeacherProfile = React.createClass({
|
||||
|
||||
mixins: [
|
||||
Reflux.listenTo(AppStore, "onAppInit"),
|
||||
Reflux.listenTo(UserStore, "onUserChanged"),
|
||||
Reflux.listenTo(SubjectStore, "onSubjectsChanged"),
|
||||
Reflux.listenTo(GenreStore, "onGenresChanged"),
|
||||
Reflux.listenTo(InstrumentStore, "onInstrumentsChanged"),
|
||||
Reflux.listenTo(LanguageStore, "onLanguagesChanged")
|
||||
]
|
||||
|
||||
TILE_ABOUT: 'about'
|
||||
TILE_EXPERIENCE: 'experience'
|
||||
TILE_SAMPLES: 'samples'
|
||||
TILE_RATINGS: 'ratings'
|
||||
TILE_PRICES: 'prices'
|
||||
|
||||
TILES: ['about', 'experience', 'samples', 'ratings', 'prices']
|
||||
|
||||
onAppInit: (@app) ->
|
||||
@app.bindScreen('profile/teacher', {beforeShow: @beforeShow, afterShow: @afterShow})
|
||||
|
||||
onSubjectsChanged: () ->
|
||||
@setState({subjects: true})
|
||||
|
||||
onInstrumentsChanged: () ->
|
||||
@setState({instruments: true})
|
||||
|
||||
onGenresChanged: () ->
|
||||
@setState({genres: true})
|
||||
|
||||
onLanguagesChanged: () ->
|
||||
@setState({languages: true})
|
||||
|
||||
componentDidMount: () ->
|
||||
@root = $(@getDOMNode())
|
||||
@starbox()
|
||||
|
||||
componentDidUpdate:() ->
|
||||
@starbox()
|
||||
|
||||
starbox:() ->
|
||||
$ratings = @root.find('.ratings-box')
|
||||
$ratings.each((i, value) =>
|
||||
$element = $(value)
|
||||
rating = $element.attr('data-ratings')
|
||||
rating = parseFloat(rating)
|
||||
|
||||
#$element.starbox('destroy')
|
||||
|
||||
$element.starbox({
|
||||
average: rating,
|
||||
changeable: false,
|
||||
autoUpdateAverage: false,
|
||||
ghosting: false
|
||||
}).show()
|
||||
)
|
||||
|
||||
|
||||
beforeShow: (e) ->
|
||||
@setState({userId: e.id, user: null})
|
||||
rest.getUserDetail({
|
||||
id: e.id,
|
||||
show_teacher: true,
|
||||
show_profile: true
|
||||
}).done((response) => @userDetailDone(response)).fail(@app.ajaxError)
|
||||
|
||||
userDetailDone: (response) ->
|
||||
if response.id == @state.userId
|
||||
@setState({user: response, isSelf: response.id == context.JK.currentUserId})
|
||||
|
||||
|
||||
afterShow: () ->
|
||||
|
||||
getInitialState: () ->
|
||||
{
|
||||
userId: null,
|
||||
user: null,
|
||||
selected: @TILE_ABOUT,
|
||||
isSelf: false,
|
||||
subjects: false,
|
||||
instruments: false,
|
||||
genres: false,
|
||||
languages: false
|
||||
}
|
||||
|
||||
onUserChanged: (userState) ->
|
||||
@user = userState?.user
|
||||
|
||||
editProfile: (selected, e) ->
|
||||
e.preventDefault()
|
||||
logger.debug("edit profile requested for state " + selected)
|
||||
ProfileActions.startTeacherEdit(selected, true)
|
||||
|
||||
editMusicProfile: (selected, e) ->
|
||||
e.preventDefault()
|
||||
logger.debug("edit music profile requested " + selected)
|
||||
ProfileActions.startProfileEdit(selected, true)
|
||||
|
||||
editProfileLink: (text, selected) ->
|
||||
if @state.isSelf
|
||||
`<a className="edit-link" onClick={this.editProfile.bind(this, selected)}>{text}</a>`
|
||||
|
||||
editMusicProfileLink: (text, selected) ->
|
||||
if @state.isSelf
|
||||
`<a className="edit-link" onClick={this.editMusicProfile.bind(this, selected)}>{text}</a>`
|
||||
|
||||
|
||||
teacherBio: (user, teacher) ->
|
||||
if teacher.biography?
|
||||
biography = teacher.biography
|
||||
else
|
||||
biography = 'No bio defined yet'
|
||||
|
||||
biography = biography.replace(/\n/g, "<br/>")
|
||||
|
||||
`<div className="section bio">
|
||||
<h3>Teacher Profile {this.editProfileLink('edit profile', 'introduction')}</h3>
|
||||
<div className="section-content">
|
||||
<div dangerouslySetInnerHTML={{__html: biography}}></div>
|
||||
</div>
|
||||
</div>`
|
||||
|
||||
sampleVideo: (user, teacher) ->
|
||||
if teacher.introductory_video?
|
||||
videoUrl = teacher.introductory_video
|
||||
|
||||
if videoUrl.indexOf(window.location.protocol) != 0
|
||||
console.log("replacing video")
|
||||
if window.location.protocol == 'http:'
|
||||
console.log("replacing https: " + videoUrl)
|
||||
videoUrl = videoUrl.replace('https://', 'http://')
|
||||
console.log("replaced : " + videoUrl)
|
||||
else
|
||||
videoUrl = videoUrl.replace('http://', 'https://')
|
||||
|
||||
videoUrl = videoUrl.replace("watch?v=", "v/")
|
||||
|
||||
|
||||
return `<div className="section introductory-video">
|
||||
<h3>Intro Video</h3>
|
||||
|
||||
<div className="section-content">
|
||||
<div className="video-wrapper">
|
||||
<div className="video-container">
|
||||
<iframe src={videoUrl} frameborder="0" allowfullscreen="allowfullscreen"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
|
||||
|
||||
teachesInfo: (user, teacher) ->
|
||||
teachesInfo = []
|
||||
|
||||
instruments = teacher.instruments.map((id) -> InstrumentStore.display(id))
|
||||
instrumentData = instruments.join(', ')
|
||||
teachesInfo.push(`<tr>
|
||||
<td>Instruments</td>
|
||||
<td>{instrumentData}</td>
|
||||
</tr>`)
|
||||
|
||||
subjects = teacher.subjects.map((id) -> SubjectStore.display(id))
|
||||
subjectsData = subjects.join(', ')
|
||||
teachesInfo.push(`<tr>
|
||||
<td>Subjects</td>
|
||||
<td>{subjectsData}</td>
|
||||
</tr>`)
|
||||
|
||||
genres = teacher.genres.map((id) -> GenreStore.display(id))
|
||||
genresData = genres.join(', ')
|
||||
teachesInfo.push(`<tr>
|
||||
<td>Genres</td>
|
||||
<td>{genresData}</td>
|
||||
</tr>`)
|
||||
|
||||
levels = []
|
||||
if teacher.teaches_beginner
|
||||
levels.push('Beginner')
|
||||
if teacher.teaches_intermediate
|
||||
levels.push('Intermediate')
|
||||
if teacher.teaches_advanced
|
||||
levels.push('Advanced')
|
||||
levelsData = levels.join(', ')
|
||||
teachesInfo.push(`<tr>
|
||||
<td>Levels</td>
|
||||
<td>{levelsData}</td>
|
||||
</tr>`)
|
||||
|
||||
ageData = '?'
|
||||
if teacher.teaches_age_lower == 0 && teacher.teaches_age_upper == 0
|
||||
ageData = 'Any'
|
||||
else if teacher.teaches_age_lower != 0 && teacher.teaches_age_upper != 0
|
||||
ageData = "#{teacher.teaches_age_lower} to #{teacher.teaches_age_upper}"
|
||||
else if teacher.teaches_age_lower == 0
|
||||
ageData = "Age #{teacher.teaches_age_upper} and younger"
|
||||
else if teacher.teaches_age_upper == 0
|
||||
ageData = "Ages #{teacher.teaches_age_lower} and up"
|
||||
teachesInfo.push(`<tr>
|
||||
<td>Ages</td>
|
||||
<td>{ageData}</td>
|
||||
</tr>`)
|
||||
|
||||
languages = teacher.languages.map((id) -> LanguageStore.display(id))
|
||||
languagesData = languages.join(', ')
|
||||
teachesInfo.push(`<tr>
|
||||
<td>Languages</td>
|
||||
<td>{languagesData}</td>
|
||||
</tr>`)
|
||||
`<div className="section teachers">
|
||||
<h3>{this.state.user.first_name} Teaches {this.editProfileLink('edit teaching', 'basics')}</h3>
|
||||
|
||||
<div className="section-content">
|
||||
<table className="jamtable" cellspacing="0" cellpadding="0" border="0">
|
||||
<tbody>
|
||||
{teachesInfo}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>`
|
||||
|
||||
musicianBio: (user, teacher) ->
|
||||
|
||||
if user.biography?
|
||||
musicianBio = user.biography
|
||||
else
|
||||
musicianBio = 'None specified.'
|
||||
|
||||
musicianBio = musicianBio.replace(/\n/g, "<br/>")
|
||||
|
||||
`<div className="section musician-bio">
|
||||
<h3>Musical Profile {this.editMusicProfileLink('update bio', '')}</h3>
|
||||
|
||||
<div className="section-content" dangerouslySetInnerHTML={{__html: musicianBio}}></div>
|
||||
</div>`
|
||||
|
||||
plays: (user, teacher) ->
|
||||
if user.profile.concert_count > 0
|
||||
gigs = "Has played #{profileUtils.gigMap[user.profile.concert_count]} concert gigs"
|
||||
else
|
||||
gigs = "Has played an unknown # of concert gigs"
|
||||
|
||||
if user.profile.studio_session_count > 0
|
||||
studioSessions = "Has played #{profileUtils.gigMap[user.profile.studio_session_count]} studio sessions"
|
||||
else
|
||||
studioSessions = "Has played an unknown # of studio sessions"
|
||||
|
||||
|
||||
gigTable = `<table className="jamtable giginfo">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{gigs}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{studioSessions}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>`
|
||||
|
||||
display_instruments = []
|
||||
for instrument in user.instruments
|
||||
|
||||
description = InstrumentStore.display(instrument.instrument_id)
|
||||
proficiency = instrument.proficiency_level;
|
||||
instrument_icon_url = context.JK.getInstrumentIcon256(instrument.instrument_id);
|
||||
display_instruments.push(`<div className="profile-instrument">
|
||||
<img src={instrument_icon_url} width="70" height="70"/><br />
|
||||
<span>{description}</span><br />
|
||||
<span className={proficiencyCssMap[proficiency]}>{proficiencyDescriptionMap[proficiency]}</span>
|
||||
</div>`)
|
||||
|
||||
`<div className="section plays">
|
||||
<h3>{this.state.user.first_name} Plays {this.editMusicProfileLink('update instruments', 'experience')}</h3>
|
||||
|
||||
<div className="section-content">
|
||||
{display_instruments}
|
||||
{gigTable}
|
||||
</div>
|
||||
<br className="clearall"/>
|
||||
</div>`
|
||||
|
||||
createPresence: (className, url, img) ->
|
||||
`<div className={className + "-presence logo online-presence-option"}>
|
||||
<a href={url}><img className="logo" src={img}/></a>
|
||||
</div>`
|
||||
|
||||
|
||||
onlinePresences: (user, teacher) ->
|
||||
online_presences = user.profile.online_presences
|
||||
|
||||
presences = []
|
||||
|
||||
if user.profile.website
|
||||
# make sure website is rooted
|
||||
website = user.profile.website
|
||||
if website.indexOf('http') == -1
|
||||
website = 'http://' + website
|
||||
presences.push(@createPresence("user-website", website, "/assets/content/website-logo.png"))
|
||||
|
||||
matches = profileUtils.soundCloudPresences(online_presences)
|
||||
if matches.length > 0
|
||||
presences.push(@createPresence("soundcloud", "http://www.soundcloud.com/#{matches[0].username}",
|
||||
"/assets/content/soundcloud-logo.png"))
|
||||
|
||||
matches = profileUtils.reverbNationPresences(online_presences)
|
||||
if matches.length > 0
|
||||
presences.push(@createPresence("reverbnation", "http://www.reverbnation.com/#{matches[0].username}",
|
||||
"/assets/content/reverbnation-logo.png"))
|
||||
|
||||
matches = profileUtils.bandCampPresences(online_presences)
|
||||
if matches.length > 0
|
||||
presences.push(@createPresence("bandcamp", 'http://' + matches[0].username + '.bandcamp.com/',
|
||||
"/assets/content/bandcamp-logo.png"))
|
||||
|
||||
matches = profileUtils.fandalismPresences(online_presences)
|
||||
if matches.length > 0
|
||||
presences.push(@createPresence("fandalism", 'http://www.fandalism.com/' + matches[0].username,
|
||||
"/assets/content/fandalism-logo.png"))
|
||||
|
||||
matches = profileUtils.youTubePresences(online_presences)
|
||||
if matches.length > 0
|
||||
presences.push(@createPresence("youtube", 'http://www.youtube.com/' + matches[0].username,
|
||||
"/assets/content/youtube-logo.png"))
|
||||
|
||||
matches = profileUtils.facebookPresences(online_presences)
|
||||
if matches.length > 0
|
||||
presences.push(@createPresence("facebook", 'http://www.facebook.com/' + matches[0].username,
|
||||
"/assets/content/facebook-logo.png"))
|
||||
|
||||
matches = profileUtils.twitterPresences(online_presences)
|
||||
if matches.length > 0
|
||||
presences.push(@createPresence("twitter", 'http://www.twitter.com/' + matches[0].username,
|
||||
"/assets/content/twitter-logo.png"))
|
||||
|
||||
if presences.length == 0
|
||||
presences = `<div className="no-online-presence">None specified</div>`
|
||||
|
||||
|
||||
`<div className="section plays">
|
||||
<h3>Online Presence {this.editMusicProfileLink('update presence', 'samples')}</h3>
|
||||
|
||||
<div className="section-content">
|
||||
{presences}
|
||||
</div>
|
||||
</div>`
|
||||
|
||||
about: () ->
|
||||
user = @state.user
|
||||
teacher = user.teacher
|
||||
|
||||
`<div className="about-block info-block">
|
||||
{this.sampleVideo(user, teacher)}
|
||||
|
||||
{this.teacherBio(user, teacher)}
|
||||
|
||||
{this.teachesInfo(user, teacher)}
|
||||
|
||||
{this.musicianBio(user, teacher)}
|
||||
|
||||
{this.plays(user, teacher)}
|
||||
|
||||
{this.onlinePresences(user, teacher)}
|
||||
</div>`
|
||||
|
||||
experiences: (title, attr, update, user, teacher) ->
|
||||
teachingExperiences = []
|
||||
|
||||
for teachingExperience in teacher['experiences_' + attr]
|
||||
years = ''
|
||||
if teachingExperience.start_year > 0
|
||||
if teachingExperience.end_year && teachingExperience.end_year > 0
|
||||
years = "#{teachingExperience.start_year} - #{teachingExperience.end_year}"
|
||||
else
|
||||
years = "#{teachingExperience.start_year} - Present"
|
||||
|
||||
teachingExperiences.push(`<div className="experience">
|
||||
<div className="years">{years}</div>
|
||||
<h4>{teachingExperience.name}</h4>
|
||||
|
||||
<div className="org">{teachingExperience.organization}</div>
|
||||
</div>`)
|
||||
|
||||
if update?
|
||||
updateLink = this.editProfileLink('update ' + update, 'experience')
|
||||
|
||||
if teachingExperiences.length == 0
|
||||
teachingExperiences = `<div>None specified</div>`
|
||||
`<div className="section teaching-experience">
|
||||
<h3>{title} {updateLink}</h3>
|
||||
|
||||
<div className="section-content">
|
||||
{teachingExperiences}
|
||||
</div>
|
||||
</div>`
|
||||
|
||||
sampleClicked: (e) ->
|
||||
e.preventDefault()
|
||||
context.JK.popExternalLink($(e.target).attr('href'))
|
||||
|
||||
|
||||
musicSamples: (user, teacher) ->
|
||||
performance_samples = user.profile.performance_samples
|
||||
|
||||
jamkazamSamples = []
|
||||
samples = profileUtils.jamkazamSamples(performance_samples);
|
||||
for sample in samples
|
||||
jamkazamSamples.push(`<a className="jamkazam-playable playable" href={"/recordings/" + sample.claimed_recording.id} onClick={this.sampleClicked}>{sample.claimed_recording.name}</a>`)
|
||||
|
||||
soundCloudSamples= []
|
||||
samples = profileUtils.soundCloudSamples(performance_samples);
|
||||
for sample in samples
|
||||
soundCloudSamples.push(`<a className="sound-cloud-playable playable" href={sample.url} onClick={this.sampleClicked}>{sample.description}</a>`)
|
||||
|
||||
youTubeSamples = []
|
||||
samples = profileUtils.youTubeSamples(performance_samples);
|
||||
for sample in samples
|
||||
youTubeSamples.push(`<a className="youtube-playable playable" href={sample.url} onClick={this.sampleClicked}>{sample.description}</a>`)
|
||||
|
||||
sampleJsx = []
|
||||
|
||||
if jamkazamSamples.length > 0
|
||||
sampleJsx.push(`<div className="jamkazam-samples logo performance-sample-option">
|
||||
<img className="logo" src="/assets/header/logo.png" />
|
||||
{jamkazamSamples}
|
||||
</div>`)
|
||||
|
||||
if soundCloudSamples.length > 0
|
||||
sampleJsx.push(`<div className="soundcloud-samples logo performance-sample-option">
|
||||
<img className="logo" src="/assets/content/soundcloud-logo.png" />
|
||||
{soundCloudSamples}
|
||||
</div>`)
|
||||
|
||||
if youTubeSamples.length > 0
|
||||
sampleJsx.push(`<div className="youtube-samples logo performance-sample-option">
|
||||
<img className="logo" src="/assets/content/youtube-logo.png" />
|
||||
{youTubeSamples}
|
||||
</div>`)
|
||||
|
||||
|
||||
`<div className="section samples">
|
||||
<h3>Performance Samples {this.editMusicProfileLink('update samples', 'samples')}</h3>
|
||||
|
||||
<div className="section-content">
|
||||
{sampleJsx}
|
||||
</div>
|
||||
</div>`
|
||||
|
||||
experience: () ->
|
||||
user = @state.user
|
||||
teacher = user.teacher
|
||||
|
||||
`<div className="experience-block info-block">
|
||||
{this.experiences('Teaching Experience', 'teaching', 'experience', user, teacher)}
|
||||
|
||||
{this.experiences('Music Education', 'education', null, user, teacher)}
|
||||
|
||||
{this.experiences('Music Awards', 'award', null, user, teacher)}
|
||||
</div>`
|
||||
|
||||
samples: () ->
|
||||
user = @state.user
|
||||
teacher = user.teacher
|
||||
|
||||
`<div className="samples-block info-block">
|
||||
{this.musicSamples(user, teacher)}
|
||||
</div>`
|
||||
|
||||
ratings: () ->
|
||||
user = @state.user
|
||||
teacher = user.teacher
|
||||
|
||||
summary = teacher.review_summary || {avg_rating: 0, review_count: 0}
|
||||
|
||||
if summary.review_count == 1
|
||||
reviewCount = '1 review'
|
||||
else
|
||||
reviewCount = sumarry.review_count + ' reviews'
|
||||
reviews = []
|
||||
|
||||
for review in teacher.recent_reviews
|
||||
photo_url = review.user.photo_url
|
||||
if !photo_url?
|
||||
photo_url = '/assets/shared/avatar_generic.png'
|
||||
|
||||
name = `<div className="reviewer-name">{review.user.name}</div>`
|
||||
reviews.push(`<div className="review">
|
||||
<div className="review-header">
|
||||
<div className="avatar small">
|
||||
<img src={photo_url} />
|
||||
</div>
|
||||
{name}
|
||||
<div className="ratings-box hidden" data-ratings={review.rating / 5 }/>
|
||||
<div className="review-time">{context.JK.formatDateShort(review.created_at)}</div>
|
||||
</div>
|
||||
<div className="review-content" dangerouslySetInnerHTML={{__html: review.description}}></div>
|
||||
</div>`)
|
||||
`<div className="ratings-block info-block">
|
||||
<h3>Ratings & Reviews</h3>
|
||||
|
||||
<h4>{user.first_name} Summary Rating: <div data-ratings={summary.avg_rating / 5} className="ratings-box hidden"/> <div className="review-count">({reviewCount})</div></h4>
|
||||
|
||||
{reviews}
|
||||
</div>`
|
||||
|
||||
prices: () ->
|
||||
user = @state.user
|
||||
teacher = user.teacher
|
||||
|
||||
rows = []
|
||||
|
||||
for minutes in [30, 45, 60, 90, 120]
|
||||
lesson_price = teacher["price_per_lesson_#{minutes}_cents"]
|
||||
monthly_price = teacher["price_per_month_#{minutes}_cents"]
|
||||
duration_enabled = teacher["lesson_duration_#{minutes}"]
|
||||
|
||||
console.log("lesson_price", lesson_price)
|
||||
console.log("monthly_price", monthly_price)
|
||||
console.log("duration neabled", duration_enabled)
|
||||
if duration_enabled && teacher.prices_per_lesson && lesson_price? && lesson_price > 0
|
||||
lessonPriceFormatted = '$ ' + (lesson_price / 100).toFixed(2)
|
||||
else
|
||||
lessonPriceFormatted = 'N/A'
|
||||
|
||||
if duration_enabled && teacher.prices_per_month && monthly_price? && monthly_price > 0
|
||||
monthlyPriceFormatted = '$ ' + (monthly_price / 100).toFixed(2)
|
||||
else
|
||||
monthlyPriceFormatted = 'N/A'
|
||||
|
||||
rows.push(`<tr><td>{minutes + " Minutes"}</td><td>{lessonPriceFormatted}</td><td>{monthlyPriceFormatted}</td></tr>`)
|
||||
|
||||
`<div className="prices-block info-block">
|
||||
<h3>Lesson Prices {this.editProfileLink('update prices', 'pricing')}</h3>
|
||||
|
||||
<div className="section-content">
|
||||
<table className="jamtable price-table">
|
||||
<thead>
|
||||
<tr><th>LESSON LENGTH</th><th>PRICE PER LESSON</th><th>PRICE PER MONTH</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>`
|
||||
|
||||
mainContent: () ->
|
||||
if @state.selected == @TILE_ABOUT
|
||||
@about()
|
||||
else if @state.selected == @TILE_EXPERIENCE
|
||||
@experience()
|
||||
else if @state.selected == @TILE_SAMPLES
|
||||
@samples()
|
||||
else if @state.selected == @TILE_RATINGS
|
||||
@ratings()
|
||||
else if @state.selected == @TILE_PRICES
|
||||
@prices()
|
||||
|
||||
profileLeft: () ->
|
||||
`<div className="profile-about-left">
|
||||
<div className="left-content">
|
||||
<div className="location">
|
||||
<div className="city">{this.state.user.city}</div>
|
||||
<div className="state">{this.state.user.state}</div>
|
||||
<div className="country">{this.state.user.country}</div>
|
||||
<div className="age">{this.state.user.age} years old</div>
|
||||
</div>
|
||||
|
||||
<div className="activity">
|
||||
<div className="last-signed-in">Last Signed In:</div>
|
||||
<div>{"very recently"}</div>
|
||||
</div>
|
||||
|
||||
<div className="backgroundCheck">
|
||||
<div className="background-check">Background Check:</div>
|
||||
<div className="last-verified">last verified</div>
|
||||
<div className="last-verified-time">3 months ago</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
|
||||
selectionMade: (selection, e) ->
|
||||
e.preventDefault()
|
||||
|
||||
@setState({selected: selection})
|
||||
|
||||
render: () ->
|
||||
if @state.user?
|
||||
avatar = context.JK.resolveAvatarUrl(@state.user.photo_url);
|
||||
if @state.user?.teacher?
|
||||
mainContent = @mainContent()
|
||||
profileLeft = @profileLeft()
|
||||
|
||||
editButton = `<a href="/client#/account/profile" className="button-orange edit-profile-btn hidden">EDIT PROFILE</a>`
|
||||
actionButtons = `<div className="right hidden">
|
||||
<a id="btn-add-friend" className="button-orange">ADD FRIEND</a>
|
||||
<a id="btn-follow-user" className="button-orange">FOLLOW</a>
|
||||
<a id="btn-message-user" className="button-orange">MESSAGE</a>
|
||||
</div>`
|
||||
profileSelections = []
|
||||
|
||||
for tile, i in @TILES
|
||||
console.log("@state.selected", @state.selected, @state.selected == tile)
|
||||
classes = classNames({last: i == @TILES.length - 1, active: @state.selected == tile})
|
||||
|
||||
profileSelections.push(`<div className="profile-tile"><a className={classes}
|
||||
onClick={this.selectionMade.bind(this, tile)}>{tile}</a>
|
||||
</div>`)
|
||||
|
||||
`<div className="content-body-scroller">
|
||||
<div className="profile-header profile-head">
|
||||
|
||||
<div className="user-header">
|
||||
<h2 id="username"></h2>
|
||||
{editButton}
|
||||
</div>
|
||||
|
||||
{actionButtons}
|
||||
<br clear="all"/><br />
|
||||
|
||||
<div className="profile-photo">
|
||||
<div className="avatar-profile">
|
||||
<img width="200" height="200" src={avatar}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="profile-nav">
|
||||
{profileSelections}
|
||||
</div>
|
||||
<div className="clearall"></div>
|
||||
</div>
|
||||
|
||||
<div className="profile-body">
|
||||
<div className="profile-wrapper">
|
||||
{profileLeft}
|
||||
<div className="main-content">
|
||||
{mainContent}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
context = window
|
||||
teacherActions = window.JK.Actions.Teacher
|
||||
logger = context.JK.logger
|
||||
rest = window.JK.Rest()
|
||||
|
||||
@TeacherProfileBasics = React.createClass({
|
||||
mixins: [
|
||||
@TeacherProfileMixin,
|
||||
Reflux.listenTo(@AppStore,"onAppInit"),
|
||||
Reflux.listenTo(TeacherStore, "onTeacherStateChanged")
|
||||
]
|
||||
|
||||
setTeacherError: () ->
|
||||
|
||||
screenName: () ->
|
||||
"basics"
|
||||
|
||||
getInitialState: () ->
|
||||
{}
|
||||
|
||||
onTeacherStateChanged: (changes) ->
|
||||
$root = jQuery(this.getDOMNode())
|
||||
logger.debug("onTeacherStateChanged", changes, changes.errors?, changes.errors)
|
||||
$(".error-text", $root).remove()
|
||||
$(".input-error", $root).removeClass("input-error")
|
||||
if changes.errors?
|
||||
for k,v of changes.errors
|
||||
logger.debug("error", k, v)
|
||||
teacherField = $root.find(".teacher-field[name='#{k}']")
|
||||
teacherField.append("<div class='error-text'>#{v.join()}</div>")
|
||||
$("input", teacherField).addClass("input-error")
|
||||
#$(".error-text", teacherField).show()
|
||||
|
||||
else
|
||||
teacher = changes.teacher
|
||||
logger.debug("@teacher", teacher)
|
||||
this.setState({
|
||||
biography: teacher.biography,
|
||||
introductory_video: teacher.introductory_video,
|
||||
years_teaching: teacher.years_teaching,
|
||||
years_playing: teacher.years_playing,
|
||||
validate_introduction: true
|
||||
})
|
||||
|
||||
captureFormState: (changes) ->
|
||||
$root = jQuery(this.getDOMNode())
|
||||
this.setState({
|
||||
biography: $root.find(".teacher-biography").val(),
|
||||
introductory_video: $root.find(".teacher-introductory-video").val(),
|
||||
years_teaching: $root.find(".years-teaching-experience").val(),
|
||||
years_playing: $root.find(".years-playing-experience").val()
|
||||
});
|
||||
logger.debug("capturedFormState", this.state, changes)
|
||||
|
||||
handleNav: (e) ->
|
||||
logger.debug("handleNav: ", this.state, this, e)
|
||||
teacherActions.change.trigger(this.state, e)
|
||||
|
||||
render: () ->
|
||||
logger.debug("RENDERING", this.props, this.state)
|
||||
`<div className="TeacherProfileComponent" >
|
||||
</div>
|
||||
`
|
||||
|
||||
})
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
context = window
|
||||
teacherActions = window.JK.Actions.Teacher
|
||||
logger = context.JK.logger
|
||||
rest = window.JK.Rest()
|
||||
|
||||
@TeacherSetupBasics = React.createClass({
|
||||
mixins: [
|
||||
@TeacherSetupMixin,
|
||||
Reflux.listenTo(@AppStore,"onAppInit"),
|
||||
Reflux.listenTo(TeacherStore, "onTeacherStateChanged")
|
||||
]
|
||||
|
||||
getInitialState: () ->
|
||||
{}
|
||||
|
||||
screenName: () ->
|
||||
"basics"
|
||||
|
||||
onTeacherStateChanged: (changes) ->
|
||||
$root = jQuery(this.getDOMNode())
|
||||
unless this.handleErrors(changes)
|
||||
teacher = changes.teacher
|
||||
this.setState(teacher)
|
||||
|
||||
captureFormState: (changes) ->
|
||||
$root = jQuery(this.getDOMNode())
|
||||
this.setState({
|
||||
|
||||
});
|
||||
|
||||
handleListChange: (listName, selectedObjects)->
|
||||
logger.debug("handleListChange:", listName, selectedObjects)
|
||||
this.setState({
|
||||
"#{listName}": selectedObjects
|
||||
});
|
||||
|
||||
handleFieldChange: (fieldName, value)->
|
||||
logger.debug("handleFieldChange:", fieldName, value)
|
||||
this.setState({
|
||||
"#{fieldName}": value
|
||||
});
|
||||
|
||||
navDestination: (instructions) ->
|
||||
navTo=null
|
||||
if instructions?
|
||||
if instructions.direction=="cancel"
|
||||
navTo = @teacherSetupSource()
|
||||
else if instructions.direction=="back"
|
||||
navTo = @teacherSetupDestination("introduction")
|
||||
else if instructions.direction=="next"
|
||||
navTo = @teacherSetupDestination("experience")
|
||||
|
||||
navTo
|
||||
|
||||
handleNav: (e) ->
|
||||
navTo = this.navDestination(e)
|
||||
this.state.validate_basics = true
|
||||
teacherActions.change.trigger(this.state, {navTo: navTo})
|
||||
|
||||
render: () ->
|
||||
# Render the following:
|
||||
# Instruments
|
||||
# Subjects
|
||||
# Genres
|
||||
# Languages
|
||||
# All lists will take a list of selected keys,
|
||||
# and will otherwise self-render the available
|
||||
|
||||
`<div className="TeacherSetupBasics TeacherSetupComponent">
|
||||
<div className="teacher-quarter-column">
|
||||
<div className="teacher-field" name="instruments">
|
||||
<h3>Instruments Taught:</h3>
|
||||
<InstrumentCheckBoxList onItemChanged={this.handleListChange} selectedInstruments={this.state.instruments}/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="teacher-quarter-column">
|
||||
<div className="teacher-field" name="subjects">
|
||||
<h3>Music Subjects Taught:</h3>
|
||||
<SubjectCheckBoxList onItemChanged={this.handleListChange} selectedSubjects={this.state.subjects}/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="teacher-quarter-column">
|
||||
<div className="teacher-field" name="genres">
|
||||
<h3>Genres Taught:</h3>
|
||||
<GenreCheckBoxList onItemChanged={this.handleListChange} selectedGenres={this.state.genres}/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="teacher-quarter-column">
|
||||
<div className="teacher-field" name="languages">
|
||||
<h3>Languages Spoken:</h3>
|
||||
<LanguageCheckBoxList onItemChanged={this.handleListChange} selectedLanguages={this.state.languages}/>
|
||||
</div>
|
||||
</div>
|
||||
<br className="clearall"/>
|
||||
<div className="teacher-half-column">
|
||||
<div className="teacher-field" name="levels_taught">
|
||||
<h3>Student Levels Taught:</h3>
|
||||
<div className="student-levels">
|
||||
<TeacherStudentLevel onChange={this.handleFieldChange} student="teaches_beginner" display="Beginner" level={this.state.teaches_beginner} />
|
||||
<TeacherStudentLevel onChange={this.handleFieldChange} student="teaches_intermediate" display="Intermediate" level={this.state.teaches_intermediate} />
|
||||
<TeacherStudentLevel onChange={this.handleFieldChange} student="teaches_advanced" display="Advanced" level={this.state.teaches_advanced} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="teacher-half-column">
|
||||
<div className="teacher-field" name="ages_taught">
|
||||
<h3>Student Ages Taught:</h3>
|
||||
<div className="student-ages">
|
||||
<AgeRangeList onItemChanged={this.handleFieldChange} objectName="teaches_age_lower" selectedAge={this.state.teaches_age_lower} />
|
||||
<span className="age-to-age">to</span>
|
||||
<AgeRangeList onItemChanged={this.handleFieldChange} objectName="teaches_age_upper" selectedAge={this.state.teaches_age_upper} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<TeacherSetupNav handleNav={this.handleNav}></TeacherSetupNav>
|
||||
</div>`
|
||||
|
||||
|
||||
})
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
context = window
|
||||
teacherActions = window.JK.Actions.Teacher
|
||||
logger = context.JK.logger
|
||||
rest = window.JK.Rest()
|
||||
|
||||
@TeacherSetupExperience = React.createClass({
|
||||
mixins: [
|
||||
@TeacherSetupMixin,
|
||||
Reflux.listenTo(@AppStore,"onAppInit"),
|
||||
Reflux.listenTo(TeacherStore, "onTeacherStateChanged")
|
||||
]
|
||||
|
||||
getInitialState: () ->
|
||||
{
|
||||
experiences_teaching: []
|
||||
experiences_education: []
|
||||
experiences_award: []
|
||||
}
|
||||
|
||||
screenName: () ->
|
||||
"experience"
|
||||
|
||||
onTeacherStateChanged: (changes) ->
|
||||
$root = jQuery(this.getDOMNode())
|
||||
unless this.handleErrors(changes)
|
||||
teacher = changes.teacher
|
||||
this.setState({
|
||||
#validate_basics: true,
|
||||
experiences_teaching: teacher.experiences_teaching
|
||||
experiences_education: teacher.experiences_education
|
||||
experiences_award: teacher.experiences_award
|
||||
})
|
||||
|
||||
captureFormState: (changes) ->
|
||||
$root = jQuery(this.getDOMNode())
|
||||
this.setState({
|
||||
|
||||
});
|
||||
|
||||
navDestination: (instructions) ->
|
||||
navTo=null
|
||||
if instructions?
|
||||
logger.debug("handling instructions", instructions)
|
||||
if instructions.direction=="cancel"
|
||||
navTo = @teacherSetupSource()
|
||||
else if instructions.direction=="back"
|
||||
navTo = @teacherSetupDestination("basics")
|
||||
else if instructions.direction=="next"
|
||||
navTo = @teacherSetupDestination("pricing")
|
||||
|
||||
navTo
|
||||
|
||||
handleNav: (e) ->
|
||||
logger.debug("handleNav #{this.screenName()}: ", this.state, this, e)
|
||||
navTo = this.navDestination(e)
|
||||
teacherActions.change.trigger(this.state, {navTo: navTo})
|
||||
|
||||
handleListChange: (listName, listObjects)->
|
||||
logger.debug("EXPERIENCE handleListChange:", listName, listObjects)
|
||||
this.setState({
|
||||
"experiences_#{listName}": listObjects
|
||||
})
|
||||
#this.forceUpdate()
|
||||
|
||||
render: () ->
|
||||
`<div className="TeacherSetupExperience TeacherSetupComponent">
|
||||
<div className="teacher-third-column">
|
||||
<h3 className="sub-caption">TEACHING EXPERIENCE:</h3>
|
||||
<TeacherExperienceEditableList showEndDate="true" experienceType="teaching" onItemChanged={this.handleListChange} listItems={this.state.experiences_teaching}/>
|
||||
</div>
|
||||
|
||||
<div className="teacher-third-column">
|
||||
<h3 className="sub-caption">EDUCATION:</h3>
|
||||
<TeacherExperienceEditableList
|
||||
showEndDate="true"
|
||||
experienceType="education"
|
||||
onItemChanged={this.handleListChange}
|
||||
titleLabel="Degree/Cert"
|
||||
orgLabel="School"
|
||||
listItems={this.state.experiences_education}/>
|
||||
</div>
|
||||
|
||||
<div className="teacher-third-column">
|
||||
<h3 className="sub-caption">AWARDS:</h3>
|
||||
<TeacherExperienceEditableList
|
||||
experienceType="award"
|
||||
onItemChanged={this.handleListChange}
|
||||
titleLabel="Award"
|
||||
orgLabel="Organization"
|
||||
listItems={this.state.experiences_award}/>
|
||||
</div>
|
||||
|
||||
<TeacherSetupNav handleNav={this.handleNav}> </TeacherSetupNav>
|
||||
</div>`
|
||||
|
||||
|
||||
})
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
context = window
|
||||
teacherActions = window.JK.Actions.Teacher
|
||||
logger = context.JK.logger
|
||||
rest = window.JK.Rest()
|
||||
|
||||
@TeacherSetupIntroduction = React.createClass({
|
||||
mixins: [
|
||||
@TeacherSetupMixin,
|
||||
Reflux.listenTo(@AppStore,"onAppInit"),
|
||||
Reflux.listenTo(TeacherStore, "onTeacherStateChanged")
|
||||
]
|
||||
|
||||
screenName: () ->
|
||||
"introduction"
|
||||
|
||||
getInitialState: () ->
|
||||
{}
|
||||
|
||||
onTeacherStateChanged: (changes) ->
|
||||
$root = jQuery(this.getDOMNode())
|
||||
logger.debug("onTeacherIntroStateChanged", changes, changes.errors?, changes.errors)
|
||||
unless this.handleErrors(changes)
|
||||
teacher = changes.teacher
|
||||
this.setState({
|
||||
biography: teacher.biography,
|
||||
introductory_video: teacher.introductory_video,
|
||||
years_teaching: teacher.years_teaching,
|
||||
years_playing: teacher.years_playing,
|
||||
validate_introduction: true
|
||||
})
|
||||
|
||||
handleTextChange: (e) ->
|
||||
this.setState({"#{e.target.name}": e.target.value})
|
||||
|
||||
navDestination: (instructions) ->
|
||||
navTo=null
|
||||
if instructions?
|
||||
if instructions.direction=="cancel" || instructions.direction=="back"
|
||||
navTo = @teacherSetupSource()
|
||||
else if instructions.direction=="next"
|
||||
navTo = @teacherSetupDestination('basics')
|
||||
navTo
|
||||
|
||||
handleNav: (e) ->
|
||||
navTo = this.navDestination(e)
|
||||
teacherActions.change.trigger(this.state, {navTo: navTo})
|
||||
|
||||
render: () ->
|
||||
`<div className="TeacherSetupIntroduction TeacherSetupComponent" >
|
||||
<div className="teacher-big-column left">
|
||||
<div className="teacher-field" name="biography">
|
||||
<label htmlFor="teacher-biography">Teacher Bio:</label>
|
||||
<textarea className="teacher-biography" name="biography" ref="biography" rows="12" value={this.state.biography} onChange={this.handleTextChange} required/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="teacher-small-column left">
|
||||
<div className="teacher-field" name="introductory_video">
|
||||
<label htmlFor="teacher-introductory-video">Teacher Introductory Video:</label>
|
||||
<input className="teacher-introductory-video" name="introductory_video" ref="introductory_video" type="url" maxLength="1024" value={this.state.introductory_video} onChange={this.handleTextChange} required/>
|
||||
<em className="enter-url">(enter YouTube URL)</em>
|
||||
</div>
|
||||
|
||||
<div className="teacher-field" name="years_teaching">
|
||||
<label htmlFor="years-teaching-experience">Years Teaching Experience:</label>
|
||||
<input className="years-teaching-experience" name="years_teaching" ref ="years_teaching_experience" type="number" min="0" max="99" value={this.state.years_teaching} onChange={this.handleTextChange} />
|
||||
</div>
|
||||
|
||||
<div className="teacher-field" name="years_playing">
|
||||
<label htmlFor="teacher-playing-experience">Years Playing Experience:</label>
|
||||
<input className="years-playing-experience" name="years_playing" ref="years_playing_experience" type="number" min="0" max="99" value={this.state.years_playing} onChange={this.handleTextChange} />
|
||||
</div>
|
||||
</div>
|
||||
<TeacherSetupNav hideBack={true} handleNav={this.handleNav}/>
|
||||
</div>`
|
||||
|
||||
})
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
context = window
|
||||
teacherActions = window.JK.Actions.Teacher
|
||||
SessionActions = @SessionActions
|
||||
ProfileActions = @ProfileActions
|
||||
|
||||
@TeacherSetupNav = React.createClass({
|
||||
|
||||
navBack: (e) ->
|
||||
e.preventDefault();
|
||||
console.log("navBack this.props", this.state, this.props)
|
||||
this.props.handleNav({direction: "back"})
|
||||
|
||||
navCancel: (e) ->
|
||||
ProfileActions.cancelTeacherEdit()
|
||||
|
||||
navNext: (e) ->
|
||||
e.preventDefault()
|
||||
console.log("navNext this.props", this.state, this.props)
|
||||
this.props.handleNav({direction: "next"})
|
||||
|
||||
render: () ->
|
||||
if window.ProfileStore.solo
|
||||
saveText = 'SAVE & RETURN TO PROFILE'
|
||||
else
|
||||
if @props.last
|
||||
saveText = 'SAVE & FINISH'
|
||||
else
|
||||
saveText = 'SAVE & NEXT'
|
||||
|
||||
if !this.props.hideBack && !window.ProfileStore.solo
|
||||
back = `<a className="nav-button button-grey" onClick={this.navBack}>
|
||||
BACK
|
||||
</a>`
|
||||
|
||||
`<div className="TeacherSetupNav right">
|
||||
<a className="nav-button button-grey" onClick={this.navCancel}>
|
||||
CANCEL
|
||||
</a>
|
||||
{back}
|
||||
<a className="nav-button button-orange" onClick={this.navNext}>
|
||||
{saveText}
|
||||
</a>
|
||||
</div>`
|
||||
})
|
||||
|
|
@ -0,0 +1,283 @@
|
|||
context = window
|
||||
teacherActions = window.JK.Actions.Teacher
|
||||
logger = context.JK.logger
|
||||
rest = window.JK.Rest()
|
||||
|
||||
@TeacherSetupPricing = React.createClass({
|
||||
mixins: [
|
||||
@TeacherSetupMixin,
|
||||
Reflux.listenTo(@AppStore,"onAppInit"),
|
||||
Reflux.listenTo(TeacherStore, "onTeacherStateChanged")
|
||||
]
|
||||
|
||||
iCheckIgnore: false
|
||||
componentDidUnmount: () ->
|
||||
@root.off("change", ".checkbox-enabler")
|
||||
|
||||
componentDidMount: () ->
|
||||
@root = jQuery(this.getDOMNode())
|
||||
@enableCheckBoxTargets()
|
||||
@updateCheckboxState()
|
||||
|
||||
getInitialState: () ->
|
||||
{}
|
||||
|
||||
componentDidUpdate: () ->
|
||||
@updateCheckboxState()
|
||||
@enableCheckBoxTargets()
|
||||
|
||||
updateCheckboxState: () ->
|
||||
for minutes in [30, 45, 60, 90, 120]
|
||||
priceKey = "lesson_duration_#{minutes}"
|
||||
enabled = @state[priceKey]
|
||||
containerName = ".#{priceKey}_container input[type='checkbox']"
|
||||
@iCheckIgnore = true
|
||||
if enabled
|
||||
@root.find(containerName).iCheck('check').attr('checked', true);
|
||||
else
|
||||
@root.find(containerName).iCheck('uncheck').attr('checked', false);
|
||||
@iCheckIgnore = false
|
||||
|
||||
enableCheckBoxTargets: (e) ->
|
||||
checkboxes = @root.find('input[type="checkbox"]')
|
||||
context.JK.checkbox(checkboxes)
|
||||
checkboxes.on('ifChanged', (e)=> @checkboxChanged(e))
|
||||
true
|
||||
|
||||
checkboxChanged: (e) ->
|
||||
if @iCheckIgnore
|
||||
return
|
||||
checkbox = $(e.target)
|
||||
name = checkbox.attr('name')
|
||||
checked = checkbox.is(':checked')
|
||||
logger.debug("check change", e.target.name, e.target.checked)
|
||||
this.setState({"#{e.target.name}": e.target.checked})
|
||||
|
||||
|
||||
screenName: () ->
|
||||
"pricing"
|
||||
|
||||
onTeacherStateChanged: (changes) ->
|
||||
|
||||
unless this.handleErrors(changes)
|
||||
teacher = changes.teacher
|
||||
this.setState({
|
||||
price_per_lesson_30_cents: teacher.price_per_lesson_30_cents
|
||||
price_per_lesson_45_cents: teacher.price_per_lesson_45_cents
|
||||
price_per_lesson_60_cents: teacher.price_per_lesson_60_cents
|
||||
price_per_lesson_90_cents: teacher.price_per_lesson_90_cents
|
||||
price_per_lesson_120_cents: teacher.price_per_lesson_120_cents
|
||||
price_per_month_30_cents: teacher.price_per_month_30_cents
|
||||
price_per_month_45_cents: teacher.price_per_month_45_cents
|
||||
price_per_month_60_cents: teacher.price_per_month_60_cents
|
||||
price_per_month_90_cents: teacher.price_per_month_90_cents
|
||||
price_per_month_120_cents: teacher.price_per_month_120_cents
|
||||
prices_per_lesson: teacher.prices_per_lesson
|
||||
prices_per_month: teacher.prices_per_month
|
||||
lesson_duration_30: teacher.lesson_duration_30
|
||||
lesson_duration_45: teacher.lesson_duration_45
|
||||
lesson_duration_60: teacher.lesson_duration_60
|
||||
lesson_duration_90: teacher.lesson_duration_90
|
||||
lesson_duration_120: teacher.lesson_duration_120
|
||||
})
|
||||
false
|
||||
|
||||
|
||||
|
||||
captureFormState: (e) ->
|
||||
this.setState({
|
||||
prices_per_lesson: $("[name='prices_per_lesson_input']", @root).is(":checked")
|
||||
prices_per_month: $("[name='prices_per_month_input']", @root).is(":checked")
|
||||
lesson_duration_30: $("[name='lesson_duration_30_input']", @root).is(":checked")
|
||||
lesson_duration_45: $("[name='lesson_duration_45_input']", @root).is(":checked")
|
||||
lesson_duration_60: $("[name='lesson_duration_60_input']", @root).is(":checked")
|
||||
lesson_duration_90: $("[name='lesson_duration_90_input']", @root).is(":checked")
|
||||
lesson_duration_120: $("[name='lesson_duration_120_input']", @root).is(":checked")
|
||||
})
|
||||
|
||||
#this.forceUpdate()
|
||||
|
||||
captureCurrency: (e) ->
|
||||
for minutes in [30, 45, 60, 90, 120]
|
||||
pricePerLessonCents = context.JK.ProfileUtils.normalizeMoneyForSubmit($("[name='price_per_lesson_#{minutes}_cents']", @root).val())
|
||||
pricePerMonthCents = context.JK.ProfileUtils.normalizeMoneyForSubmit($("[name='price_per_month_#{minutes}_cents']", @root).val())
|
||||
|
||||
this.setState({
|
||||
"price_per_lesson_#{minutes}_cents": pricePerLessonCents
|
||||
"price_per_month_#{minutes}_cents": pricePerMonthCents
|
||||
})
|
||||
|
||||
displayLessonAmount = context.JK.ProfileUtils.normalizeMoneyForDisplay(pricePerLessonCents)
|
||||
displayMonthAmount = context.JK.ProfileUtils.normalizeMoneyForDisplay(pricePerMonthCents)
|
||||
$("[name='price_per_lesson_#{minutes}_cents']", @root).val(displayLessonAmount)
|
||||
$("[name='price_per_month_#{minutes}_cents']", @root).val(displayMonthAmount)
|
||||
|
||||
navDestination: (instructions) ->
|
||||
navTo=null
|
||||
if instructions?
|
||||
if instructions.direction=="cancel"
|
||||
navTo = @teacherSetupSource()
|
||||
else if instructions.direction=="back"
|
||||
navTo = @teacherSetupDestination("experience")
|
||||
else if instructions.direction=="next"
|
||||
# We are done:
|
||||
navTo = @teacherSetupSource()
|
||||
|
||||
navTo
|
||||
|
||||
handleNav: (e) ->
|
||||
navTo = this.navDestination(e)
|
||||
teacherActions.change.trigger(this.state, {navTo: navTo})
|
||||
|
||||
handleFocus: (e) ->
|
||||
@pricePerLessonCents=e.target.value
|
||||
|
||||
handleTextChange: (e) ->
|
||||
@pricePerLessonCents=e.target.value
|
||||
this.forceUpdate()
|
||||
|
||||
handleCheckChange: (e) ->
|
||||
if @iCheckIgnore
|
||||
return
|
||||
logger.debug("check change", e.target.name, e.target.checked)
|
||||
this.setState({"#{e.target.name}": e.target.checked})
|
||||
|
||||
render: () ->
|
||||
priceRows = []
|
||||
logger.debug("Current State is", this.state)
|
||||
for minutes in [30, 45, 60, 90, 120]
|
||||
pricePerLessonName = "price_per_lesson_#{minutes}_cents"
|
||||
pricePerMonthName = "price_per_month_#{minutes}_cents"
|
||||
priceKey = "lesson_duration_#{minutes}"
|
||||
inputName = "#{priceKey}_input"
|
||||
containerName = "#{priceKey}_container"
|
||||
durationChecked = this.state[priceKey]
|
||||
|
||||
# If we are currently editing, don't format; used cache value:
|
||||
if $("[name='#{pricePerLessonName}']", @root).is(":focus")
|
||||
pricePerLessonCents = @pricePerLessonCents
|
||||
else
|
||||
ppl_fld_name="price_per_lesson_"+minutes+"_cents"
|
||||
pricePerLessonCents = context.JK.ProfileUtils.normalizeMoneyForDisplay(this.state[ppl_fld_name])
|
||||
|
||||
|
||||
# If we are currently editing, don't format; used cache value:
|
||||
if $("[name='#{pricePerMonthName}']", @root).is(":focus")
|
||||
pricePerMonthCents = @pricePerMonthCents
|
||||
else
|
||||
pricePerMonthCents = context.JK.ProfileUtils.normalizeMoneyForDisplay(this.state["price_per_month_"+minutes+"_cents"])
|
||||
|
||||
pricesPerLessonEnabled = this.state.prices_per_lesson
|
||||
pricesPerMonthEnabled = this.state.prices_per_month
|
||||
|
||||
monthlyEnabled = durationChecked && pricesPerMonthEnabled
|
||||
lessonEnabled = durationChecked && pricesPerLessonEnabled
|
||||
perMonthInputStyles = classNames({"per-month-target" : true, disabled: !monthlyEnabled})
|
||||
perLessonInputStyles = classNames({"per-lesson-target": true, disabled: !lessonEnabled})
|
||||
|
||||
|
||||
priceRows.push `
|
||||
<div className="teacher-price-row" key={minutes}>
|
||||
<div className="teacher-half-column left pricing-options">
|
||||
<div className="teacher-field" name={containerName}>
|
||||
<input type='checkbox'
|
||||
className={"checkbox-enabler " + priceKey}
|
||||
data-enable-target={"lesson-"+minutes+"-target"}
|
||||
name={priceKey}
|
||||
key={priceKey}
|
||||
checked={this.state[priceKey]}
|
||||
onChange={this.handleCheckChange}
|
||||
ref={priceKey}>
|
||||
</input>
|
||||
<label htmlFor='{priceKey}' key='{priceKey}' className="checkbox-label">
|
||||
{minutes} Minutes
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="teacher-half-column right pricing-amounts">
|
||||
<div className={"teacher-field pricing-field month-"+minutes+"-target"}>
|
||||
<div className="teacher-third-column minute-label inline">
|
||||
<label>{minutes} Minutes</label>
|
||||
</div>
|
||||
<div className="teacher-third-column inline per-lesson">
|
||||
<input key={minutes}
|
||||
name={pricePerLessonName}
|
||||
ref={pricePerLessonName}
|
||||
className={perLessonInputStyles}
|
||||
type="text"
|
||||
min="0"
|
||||
max="100000"
|
||||
value={pricePerLessonCents}
|
||||
onBlur={this.captureCurrency}
|
||||
onChange={this.handleTextChange}
|
||||
onFocus={this.handleFocus}
|
||||
disabled={!lessonEnabled}>
|
||||
</input>
|
||||
</div>
|
||||
<div className="teacher-third-column inline per-month">
|
||||
<input key={minutes}
|
||||
name={pricePerMonthName}
|
||||
ref={pricePerMonthName}
|
||||
className={perMonthInputStyles}
|
||||
type="text"
|
||||
min="0"
|
||||
max="100000"
|
||||
value={pricePerMonthCents}
|
||||
onBlur={this.captureCurrency}
|
||||
onChange={this.handleTextChange}
|
||||
onFocus={this.handleFocus}
|
||||
disabled={!monthlyEnabled}>
|
||||
</input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br className="clearall"/>
|
||||
</div>`
|
||||
|
||||
# Render:
|
||||
`<div className="TeacherSetupPricing TeacherSetupComponent" >
|
||||
<div className="teacher-half-column left pricing-options">
|
||||
<h3 className="margined">Offer Lessons Pricing & Payments:</h3>
|
||||
|
||||
<div className="teacher-field" name="prices_per_lesson_container">
|
||||
<input type='checkbox' className='checkbox-enabler' data-enable-target="per-lesson-target" name="prices_per_lesson" checked={this.state.prices_per_lesson} ref="prices_per_lesson" onChange={this.handleCheckChange}></input>
|
||||
<label htmlFor='prices_per_lesson' className="checkbox-label">Per Lesson</label>
|
||||
</div>
|
||||
|
||||
<div className="teacher-field" name="prices_per_month_container">
|
||||
<input type='checkbox' className='checkbox-enabler' data-enable-target="per-month-target" name="prices_per_month" checked={this.state.prices_per_month} ref="prices_per_month" onChange={this.handleCheckChange}></input>
|
||||
<label htmlFor='prices_per_month' className="checkbox-label">Per Month</label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="teacher-half-column right pricing-amounts">
|
||||
<h3 className="margined pricing-amount-text-prompt">Please fill in the prices (in US Dollars) for the lessons you have chosen to offer in the boxes below:</h3>
|
||||
</div>
|
||||
<br className="clearall"/>
|
||||
|
||||
<div className="teacher-price-row">
|
||||
<div className="teacher-half-column left pricing-options">
|
||||
<h3 className="margined">Offer Lessons of These Durations:</h3>
|
||||
</div>
|
||||
<div className="teacher-half-column right pricing-amounts">
|
||||
<div className="teacher-third-column"> </div>
|
||||
<div className="teacher-third-column">
|
||||
<h3 className="margined">Price Per Lesson</h3>
|
||||
</div>
|
||||
<div className="teacher-third-column">
|
||||
<h3 className="margined">Price Per Month</h3>
|
||||
</div>
|
||||
</div>
|
||||
<br className="clearall"/>
|
||||
</div>
|
||||
|
||||
{priceRows}
|
||||
|
||||
<br className="clearall"/>
|
||||
<TeacherSetupNav handleNav={this.handleNav} last={true}></TeacherSetupNav>
|
||||
</div>`
|
||||
|
||||
|
||||
})
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
context = window
|
||||
logger = context.JK.logger
|
||||
|
||||
@TeacherStudentLevel = React.createClass({
|
||||
|
||||
|
||||
render: () ->
|
||||
`<span>
|
||||
<input objectName={this.props.student} type="checkbox" className="student-level" onChange={this.studentLevelChanged.bind(this, this.props.student)} checked={this.state.checked}/>
|
||||
<span className="student-level">{this.props.display}</span>
|
||||
</span>`
|
||||
|
||||
studentLevelChanged: (level, e) ->
|
||||
@setState({checked: $(e.target).is(':checked') })
|
||||
|
||||
@props.onChange(this.props.student, $(e.target).is(':checked'))
|
||||
|
||||
getInitialState: () ->
|
||||
{checked:@props.level}
|
||||
|
||||
componentWillReceiveProps: (nextProps) ->
|
||||
@setState({checked: nextProps.level})
|
||||
})
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
context = window
|
||||
rest = window.JK.Rest()
|
||||
logger = context.JK.logger
|
||||
|
||||
@YearSelect = React.createClass({
|
||||
|
||||
|
||||
render: () ->
|
||||
options = []
|
||||
|
||||
now = new Date().getFullYear()
|
||||
for yr in [1901..now]
|
||||
options.push `<option value={yr}>{yr}</option>`
|
||||
|
||||
`<select className="YearSelect react-component" name={this.props.name} required placeholder="Select" defaultValue="2010">
|
||||
{options}
|
||||
</select>`
|
||||
})
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
context = window
|
||||
|
||||
@GenreActions = Reflux.createActions({
|
||||
|
||||
})
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
context = window
|
||||
|
||||
@InstrumentActions = Reflux.createActions({
|
||||
|
||||
})
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
context = window
|
||||
|
||||
@LanguageActions = Reflux.createActions({
|
||||
|
||||
})
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
context = window
|
||||
|
||||
@ProfileActions = Reflux.createActions({
|
||||
|
||||
startTeacherEdit: {}
|
||||
cancelTeacherEdit: {}
|
||||
doneTeacherEdit: {}
|
||||
startProfileEdit: {}
|
||||
cancelProfileEdit: {}
|
||||
doneProfileEdit: {}
|
||||
editProfileNext: {}
|
||||
})
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
context = window
|
||||
|
||||
@SubjectActions = Reflux.createActions({
|
||||
|
||||
})
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
context = window
|
||||
|
||||
@TeacherActions = Reflux.createActions({
|
||||
load: {},
|
||||
change: {}
|
||||
})
|
||||
|
||||
context.JK.Actions.Teacher = TeacherActions
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
context = window
|
||||
teacherActions = window.JK.Actions.Teacher
|
||||
|
||||
@TeacherProfileMixin = {
|
||||
onAppInit: (app) ->
|
||||
logger.debug("TeacherProfile onAppInit", app, document.referrer)
|
||||
screenBindings = {
|
||||
'beforeShow': @beforeShow
|
||||
}
|
||||
|
||||
logger.debug("Binding setup to: teachers/profile/#{@screenName()}")
|
||||
app.bindScreen("teachers/profile/#{@screenName()}", screenBindings)
|
||||
|
||||
beforeShow: (data) ->
|
||||
logger.debug("TeacherProfile beforeShow", data, data.d)
|
||||
|
||||
if data? && data.d?
|
||||
@teacherId = data.d
|
||||
teacherActions.load.trigger({teacher_id: @teacherId})
|
||||
else
|
||||
teacherActions.load.trigger({})
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
context = window
|
||||
teacherActions = window.JK.Actions.Teacher
|
||||
|
||||
@TeacherSetupMixin = {
|
||||
onAppInit: (app) ->
|
||||
@app=app
|
||||
screenBindings = {
|
||||
'beforeShow': @beforeShow
|
||||
}
|
||||
@root = jQuery(this.getDOMNode())
|
||||
@app.bindScreen("teachers/setup/#{@screenName()}", screenBindings)
|
||||
|
||||
beforeShow: (data) ->
|
||||
if data? && data.d?
|
||||
@teacherId = data.d
|
||||
teacherActions.load.trigger({teacher_id: @teacherId})
|
||||
else
|
||||
teacherActions.load.trigger({})
|
||||
|
||||
# TODO: Determine who started us and store, so
|
||||
# we can return there in case of cancel, or being
|
||||
# done. For now, teacherSetupSource() will return
|
||||
# a default location:
|
||||
@postmark = null
|
||||
# params = this.getParams()
|
||||
# @postmark = params.p
|
||||
|
||||
handleErrors: (changes) ->
|
||||
$(".error-text", @root).remove()
|
||||
if changes.errors?
|
||||
@addError(k,v) for k,v of changes.errors
|
||||
|
||||
changes.errors?
|
||||
|
||||
addError: (k,v) ->
|
||||
teacherField = @root.find(".teacher-field[name='#{k}']")
|
||||
teacherField.append("<div class='error-text'>#{v.join()}</div>")
|
||||
$("input", teacherField).addClass("input-error")
|
||||
|
||||
getParams:() =>
|
||||
params = {}
|
||||
q = window.location.href.split("?")[1]
|
||||
if q?
|
||||
q = q.split('#')[0]
|
||||
raw_vars = q.split("&")
|
||||
for v in raw_vars
|
||||
[key, val] = v.split("=")
|
||||
params[key] = decodeURIComponent(val)
|
||||
params
|
||||
|
||||
teacherSetupSource:() ->
|
||||
if @postmark? then @postmark else "/client#/account"
|
||||
|
||||
teacherSetupDestination:(phase) ->
|
||||
pm = if @postmark? then "?p=#{encodeURIComponent(@postmark)}" else ""
|
||||
# TODO: encode postmark as part of this URI when available:
|
||||
"/client#/teachers/setup/#{phase}"
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
$ = jQuery
|
||||
context = window
|
||||
logger = context.JK.logger
|
||||
|
||||
@GenreStore = Reflux.createStore(
|
||||
{
|
||||
listenables: @GenreActions
|
||||
genres: []
|
||||
genresLookup: {}
|
||||
|
||||
init: ->
|
||||
# Register with the app store to get @app
|
||||
this.listenTo(context.AppStore, this.onAppInit)
|
||||
|
||||
onAppInit: (@app) ->
|
||||
rest.getGenres().done (genres) =>
|
||||
@genres = genres
|
||||
for genre in genres
|
||||
@genresLookup[genre.id] = genre.description
|
||||
|
||||
@trigger(@genres)
|
||||
|
||||
display: (id) ->
|
||||
@genresLookup[id]
|
||||
}
|
||||
)
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
$ = jQuery
|
||||
context = window
|
||||
logger = context.JK.logger
|
||||
|
||||
@InstrumentStore = Reflux.createStore(
|
||||
{
|
||||
listenables: @InstrumentActions
|
||||
instruments: []
|
||||
instrumentLookup: {}
|
||||
|
||||
init: ->
|
||||
# Register with the app store to get @app
|
||||
this.listenTo(context.AppStore, this.onAppInit)
|
||||
|
||||
onAppInit: (@app) ->
|
||||
rest.getInstruments().done (instruments) =>
|
||||
@instruments = instruments
|
||||
for instrument in instruments
|
||||
@instrumentLookup[instrument.id] = instrument.description
|
||||
|
||||
@trigger(@instruments)
|
||||
|
||||
display: (id) ->
|
||||
@instrumentLookup[id]
|
||||
}
|
||||
)
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
$ = jQuery
|
||||
context = window
|
||||
logger = context.JK.logger
|
||||
|
||||
@LanguageStore = Reflux.createStore(
|
||||
{
|
||||
listenables: @LanguageActions
|
||||
languages: []
|
||||
languageLookup: {}
|
||||
|
||||
init: ->
|
||||
# Register with the app store to get @app
|
||||
this.listenTo(context.AppStore, this.onAppInit)
|
||||
|
||||
onAppInit: (@app) ->
|
||||
rest.getLanguages().done (languages) =>
|
||||
@languages = languages
|
||||
for language in @languages
|
||||
@languageLookup[language.id] = language.description
|
||||
@trigger(@languages)
|
||||
|
||||
display: (id) ->
|
||||
@languageLookup[id]
|
||||
}
|
||||
)
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
$ = jQuery
|
||||
context = window
|
||||
logger = context.JK.logger
|
||||
|
||||
ProfileActions = @ProfileActions
|
||||
|
||||
@ProfileStore = Reflux.createStore(
|
||||
{
|
||||
listenables: ProfileActions
|
||||
|
||||
returnNav: null
|
||||
solo: false
|
||||
|
||||
# step can be:
|
||||
# introduction
|
||||
# basics
|
||||
# pricing
|
||||
# experience
|
||||
onStartProfileEdit: (step, solo) ->
|
||||
|
||||
if !step?
|
||||
step = ''
|
||||
|
||||
if step != '' && step != 'samples' && step != 'interests' && step != 'experience'
|
||||
alert("invalid step: " + step)
|
||||
return
|
||||
|
||||
@solo = solo
|
||||
@returnNav = window.location.href
|
||||
|
||||
window.location = '/client#/account/profile/' + step
|
||||
|
||||
onDoneProfileEdit: () ->
|
||||
|
||||
if @returnNav
|
||||
window.location = @returnNav
|
||||
@returnNav = null
|
||||
else
|
||||
window.location = "/client#/profile/" + context.JK.currentUserId;
|
||||
|
||||
@solo = false
|
||||
|
||||
onCancelProfileEdit: () ->
|
||||
|
||||
if @returnNav
|
||||
window.location = @returnNav
|
||||
@returnNav = null
|
||||
else
|
||||
window.location = '/client#/profile/' + context.JK.currentUserId
|
||||
@solo = false
|
||||
|
||||
onStartTeacherEdit: (step, solo) ->
|
||||
|
||||
if !step?
|
||||
step = 'introduction'
|
||||
|
||||
if step != 'introduction' && step != 'basics' && step != 'pricing' && step != 'experience'
|
||||
alert("invalid step: " + step)
|
||||
return
|
||||
|
||||
@solo = solo
|
||||
@returnNav = window.location.href
|
||||
|
||||
window.location = '/client#/teachers/setup/' + step
|
||||
|
||||
onDoneTeacherEdit: () ->
|
||||
|
||||
if @solo
|
||||
if @returnNav
|
||||
window.location = @returnNav
|
||||
@returnNav = null
|
||||
else
|
||||
window.location = '/client#/home'
|
||||
|
||||
@solo = false
|
||||
|
||||
onCancelTeacherEdit: () ->
|
||||
|
||||
if @returnNav
|
||||
window.location = @returnNav
|
||||
@returnNav = null
|
||||
else
|
||||
window.location = '/client#/home'
|
||||
|
||||
@solo = false
|
||||
|
||||
onEditProfileNext: (step) ->
|
||||
|
||||
if @solo
|
||||
if @returnNav
|
||||
window.location = @returnNav
|
||||
@returnNav = null
|
||||
else
|
||||
window.location = '/client#/home'
|
||||
@solo = false
|
||||
else
|
||||
context.location = "/client#/account/profile/" + step
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
$ = jQuery
|
||||
context = window
|
||||
logger = context.JK.logger
|
||||
|
||||
@SubjectStore = Reflux.createStore(
|
||||
{
|
||||
listenables: @SubjectActions
|
||||
subjects: []
|
||||
subjectLookup: {}
|
||||
|
||||
init: ->
|
||||
# Register with the app store to get @app
|
||||
this.listenTo(context.AppStore, this.onAppInit)
|
||||
|
||||
onAppInit: (@app) ->
|
||||
rest.getSubjects().done (subjects) =>
|
||||
@subjects = subjects
|
||||
for subject in subjects
|
||||
@subjectLookup[subject.id] = subject.description
|
||||
|
||||
@trigger(@subjects)
|
||||
|
||||
display: (id) ->
|
||||
@subjectLookup[id]
|
||||
|
||||
}
|
||||
)
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
$ = jQuery
|
||||
context = window
|
||||
logger = context.JK.logger
|
||||
rest = context.JK.Rest()
|
||||
EVENTS = context.JK.EVENTS
|
||||
@teacherActions = window.JK.Actions.Teacher
|
||||
|
||||
@TeacherStore = Reflux.createStore({
|
||||
listenables: @teacherActions
|
||||
teacher: null
|
||||
|
||||
init: ->
|
||||
# Register with the app store to get @app
|
||||
this.listenTo(context.AppStore, this.onAppInit)
|
||||
this.listenTo(context.TeacherActions.load, this.onLoadTeacher)
|
||||
this.listenTo(context.TeacherActions.change, this.onSaveTeacher)
|
||||
|
||||
onAppInit: (app) ->
|
||||
@app = app
|
||||
|
||||
defaultTeacher: ->
|
||||
{
|
||||
experiences_teaching: []
|
||||
experiences_education: []
|
||||
experiences_award: []
|
||||
}
|
||||
|
||||
defaults: (teacher) ->
|
||||
|
||||
if teacher.languages.length == 0
|
||||
teacher.languages.push('EN')
|
||||
|
||||
onLoadTeacher: (options) ->
|
||||
logger.debug("onLoadTeacher", options)
|
||||
if !options?
|
||||
throw new Error('@teacher must be specified')
|
||||
|
||||
rest.getTeacher(options)
|
||||
.done((savedTeacher) =>
|
||||
logger.debug("LOADING TEACHER",savedTeacher)
|
||||
|
||||
@defaults(savedTeacher)
|
||||
|
||||
this.trigger({teacher: savedTeacher}))
|
||||
.fail((jqXHR, textStatus, errorMessage) =>
|
||||
logger.debug("FAILED",jqXHR, textStatus, errorMessage)
|
||||
if (jqXHR.status==404)
|
||||
this.trigger({teacher: this.defaultTeacher()})
|
||||
else
|
||||
context.JK.app.ajaxError(jqXHR, textStatus, errorMessage)
|
||||
)
|
||||
|
||||
onSaveTeacher: (teacher, instructions) ->
|
||||
logger.debug("onSaveTeacher", teacher, instructions)
|
||||
rest.updateTeacher(teacher)
|
||||
.done((savedTeacher) =>
|
||||
logger.debug("SAVED TEACHER",savedTeacher)
|
||||
this.trigger({teacher: savedTeacher})
|
||||
if ProfileStore.solo
|
||||
ProfileActions.doneTeacherEdit()
|
||||
else
|
||||
if instructions.navTo?
|
||||
logger.debug("NAVIGATING TO",instructions.navTo)
|
||||
window.location = instructions.navTo
|
||||
).fail((jqXHR, textStatus, errorMessage) =>
|
||||
logger.debug("FAILED",jqXHR, textStatus, errorMessage)
|
||||
#errors = JSON.parse(jqXHR.responseText)
|
||||
|
||||
if (jqXHR.status==422)
|
||||
logger.debug("FAILED422",jqXHR.responseJSON.errors)
|
||||
this.trigger({errors: jqXHR.responseJSON.errors})
|
||||
else
|
||||
context.JK.app.ajaxError(textStatus)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
@ -205,7 +205,7 @@ mixins.push(Reflux.listenTo(VideoStore, 'onVideoStateChanged'))
|
|||
componentWillUpdate: (nextProps, nextState) ->
|
||||
# protect against non-video clients pointed at video-enabled server from getting into a session
|
||||
|
||||
@logger.debug("webcam devices", nextState.deviceNames, @state.deviceNames)
|
||||
#@logger.debug("webcam devices", nextState.deviceNames, @state.deviceNames)
|
||||
|
||||
if !@initialScan?
|
||||
@initialScan = true
|
||||
|
|
@ -352,7 +352,7 @@ mixins.push(Reflux.listenTo(VideoStore, 'onVideoStateChanged'))
|
|||
webcamName = null
|
||||
# protect against non-video clients pointed at video-enabled server from getting into a session
|
||||
webcam = state.currentDevice
|
||||
@logger.debug("currently selected video device", webcam)
|
||||
#@logger.debug("currently selected video device", webcam)
|
||||
if (webcam? && Object.keys(webcam).length>0)
|
||||
webcamName = Object.keys(webcam)[0]
|
||||
|
||||
|
|
|
|||
|
|
@ -714,6 +714,11 @@
|
|||
return context.JK.padString(date.getMonth() + 1, 2) + "/" + context.JK.padString(date.getDate(), 2) + "/" + date.getFullYear() + " - " + date.toLocaleTimeString();
|
||||
}
|
||||
|
||||
context.JK.formatDateShort = function (dateString) {
|
||||
var date = dateString instanceof Date ? dateString : new Date(dateString);
|
||||
return months[date.getMonth()] + ' ' + date.getDate() + ', ' + date.getFullYear();
|
||||
}
|
||||
|
||||
// returns Fri May 20, 2013
|
||||
context.JK.formatDate = function (dateString, suppressDay) {
|
||||
var date = new Date(dateString);
|
||||
|
|
|
|||
|
|
@ -492,9 +492,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 1.05em;
|
||||
}
|
||||
}
|
||||
|
||||
#bands-screen {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
*= require_self
|
||||
*= require web/Raleway
|
||||
*= require jquery.ui.datepicker
|
||||
*= require jquery.jstarbox
|
||||
*= require ./ie
|
||||
*= require jquery.bt
|
||||
*= require easydropdown
|
||||
|
|
@ -88,6 +89,7 @@
|
|||
*= require ./jamTrackPreview
|
||||
*= require users/signinCommon
|
||||
*= require landings/partner_agreement_v1
|
||||
*= require ./teachers
|
||||
*= require_directory ./react-components
|
||||
|
||||
*/
|
||||
|
|
@ -322,7 +322,7 @@ input[type="button"] {
|
|||
box-shadow: inset 2px 2px 3px 0px #888;
|
||||
}
|
||||
|
||||
input[type="text"], input[type="password"]{
|
||||
input[type="text"], input[type="password"], input[type="url"], input[type="number"] {
|
||||
background-color:$ColorTextBoxBackground;
|
||||
border:none;
|
||||
padding:3px;
|
||||
|
|
|
|||
|
|
@ -27,6 +27,29 @@
|
|||
&.no-online-presence {
|
||||
display:block;
|
||||
}
|
||||
float:left;
|
||||
margin-right:20px;
|
||||
margin-top:20px;
|
||||
|
||||
&.bandcamp-presence img {
|
||||
|
||||
position:relative;
|
||||
top:9px;
|
||||
}
|
||||
|
||||
&.user-website img {
|
||||
position:relative;
|
||||
top:-5px;
|
||||
}
|
||||
}
|
||||
.performance-sample-option {
|
||||
margin-right:40px;
|
||||
a {
|
||||
display:block;
|
||||
}
|
||||
img {
|
||||
margin-bottom:5px;
|
||||
}
|
||||
}
|
||||
.instruments-holder {
|
||||
margin-bottom:20px;
|
||||
|
|
@ -78,6 +101,7 @@
|
|||
font-weight:600;
|
||||
font-size:18px;
|
||||
margin: 0px 0px 10px 0px;
|
||||
clear:both;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
@import "client/common.css.scss";
|
||||
@import "client/screen_common.css.scss";
|
||||
|
||||
.invisible {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.react-component {
|
||||
width: 100%;
|
||||
em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.checkbox-scroller, .editable-scroller {
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
width: 100%;
|
||||
min-height: 5em;
|
||||
max-height: 20em;
|
||||
@include border_box_sizing;
|
||||
text-align:left;
|
||||
margin-bottom:0.5em;
|
||||
margin-top:0.5em;
|
||||
padding: 0.25em;
|
||||
}
|
||||
|
||||
.editable-scroller {
|
||||
border: 2px solid #c5c5c5;
|
||||
color: #c5c5c5;
|
||||
height: 10em;
|
||||
padding:10px;
|
||||
.list-item {
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-scroller {
|
||||
background-color: #c5c5c5;
|
||||
height: 15em;
|
||||
.checkItem {
|
||||
clear: both;
|
||||
label {
|
||||
color: black;
|
||||
display: inline;
|
||||
float: left;
|
||||
font-size: 1em;
|
||||
}
|
||||
input {
|
||||
width: auto;
|
||||
text-align: left;
|
||||
float: left;
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,257 @@
|
|||
@import "client/common";
|
||||
|
||||
#teacher-profile {
|
||||
div[data-react-class="TeacherProfile"] {
|
||||
height:100%;
|
||||
}
|
||||
|
||||
.profile-nav a {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
height: 100%;
|
||||
width: 98%;
|
||||
margin: 0 auto;
|
||||
padding: 57px 0 0 0;
|
||||
@include border-box_sizing;
|
||||
}
|
||||
|
||||
.profile-tile {
|
||||
width:20%;
|
||||
float:left;
|
||||
@include border-box_sizing;
|
||||
height:87px;
|
||||
position:relative;
|
||||
}
|
||||
.profile-about-left {
|
||||
@include border-box_sizing;
|
||||
width:16%;
|
||||
}
|
||||
.profile-body {
|
||||
padding-top:159px;
|
||||
}
|
||||
.left-content {
|
||||
width:88px;
|
||||
text-align:center;
|
||||
}
|
||||
.profile-photo {
|
||||
width:16%;
|
||||
@include border-box_sizing;
|
||||
}
|
||||
.profile-nav {
|
||||
margin:0;
|
||||
width:84%;
|
||||
}
|
||||
.profile-wrapper {
|
||||
padding:10px 20px
|
||||
}
|
||||
|
||||
.main-content {
|
||||
float:left;
|
||||
@include border-box_sizing;
|
||||
width:84%;
|
||||
}
|
||||
|
||||
.edit-link {
|
||||
font-size:12px;
|
||||
margin-left:11px;
|
||||
font-weight:normal;
|
||||
}
|
||||
.activity {
|
||||
margin-top:20px;
|
||||
}
|
||||
|
||||
.backgroundCheck {
|
||||
margin-top:20px;
|
||||
}
|
||||
|
||||
.introductory-video {
|
||||
float:right;
|
||||
width:40%;
|
||||
position:relative;
|
||||
}
|
||||
|
||||
.info-block {
|
||||
h3 {
|
||||
font-weight:bold;
|
||||
font-size:18px;
|
||||
margin-bottom:10px;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom:40px;
|
||||
&.teachers {
|
||||
clear:both;
|
||||
}
|
||||
}
|
||||
|
||||
table.jamtable {
|
||||
font-size:14px;
|
||||
width:100%;
|
||||
|
||||
&.price-table {
|
||||
max-width:600px;
|
||||
|
||||
|
||||
th:nth-child(2) {
|
||||
text-align:right;
|
||||
}
|
||||
th:nth-child(3) {
|
||||
text-align:right;
|
||||
}
|
||||
|
||||
td:nth-child(2) {
|
||||
text-align:right;
|
||||
}
|
||||
td:nth-child(3) {
|
||||
text-align:right;
|
||||
}
|
||||
}
|
||||
&.giginfo {
|
||||
top: 10px;
|
||||
position: relative;
|
||||
min-width: 200px;
|
||||
width:auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.online-presence-option, .performance-sample-option {
|
||||
display:block;
|
||||
vertical-align:middle;
|
||||
&.no-online-presence {
|
||||
display:block;
|
||||
}
|
||||
float:left;
|
||||
margin-right:20px;
|
||||
margin-top:20px;
|
||||
|
||||
&.bandcamp-presence img {
|
||||
|
||||
position:relative;
|
||||
top:9px;
|
||||
}
|
||||
|
||||
&.user-website img {
|
||||
position:relative;
|
||||
top:-5px;
|
||||
}
|
||||
}
|
||||
|
||||
.performance-sample-option {
|
||||
margin-right:40px;
|
||||
a {
|
||||
display:block;
|
||||
}
|
||||
img {
|
||||
margin-bottom:5px;
|
||||
}
|
||||
}
|
||||
|
||||
.video-container {
|
||||
width: 100%;
|
||||
padding-bottom: 53.33%;
|
||||
|
||||
}
|
||||
|
||||
.profile-instrument {
|
||||
float: left;
|
||||
margin-right: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.experience {
|
||||
width:600px;
|
||||
margin-bottom:20px;
|
||||
h4 {
|
||||
font-size:14px;
|
||||
font-weight:bold;
|
||||
margin-bottom:6px;
|
||||
color:white;
|
||||
}
|
||||
.org {
|
||||
font-size:14px;
|
||||
}
|
||||
.years {float:right}
|
||||
}
|
||||
.ratings-block {
|
||||
|
||||
h3 {
|
||||
margin-bottom:30px;
|
||||
}
|
||||
h4 {
|
||||
margin-top:20px;
|
||||
font-weight:bold;
|
||||
margin-bottom:20px;
|
||||
color:white;
|
||||
}
|
||||
.ratings-box {
|
||||
display:inline-block;
|
||||
}
|
||||
.stars {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
left: 20px;
|
||||
}
|
||||
|
||||
.review-count {
|
||||
font-weight:normal;
|
||||
display: inline-block;
|
||||
margin-left: 40px;
|
||||
font-size:12px;
|
||||
color:$ColorTextTypical;
|
||||
}
|
||||
|
||||
.review {
|
||||
border-width:1px 0 0 0;
|
||||
border-color:$ColorTextTypical;
|
||||
border-style:solid;
|
||||
padding:20px 3px;
|
||||
|
||||
.review-header {
|
||||
margin-bottom:20px;
|
||||
}
|
||||
.avatar {
|
||||
display:inline-block;
|
||||
padding:1px;
|
||||
width:36px;
|
||||
height:36px;
|
||||
background-color:#ed3618;
|
||||
margin:0 20px 0 0;
|
||||
-webkit-border-radius:18px;
|
||||
-moz-border-radius:18px;
|
||||
border-radius:18px;
|
||||
}
|
||||
|
||||
.avatar-small {
|
||||
float:left;
|
||||
|
||||
}
|
||||
|
||||
.avatar img {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
-webkit-border-radius:18px;
|
||||
-moz-border-radius:18px;
|
||||
border-radius:18px;
|
||||
}
|
||||
.stars {
|
||||
top:4px;
|
||||
}
|
||||
|
||||
.review-time {
|
||||
float:right;
|
||||
}
|
||||
|
||||
.reviewer-name {
|
||||
height:40px;
|
||||
display:inline-block;
|
||||
line-height:40px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.reviewer-content {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,353 @@
|
|||
@import "client/common.css.scss";
|
||||
@import "client/screen_common.css.scss";
|
||||
|
||||
.teacher-setup {
|
||||
font-family: Raleway, Arial, Helvetica, verdana, arial, sans-serif;
|
||||
|
||||
.content-body-scroller {
|
||||
padding:20px;
|
||||
@include border_box_sizing;
|
||||
}
|
||||
.editable-scroller {
|
||||
width:265px
|
||||
}
|
||||
h2 {
|
||||
margin-bottom: 0.75em;
|
||||
}
|
||||
|
||||
.TeacherSetupComponent {
|
||||
@include border_box_sizing;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
|
||||
}
|
||||
|
||||
// introduction
|
||||
.teacher-setup-step-0 {
|
||||
.TeacherSetupNav {
|
||||
clear: both;
|
||||
margin-right:calc(5% - 20px);
|
||||
@include border_box_sizing;
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
// basics
|
||||
.teacher-setup-step-1 {
|
||||
.TeacherSetupNav {
|
||||
clear: both;
|
||||
margin-right:3px;
|
||||
@include border_box_sizing;
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
// experience
|
||||
.teacher-setup-step-2 {
|
||||
.TeacherSetupNav {
|
||||
clear: both;
|
||||
margin-right: calc(100% - 833px);
|
||||
@include border_box_sizing;
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// experience
|
||||
.teacher-setup-step-3 {
|
||||
.TeacherSetupNav {
|
||||
clear: both;
|
||||
margin-right: 22px;
|
||||
@include border_box_sizing;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.teacher-field {
|
||||
margin-bottom:10px !important;
|
||||
.icheckbox_minimal {
|
||||
display:inline-block;
|
||||
position:relative;
|
||||
top:4px;
|
||||
margin-right:4px;
|
||||
}
|
||||
|
||||
&.pricing-field {
|
||||
margin-top:1px;
|
||||
}
|
||||
}
|
||||
.teacher-half-column {
|
||||
&.pricing-options {
|
||||
width:40% !important;
|
||||
}
|
||||
|
||||
&.pricing-amounts {
|
||||
width:60% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.minute-label label {
|
||||
text-align:right;
|
||||
}
|
||||
|
||||
.pricing-amount-text-prompt {
|
||||
margin-left:14%;
|
||||
}
|
||||
}
|
||||
|
||||
.TeacherSetupBasics {
|
||||
.teacher-half-column {
|
||||
margin-top:20px;
|
||||
}
|
||||
|
||||
.student-levels, .student-ages {
|
||||
margin-top:20px;
|
||||
margin-bottom:7px;
|
||||
}
|
||||
input.student-level {
|
||||
display:inline;
|
||||
margin-right:10px;
|
||||
}
|
||||
span.student-level {
|
||||
display:inline;
|
||||
margin-right:30px;
|
||||
}
|
||||
select {
|
||||
width:auto !important;
|
||||
display:inline !important;
|
||||
}
|
||||
.age-to-age {
|
||||
margin:0 10px;
|
||||
display:inline;
|
||||
}
|
||||
}
|
||||
|
||||
.EditableList .list-item {
|
||||
position:relative;
|
||||
|
||||
.actions {
|
||||
position:absolute;
|
||||
top:0;
|
||||
right:0;
|
||||
}
|
||||
}
|
||||
|
||||
h3.margined {
|
||||
font-size:16px;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.inline {
|
||||
display: inline;
|
||||
//width: auto !important;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size:16px;
|
||||
}
|
||||
|
||||
.teacher-setup-form {
|
||||
padding: 1em;
|
||||
.error-text {
|
||||
display: block;
|
||||
//background-color: #600;
|
||||
color: #f00;
|
||||
}
|
||||
.teacher-small-column {
|
||||
width:35%;
|
||||
}
|
||||
|
||||
.teacher-big-column {
|
||||
width: 60%;
|
||||
margin-right:20px;
|
||||
}
|
||||
|
||||
.teacher-quarter-column {
|
||||
@extend .w25;
|
||||
@include border_box_sizing;
|
||||
float:left;
|
||||
padding: 0 10px;
|
||||
|
||||
&:first-child {
|
||||
padding-left:0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-right:0;
|
||||
}
|
||||
}
|
||||
|
||||
.teacher-half-column {
|
||||
width:50%;
|
||||
@include border_box_sizing;
|
||||
float:left;
|
||||
padding: 0 10px;
|
||||
|
||||
&:first-child {
|
||||
padding-left:0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-right:0;
|
||||
}
|
||||
}
|
||||
|
||||
.teacher-third-column {
|
||||
@include border_box_sizing;
|
||||
min-width: 1px;
|
||||
width: 33%;
|
||||
float: left;
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
table.form-table {
|
||||
width: 100%;
|
||||
margin-bottom: 1em;
|
||||
label, input, select {
|
||||
width: 100%;
|
||||
margin: 4px 4px 4px 0px;
|
||||
}
|
||||
|
||||
.inline-fields {
|
||||
display: inline;
|
||||
label, input, select {
|
||||
display: inline;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.enter-url {
|
||||
font-size:12px;
|
||||
margin-top:4px;
|
||||
}
|
||||
|
||||
.delete-list-item {
|
||||
color:$ColorTextTypical;
|
||||
}
|
||||
.TeacherSetupExperience {
|
||||
.year-range-cell {
|
||||
text-align:center;
|
||||
display:inline-block;
|
||||
width:165px;
|
||||
}
|
||||
select[name="start_year"] {
|
||||
float:left;
|
||||
margin:0 !important;
|
||||
display:inline-block;
|
||||
width:auto !important;
|
||||
}
|
||||
select[name="end_year"] {
|
||||
float:right;
|
||||
margin:0 !important;
|
||||
display:inline-block;
|
||||
width:auto !important;
|
||||
}
|
||||
label[for="end-year"] {
|
||||
vertical-align: middle;
|
||||
margin-top: 1px;
|
||||
display: inline-block;
|
||||
}
|
||||
.add-experience-btn {
|
||||
width:80px;
|
||||
font-size:12px;
|
||||
margin-right:0;
|
||||
}
|
||||
.teacher-third-column {
|
||||
width:280px;
|
||||
}
|
||||
.teacher-field {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.teacher-field {
|
||||
@include border_box_sizing;
|
||||
margin-bottom: 30px;
|
||||
|
||||
&.title {
|
||||
label {
|
||||
width:100px;
|
||||
display:inline-block;
|
||||
}
|
||||
input {
|
||||
width:165px;
|
||||
display:inline-block;
|
||||
}
|
||||
}
|
||||
&.organization {
|
||||
label {
|
||||
width:100px;
|
||||
display:inline-block;
|
||||
}
|
||||
input {
|
||||
width:165px;
|
||||
display:inline-block;
|
||||
}
|
||||
}
|
||||
&.date {
|
||||
label {
|
||||
width:100px;
|
||||
display:inline-block;
|
||||
|
||||
&[for="end-year"] {
|
||||
width:auto !important;
|
||||
height: 12px;
|
||||
vertical-align: middle;
|
||||
line-height: 12px;
|
||||
}
|
||||
}
|
||||
select {
|
||||
width:auto !important;
|
||||
}
|
||||
}
|
||||
input, select, textarea {
|
||||
@include border_box_sizing;
|
||||
overflow: hidden;
|
||||
width:100%;
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
height: 24px;
|
||||
width:45px;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
display: inline;
|
||||
width: auto;
|
||||
vertical-align: center;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
label {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
label.checkbox-label {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
textarea {
|
||||
height: auto;
|
||||
overflow:hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
label.strong-label {
|
||||
font-weight: bold;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
h3.sub-caption {
|
||||
font-weight: normal;
|
||||
font-size: 18px;
|
||||
margin: 8px 4px 8px 0px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
|
@ -77,6 +77,10 @@ body.web.individual_jamtrack {
|
|||
}
|
||||
}
|
||||
|
||||
.edit-link {
|
||||
font-size:12px;
|
||||
}
|
||||
|
||||
.video-container {
|
||||
width: 420px;
|
||||
padding-bottom: 53.33%;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
class ApiLanguagesController < ApiController
|
||||
|
||||
respond_to :json
|
||||
|
||||
def index
|
||||
@languages = Language.order(:description)
|
||||
@languages = @languages.sort_by { |l| l.id == 'EN' ? 0 : 1 }
|
||||
respond_with @languages
|
||||
end
|
||||
|
||||
def show
|
||||
@language = Language.find(params[:id])
|
||||
gon.language_id = @language.id
|
||||
gon.description = @language.description
|
||||
end
|
||||
|
||||
end
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
require 'sanitize'
|
||||
class ApiReviewsController < ApiController
|
||||
before_filter :api_signed_in_user, :except => [:index]
|
||||
before_filter :lookup_review_summary, :only => [:details]
|
||||
before_filter :lookup_review, :only => [:update, :delete, :show]
|
||||
|
||||
respond_to :json
|
||||
|
||||
# List review summaries according to params:
|
||||
def index
|
||||
summaries = ReviewSummary.index(params[:review])
|
||||
@reviews = summaries.paginate(page: params[:page], per_page: params[:per_page])
|
||||
respond_with @reviews, responder: ApiResponder, :status => 200
|
||||
end
|
||||
|
||||
# Create a review:
|
||||
def create
|
||||
@review = Review.new
|
||||
@review.target_id = params[:target_id]
|
||||
@review.user = current_user
|
||||
@review.rating = params[:rating]
|
||||
@review.description = params[:description]
|
||||
@review.target_type = params[:target_type]
|
||||
@review.save
|
||||
respond_with_model(@review)
|
||||
end
|
||||
|
||||
# List reviews matching targets for given review summary:
|
||||
def details
|
||||
reviews = Review.index(:target_id=>@review_summary.target_id)
|
||||
@reviews = reviews.paginate(page: params[:page], per_page: params[:per_page])
|
||||
respond_with @reviews, responder: ApiResponder, :status => 200
|
||||
end
|
||||
|
||||
# Update a review:
|
||||
def update
|
||||
mods = params[:mods]
|
||||
if mods.present?
|
||||
@review.rating = mods[:rating] if mods.key?(:rating)
|
||||
@review.description = mods[:description] if mods.key?(:description)
|
||||
@review.save
|
||||
end
|
||||
respond_with_model(@review)
|
||||
end
|
||||
|
||||
# Mark a review as deleted:
|
||||
def delete
|
||||
@review.deleted_at = Time.now()
|
||||
@review
|
||||
@review.save
|
||||
render :json => {}, status: 204
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def lookup_review_summary
|
||||
@review_summary = ReviewSummary.find(params[:review_summary_id])
|
||||
end
|
||||
|
||||
def lookup_review
|
||||
arel = Review.where("id=?", params[:id])
|
||||
arel = arel.where("user_id=?", current_user) unless current_user.admin
|
||||
@review = arel.first
|
||||
raise ActiveRecord::RecordNotFound, "Couldn't find review matching #{arel}" if @review.nil?
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
class ApiSubjectsController < ApiController
|
||||
|
||||
respond_to :json
|
||||
|
||||
def index
|
||||
@subjects = Subject.order(:description)
|
||||
respond_with @subjects
|
||||
end
|
||||
|
||||
def show
|
||||
@subject = Subject.find(params[:id])
|
||||
gon.subject_id = @subject.id
|
||||
gon.description = @subject.description
|
||||
end
|
||||
|
||||
end
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
class ApiTeachersController < ApiController
|
||||
|
||||
before_filter :api_signed_in_user, :except => [:index, :detail]
|
||||
before_filter :auth_teacher, :only => [:update, :delete]
|
||||
before_filter :auth_user, :only => [:create, :update]
|
||||
|
||||
respond_to :json
|
||||
|
||||
def index
|
||||
@teachers = Teacher.paginate(page: params[:page])
|
||||
end
|
||||
|
||||
def detail
|
||||
teacher_id=(params[:teacher_id].present?) ? params[:teacher_id] : (current_user.teacher && current_user.teacher.id)
|
||||
@teacher = Teacher.find(teacher_id)
|
||||
|
||||
respond_with_model(@teacher)
|
||||
end
|
||||
|
||||
def delete
|
||||
@teacher.try(:destroy)
|
||||
respond_with @teacher, responder => ApiResponder
|
||||
end
|
||||
|
||||
def create
|
||||
@teacher = Teacher.save_teacher(@user, params)
|
||||
respond_with_model(@teacher, new: true, location: lambda { return api_teacher_detail_url(@teacher.id) })
|
||||
end
|
||||
|
||||
def update
|
||||
@teacher = Teacher.save_teacher(@user, params)
|
||||
respond_with_model(@teacher)
|
||||
end
|
||||
|
||||
private
|
||||
def auth_teacher
|
||||
if current_user.admin
|
||||
@teacher = Teacher.find(params[:id])
|
||||
else
|
||||
@teacher = Teacher.where("user_id=? AND id=?", current_user.id, params[:id]).first
|
||||
end
|
||||
|
||||
unless @teacher
|
||||
Rails.logger.info("Could not find teacher #{params[:id]} for #{current_user}")
|
||||
raise JamPermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR
|
||||
end
|
||||
end
|
||||
|
||||
def auth_user
|
||||
if params[:user_id].present?
|
||||
if params[:user_id]==current_user.id
|
||||
@user=current_user
|
||||
else
|
||||
if current_user.admin
|
||||
@user=User.find(params[:user_id])
|
||||
else
|
||||
# Can't specify other user:
|
||||
raise JamPermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR
|
||||
end
|
||||
end
|
||||
else
|
||||
@user=current_user
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
|
@ -34,6 +34,9 @@ ApiUsersController < ApiController
|
|||
def show
|
||||
@user=lookup_user
|
||||
|
||||
@show_teacher = params[:show_teacher]
|
||||
@show_profile = params[:show_profile]
|
||||
|
||||
respond_with @user, responder: ApiResponder, :status => 200
|
||||
end
|
||||
|
||||
|
|
@ -56,6 +59,8 @@ ApiUsersController < ApiController
|
|||
:online_presences, :performance_samples])
|
||||
.find(params[:id])
|
||||
|
||||
@show_teacher_profile = params[:show_teacher]
|
||||
|
||||
respond_with @profile, responder: ApiResponder, :status => 200
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,102 @@
|
|||
object @teacher
|
||||
|
||||
attributes :id,
|
||||
:biography,
|
||||
:created_at,
|
||||
:id,
|
||||
:introductory_video,
|
||||
:lesson_duration_120,
|
||||
:lesson_duration_30,
|
||||
:lesson_duration_45,
|
||||
:lesson_duration_60,
|
||||
:lesson_duration_90,
|
||||
:price_per_lesson_120_cents,
|
||||
:price_per_lesson_30_cents,
|
||||
:price_per_lesson_45_cents,
|
||||
:price_per_lesson_60_cents,
|
||||
:price_per_lesson_90_cents,
|
||||
:price_per_month_120_cents,
|
||||
:price_per_month_30_cents,
|
||||
:price_per_month_45_cents,
|
||||
:price_per_month_60_cents,
|
||||
:price_per_month_90_cents,
|
||||
:prices_per_lesson,
|
||||
:prices_per_month,
|
||||
:teaches_advanced,
|
||||
:teaches_age_lower,
|
||||
:teaches_age_upper,
|
||||
:teaches_beginner,
|
||||
:teaches_intermediate,
|
||||
:updated_at,
|
||||
:user_id,
|
||||
:website,
|
||||
:years_playing,
|
||||
:years_teaching,
|
||||
:errors
|
||||
|
||||
child :review_summary => :review_summary do
|
||||
attributes :avg_rating, :wilson_score, :review_count
|
||||
|
||||
end
|
||||
|
||||
child :recent_reviews => :recent_reviews do
|
||||
attributes :description, :rating, :created_at
|
||||
|
||||
child(:user => :user) {
|
||||
attributes :id, :first_name, :last_name, :name, :photo_url
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
|
||||
node :instruments do |teacher|
|
||||
teacher.instruments.collect{|o|o.id}
|
||||
end
|
||||
|
||||
node :subjects do |teacher|
|
||||
teacher.subjects.collect{|o|o.id}
|
||||
end
|
||||
|
||||
node :genres do |teacher|
|
||||
teacher.genres.collect{|o|o.id}
|
||||
end
|
||||
|
||||
node :languages do |teacher|
|
||||
teacher.languages.collect{|o|o.id}
|
||||
end
|
||||
|
||||
node :experiences_teaching do |teacher|
|
||||
teacher.experiences_teaching.collect do |o|
|
||||
{
|
||||
name: o.name,
|
||||
experience_type: o.experience_type,
|
||||
organization: o.organization,
|
||||
start_year: o.start_year,
|
||||
end_year: o.end_year
|
||||
}
|
||||
end # collect
|
||||
end
|
||||
|
||||
node :experiences_education do |teacher|
|
||||
teacher.experiences_education.collect do |o|
|
||||
{
|
||||
name: o.name,
|
||||
experience_type: o.experience_type,
|
||||
organization: o.organization,
|
||||
start_year: o.start_year,
|
||||
end_year: o.end_year
|
||||
}
|
||||
end # collect
|
||||
end
|
||||
|
||||
node :experiences_award do |teacher|
|
||||
teacher.experiences_award.collect do |o|
|
||||
{
|
||||
name: o.name,
|
||||
experience_type: o.experience_type,
|
||||
organization: o.organization,
|
||||
start_year: o.start_year,
|
||||
end_year: o.end_year
|
||||
}
|
||||
end # collect
|
||||
end
|
||||
|
|
@ -30,4 +30,10 @@ end
|
|||
|
||||
child :musician_instruments => :instruments do
|
||||
attributes :description, :proficiency_level, :priority, :instrument_id
|
||||
end
|
||||
end
|
||||
|
||||
if @show_teacher_profile && @profile && @profile.teacher
|
||||
node :teacher do
|
||||
partial("api_teachers/detail", :object => @profile.teacher)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,6 +9,19 @@ else
|
|||
node :location do @user.online ? 'Online' : 'Offline' end
|
||||
end
|
||||
|
||||
if @show_teacher && @user.teacher
|
||||
node :teacher do
|
||||
partial("api_teachers/detail", :object => @user.teacher)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
if @show_profile
|
||||
node :profile do
|
||||
partial("api_users/profile_show", :object => @user)
|
||||
end
|
||||
end
|
||||
|
||||
# give back more info if the user being fetched is yourself
|
||||
if @user == current_user
|
||||
attributes :email, :original_fpfile, :cropped_fpfile, :crop_selection, :session_settings, :show_whats_next, :show_whats_next_count, :subscribe_email, :auth_twitter, :new_notifications, :sales_count, :reuse_card, :purchased_jamtracks_count, :first_downloaded_client_at, :created_at, :first_opened_jamtrack_web_player, :gifted_jamtracks, :has_redeemable_jamtrack
|
||||
|
|
@ -115,3 +128,4 @@ end
|
|||
node :last_jam_audio_latency do |user|
|
||||
user.last_jam_audio_latency.round if user.last_jam_audio_latency
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@
|
|||
<div class="user-header">
|
||||
<h2 id="username"></h2>
|
||||
<%= link_to("EDIT PROFILE", '/client#/account/profile', :class => "button-orange edit-profile-btn") %>
|
||||
<%= link_to("EDIT TEACHER PROFILE", '/client#/profile/teachers/setup/introduction', :class => "button-orange edit-teacher-profile-btn") %>
|
||||
<%= link_to("VIEW TEACHER PROFILE", '/client#/profile/profile/teacher/ID', :class => "button-orange view-teacher-profile-btn") %>
|
||||
</div>
|
||||
|
||||
<!-- action buttons -->
|
||||
|
|
|
|||
|
|
@ -38,6 +38,11 @@
|
|||
<%= render "bandProfile" %>
|
||||
<%= render "band_setup" %>
|
||||
<%= render "band_setup_photo" %>
|
||||
<%= render "clients/teachers/setup/introduction" %>
|
||||
<%= render "clients/teachers/setup/basics" %>
|
||||
<%= render "clients/teachers/setup/experience" %>
|
||||
<%= render "clients/teachers/setup/pricing" %>
|
||||
<%= render "clients/teachers/profile/profile" %>
|
||||
<%= render "users/feed_music_session_ajax" %>
|
||||
<%= render "users/feed_recording_ajax" %>
|
||||
<%= render "jamtrack_search" %>
|
||||
|
|
@ -245,7 +250,7 @@
|
|||
var accountProfileInterests = new JK.AccountProfileInterests(JK.app);
|
||||
accountProfileInterests.initialize();
|
||||
|
||||
var accountProfileSamples = new JK.AccountProfileSamples(JK.app, $(".account-profile-samples"), api.getUserProfile, api.updateUser)
|
||||
var accountProfileSamples = new JK.AccountProfileSamples(JK.app, $(".screen.account-profile-samples"), api.getUserProfile, api.updateUser)
|
||||
accountProfileSamples.initialize();
|
||||
|
||||
var accountAudioProfile = new JK.AccountAudioProfile(JK.app);
|
||||
|
|
@ -358,6 +363,8 @@
|
|||
|
||||
var signinDialog = new JK.SigninDialog(JK.app);
|
||||
signinDialog.initialize();
|
||||
|
||||
|
||||
JK.SigninPage(); // initialize signin helper
|
||||
|
||||
// do a client update early check upon initialization
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
#teacher-setup-basics.screen.secondary layout="screen" layout-id="teachers/profile/basics"
|
||||
.content-head
|
||||
.content-icon
|
||||
= image_tag "content/icon_bands.png", :size => "19x19"
|
||||
h1#teacher-setup-title
|
||||
| my account
|
||||
= render "screen_navigation"
|
||||
.content-body
|
||||
.content-body-scroller
|
||||
form.teacher-setup-form
|
||||
.teacher-setup-step-0.teacher-step.content-wrapper
|
||||
h2 edit teacher profile: basics
|
||||
= react_component 'TeacherProfileBasics', {}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
#teacher-profile.screen.secondary.no-login-required layout="screen" layout-id="profile/teacher" layout-arg="id"
|
||||
.content-head
|
||||
.content-icon
|
||||
= image_tag "content/icon_profile.png", :size => "19x19"
|
||||
h1#teacher-setup-title
|
||||
| teacher profile
|
||||
= render "screen_navigation"
|
||||
.content-body
|
||||
= react_component 'TeacherProfile', {}
|
||||
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
#teacher-setup-basics.teacher-setup.screen.secondary layout="screen" layout-id="teachers/setup/basics"
|
||||
.content-head
|
||||
.content-icon
|
||||
= image_tag "content/icon_bands.png", :size => "19x19"
|
||||
h1#teacher-setup-title
|
||||
| my account
|
||||
= render "screen_navigation"
|
||||
.content-body
|
||||
.content-body-scroller
|
||||
.teacher-setup-form
|
||||
.teacher-setup-step-1.teacher-step.content-wrapper
|
||||
h2 edit teacher profile: basics
|
||||
=react_component 'TeacherSetupBasics', {}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
#teacher-setup-experience.teacher-setup.screen.secondary layout="screen" layout-id="teachers/setup/experience"
|
||||
.content-head
|
||||
.content-icon
|
||||
= image_tag "content/icon_bands.png", :size => "19x19"
|
||||
h1#teacher-setup-title
|
||||
| my account
|
||||
= render "screen_navigation"
|
||||
.content-body
|
||||
.content-body-scroller
|
||||
.teacher-setup-form
|
||||
.teacher-setup-step-2.teacher-step.content-wrapper
|
||||
h2 edit teacher profile: experience
|
||||
=react_component 'TeacherSetupExperience', {}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
#teacher-setup-introduction.teacher-setup.screen.secondary layout="screen" layout-id="teachers/setup/introduction"
|
||||
.content-head
|
||||
.content-icon
|
||||
= image_tag "content/icon_bands.png", :size => "19x19"
|
||||
h1#teacher-setup-title
|
||||
| my account
|
||||
= render "screen_navigation"
|
||||
.content-body
|
||||
.content-body-scroller
|
||||
.teacher-setup-form
|
||||
.teacher-setup-step-0.teacher-step.content-wrapper
|
||||
h2 edit teacher profile: introduction
|
||||
=react_component 'TeacherSetupIntroduction'
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
#teacher-setup-pricing.teacher-setup.screen.secondary layout="screen" layout-id="teachers/setup/pricing"
|
||||
.content-head
|
||||
.content-icon
|
||||
= image_tag "content/icon_bands.png", :size => "19x19"
|
||||
h1#teacher-setup-title
|
||||
| my account
|
||||
= render "screen_navigation"
|
||||
.content-body
|
||||
.content-body-scroller
|
||||
.teacher-setup-form
|
||||
.teacher-setup-step-3.teacher-step.content-wrapper
|
||||
h2 edit teacher profile: pricing
|
||||
=react_component 'TeacherSetupPricing', {}
|
||||
|
|
@ -301,6 +301,12 @@ SampleApp::Application.routes.draw do
|
|||
# genres
|
||||
match '/genres' => 'api_genres#index', :via => :get
|
||||
|
||||
# language
|
||||
match '/languages' => 'api_languages#index', :via => :get
|
||||
|
||||
# subjects
|
||||
match '/subjects' => 'api_subjects#index', :via => :get
|
||||
|
||||
# users
|
||||
match '/users/isp_scoring' => 'api_users#isp_scoring', :via => :post , :as => 'isp_scoring'
|
||||
|
||||
|
|
@ -320,6 +326,12 @@ SampleApp::Application.routes.draw do
|
|||
match '/users/authorizations/google' => 'api_users#google_auth', :via => :get
|
||||
match '/users/:id/set_password' => 'api_users#set_password', :via => :post
|
||||
|
||||
match '/reviews' => 'api_reviews#index', :via => :get
|
||||
match '/reviews' => 'api_reviews#create', :via => :post
|
||||
match '/reviews/:id' => 'api_reviews#update', :via => :post
|
||||
match '/reviews/:id' => 'api_reviews#delete', :via => :delete
|
||||
match '/reviews/details/:review_summary_id' => 'api_users#details', :via => :get, :as => 'api_summary_reviews'
|
||||
|
||||
# recurly
|
||||
match '/recurly/create_account' => 'api_recurly#create_account', :via => :post
|
||||
match '/recurly/delete_account' => 'api_recurly#delete_account', :via => :delete
|
||||
|
|
@ -471,6 +483,13 @@ SampleApp::Application.routes.draw do
|
|||
match '/bands/:id' => 'api_bands#update', :via => :post
|
||||
match '/bands/:id' => 'api_bands#delete', :via => :delete
|
||||
|
||||
# teachers
|
||||
match '/teachers' => 'api_teachers#index', :via => :get
|
||||
match '/teachers/detail' => 'api_teachers#detail', :via => :get, :as => 'api_teacher_detail'
|
||||
match '/teachers' => 'api_teachers#create', :via => :post
|
||||
match '/teachers/:id' => 'api_teachers#update', :via => :post
|
||||
match '/teachers/:id' => 'api_teachers#delete', :via => :delete
|
||||
|
||||
# photo
|
||||
match '/bands/:id/photo' => 'api_bands#update_photo', :via => :post
|
||||
match '/bands/:id/photo' => 'api_bands#delete_photo', :via => :delete
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ SitemapGenerator::Sitemap.create do
|
|||
add(product_platform_path, priority: 0.9)
|
||||
add(product_jamtracks_path, priority: 0.9)
|
||||
add(corp_about_path, priority: 0.9)
|
||||
add(buy_gift_card_path, priority, 0.9)
|
||||
|
||||
JamTrack.all.each do |jam_track|
|
||||
slug = jam_track.slug
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -64,6 +64,18 @@ namespace :db do
|
|||
make_recording
|
||||
end
|
||||
|
||||
task populate_reviews: :environment do
|
||||
Teacher.all.each do |teacher|
|
||||
@review = Review.new
|
||||
@review.target_id = teacher.id
|
||||
@review.user = User.last
|
||||
@review.rating = 5
|
||||
@review.description = 'Omg This teacher was so good. It was like whoa. Crazy whoa.'
|
||||
@review.target_type = 'JamRuby::Teacher'
|
||||
@review.save
|
||||
end
|
||||
end
|
||||
|
||||
task populate_jam_track_genres: :environment do
|
||||
genres = Genre.all
|
||||
genres = genres.sample(genres.count * 0.75)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,120 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe ApiReviewsController do
|
||||
render_views
|
||||
before(:all) do
|
||||
@logged_in_user = FactoryGirl.create(:user)
|
||||
end
|
||||
|
||||
before(:each) do
|
||||
Review.destroy_all
|
||||
ReviewSummary.destroy_all
|
||||
@user= FactoryGirl.create(:user)
|
||||
@target= FactoryGirl.create(:jam_track)
|
||||
controller.current_user = @logged_in_user
|
||||
end
|
||||
|
||||
after(:all) do
|
||||
Review.destroy_all
|
||||
ReviewSummary.destroy_all
|
||||
User.destroy_all
|
||||
JamTrack.destroy_all
|
||||
end
|
||||
|
||||
describe "create" do
|
||||
it "successful" do
|
||||
post :create, rating:3, description:"it was ok", target_id: @target.id, target_type:"JamRuby::JamTrack", format: 'json'
|
||||
response.should be_success
|
||||
Review.index.should have(1).items
|
||||
end
|
||||
end
|
||||
|
||||
describe "update" do
|
||||
before :each do
|
||||
@review=Review.create!(target:@target, rating:5, description: "blah", user_id: @logged_in_user.id)
|
||||
end
|
||||
|
||||
it "basic" do
|
||||
post :update, id:@review.id, mods: {rating:4, description: "not blah"}, :format=>'json'
|
||||
response.should be_success
|
||||
@review.reload
|
||||
@review.rating.should eq(4)
|
||||
@review.description.should eq("not blah")
|
||||
end
|
||||
|
||||
it "bad identifier" do
|
||||
post :update, id:2112, mods: {rating:4, description: "not blah"}, :format=>'json'
|
||||
response.status.should eql(404)
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete" do
|
||||
before :each do
|
||||
@review=Review.create!(target:@target, rating:5, description: "blah", user_id: @logged_in_user.id)
|
||||
end
|
||||
|
||||
it "marks review as deleted" do
|
||||
delete :delete, id:@review.id
|
||||
response.should be_success
|
||||
Review.index.should have(0).items
|
||||
Review.index(include_deleted:true).should have(1).items
|
||||
end
|
||||
end
|
||||
|
||||
describe "indexes" do
|
||||
before :each do
|
||||
@target2=FactoryGirl.create(:jam_track)
|
||||
|
||||
7.times { Review.create!(target:@target, rating:4, description: "blah", user_id: FactoryGirl.create(:user).id) }
|
||||
5.times { Review.create!(target:@target2, rating:4, description: "blah", user_id: FactoryGirl.create(:user).id) }
|
||||
end
|
||||
|
||||
it "review summaries" do
|
||||
get :index, format: 'json'
|
||||
response.should be_success
|
||||
json = JSON.parse(response.body)
|
||||
json.should have(0).items
|
||||
|
||||
ReviewSummary.index.should have(0).items
|
||||
Review.reduce()
|
||||
ReviewSummary.index.should have(2).items
|
||||
get :index, format: 'json'
|
||||
json = JSON.parse(response.body)
|
||||
json.should have(2).item
|
||||
end
|
||||
|
||||
it "details" do
|
||||
ReviewSummary.index.should have(0).items
|
||||
Review.reduce()
|
||||
ReviewSummary.index.should have(2).items
|
||||
|
||||
summaries = ReviewSummary.index
|
||||
get :details, :review_summary_id=>summaries[0].id, format: 'json'
|
||||
response.should be_success
|
||||
json = JSON.parse(response.body)
|
||||
json.should have(7).items
|
||||
|
||||
get :details, :review_summary_id=>summaries[1].id, format: 'json'
|
||||
response.should be_success
|
||||
json = JSON.parse(response.body)
|
||||
json.should have(5).items
|
||||
end
|
||||
|
||||
it "paginates details" do
|
||||
ReviewSummary.index.should have(0).items
|
||||
Review.reduce()
|
||||
summaries = ReviewSummary.index
|
||||
summaries.should have(2).items
|
||||
|
||||
get :details, review_summary_id:summaries[0].id, page: 1, per_page: 3, format: 'json'
|
||||
response.should be_success
|
||||
json = JSON.parse(response.body)
|
||||
json.should have(3).items
|
||||
|
||||
get :details, review_summary_id:summaries[0].id, page: 3, per_page: 3, format: 'json'
|
||||
response.should be_success
|
||||
json = JSON.parse(response.body)
|
||||
json.should have(1).items
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,252 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe ApiTeachersController do
|
||||
render_views
|
||||
BIO = "Once a man learned a guitar."
|
||||
|
||||
let(:user) { FactoryGirl.create(:user) }
|
||||
let(:genre1) { FactoryGirl.create(:genre, :description => "g1") }
|
||||
let(:genre2) { FactoryGirl.create(:genre, :description => "g2") }
|
||||
let(:subject1) { FactoryGirl.create(:subject, :description => "s1") }
|
||||
let(:subject2) { FactoryGirl.create(:subject, :description => "s2") }
|
||||
let(:language1) { FactoryGirl.create(:language, :description => "l1") }
|
||||
let(:language2) { FactoryGirl.create(:language, :description => "l2") }
|
||||
let(:instrument1) { FactoryGirl.create(:instrument, :description => 'a great instrument')}
|
||||
let(:instrument2) { FactoryGirl.create(:instrument, :description => 'an ok instrument')}
|
||||
|
||||
before(:each) do
|
||||
controller.current_user = user
|
||||
end
|
||||
|
||||
after(:all) do
|
||||
User.destroy_all
|
||||
Teacher.destroy_all
|
||||
end
|
||||
|
||||
describe "creates" do
|
||||
it "simple" do
|
||||
post :create, biography: BIO, format: 'json'
|
||||
response.should be_success
|
||||
t = Teacher.find_by_user_id(user)
|
||||
t.should_not be_nil
|
||||
|
||||
t.biography.should == BIO
|
||||
end
|
||||
|
||||
|
||||
it "with instruments" do
|
||||
post :create, biography: BIO, instruments: [instrument1, instrument2], format: 'json'
|
||||
response.should be_success
|
||||
t = Teacher.find_by_user_id(user)
|
||||
t.biography.should == BIO
|
||||
t.instruments.should have(2).items
|
||||
end
|
||||
|
||||
it "with child records" do
|
||||
params = {
|
||||
subjects: [subject1, subject2],
|
||||
genres: [genre1, genre2],
|
||||
languages: [language1, language2],
|
||||
teaches_age_lower: 10,
|
||||
teaches_age_upper: 20,
|
||||
teaches_beginner: true,
|
||||
teaches_intermediate: false,
|
||||
teaches_advanced: true,
|
||||
format: 'json'
|
||||
}
|
||||
|
||||
post :create, params
|
||||
|
||||
response.should be_success
|
||||
t = Teacher.find_by_user_id(user)
|
||||
|
||||
# Genres
|
||||
t.genres.should have(2).items
|
||||
|
||||
# Subjects
|
||||
t.subjects.should have(2).items
|
||||
|
||||
# Languages
|
||||
t.languages.should have(2).items
|
||||
|
||||
t.teaches_age_lower.should == 10
|
||||
t.teaches_age_upper.should == 20
|
||||
t.teaches_beginner.should be_true
|
||||
t.teaches_intermediate.should be_false
|
||||
t.teaches_advanced.should be_true
|
||||
end
|
||||
end
|
||||
|
||||
describe "gets details" do
|
||||
before :each do
|
||||
@teacher = Teacher.save_teacher(
|
||||
user,
|
||||
years_teaching: 21,
|
||||
biography: BIO,
|
||||
genres: [genre1, genre2],
|
||||
instruments: [instrument1, instrument2],
|
||||
languages: [language1, language2],
|
||||
subjects: [subject1, subject2]
|
||||
)
|
||||
|
||||
@teacher.should_not be_nil
|
||||
@teacher.id.should_not be_nil
|
||||
end
|
||||
|
||||
it "by teacher id" do
|
||||
get :detail, {:format => 'json', teacher_id: @teacher.id}
|
||||
response.should be_success
|
||||
json = JSON.parse(response.body)
|
||||
json["biography"].should==BIO
|
||||
end
|
||||
|
||||
it "by current user" do
|
||||
get :detail, {:format => 'json'}
|
||||
response.should be_success
|
||||
|
||||
json = JSON.parse(response.body)
|
||||
json["biography"].should==BIO
|
||||
end
|
||||
|
||||
it "no teacher" do
|
||||
user2 = FactoryGirl.create(:user)
|
||||
controller.current_user = user2
|
||||
get :detail, {:format => 'json'}
|
||||
response.status.should eq(404)
|
||||
end
|
||||
|
||||
|
||||
it "and retrieves sub-records" do
|
||||
get :detail, {:format => 'json', teacher_id: @teacher.id}
|
||||
response.should be_success
|
||||
json = JSON.parse(response.body)
|
||||
json["genres"].should have(2).items
|
||||
json["instruments"].should have(2).items
|
||||
json["subjects"].should have(2).items
|
||||
json["languages"].should have(2).items
|
||||
|
||||
json["genres"].first.should eq(genre1.id)
|
||||
json["instruments"].last.should eq(instrument2.id)
|
||||
json["subjects"].first.should eq(subject1.id)
|
||||
json["languages"].last.should eq(language2.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe "to existing" do
|
||||
before :each do
|
||||
@teacher = Teacher.save_teacher(
|
||||
user,
|
||||
years_teaching: 21,
|
||||
biography: BIO
|
||||
)
|
||||
@teacher.should_not be_nil
|
||||
@teacher.id.should_not be_nil
|
||||
end
|
||||
|
||||
it "deletes" do
|
||||
delete :delete, {:format => 'json', id: @teacher.id}
|
||||
response.should be_success
|
||||
t = Teacher.find_by_user_id(user)
|
||||
t.should be_nil
|
||||
end
|
||||
|
||||
it "with child records" do
|
||||
params = {
|
||||
id: @teacher.id,
|
||||
subjects: [subject1, subject2],
|
||||
genres: [genre1, genre2],
|
||||
languages: [language1, language2],
|
||||
teaches_age_lower: 10,
|
||||
teaches_age_upper: 20,
|
||||
teaches_beginner: true,
|
||||
teaches_intermediate: false,
|
||||
teaches_advanced: true,
|
||||
format: 'json'
|
||||
}
|
||||
|
||||
post :update, params
|
||||
|
||||
response.should be_success
|
||||
t = Teacher.find_by_user_id(user)
|
||||
|
||||
# Genres
|
||||
t.genres.should have(2).items
|
||||
|
||||
# Subjects
|
||||
t.subjects.should have(2).items
|
||||
|
||||
# Languages
|
||||
t.languages.should have(2).items
|
||||
|
||||
t.teaches_age_lower.should == 10
|
||||
t.teaches_age_upper.should == 20
|
||||
t.teaches_beginner.should be_true
|
||||
t.teaches_intermediate.should be_false
|
||||
t.teaches_advanced.should be_true
|
||||
end
|
||||
end
|
||||
|
||||
describe "validates" do
|
||||
it "introduction" do
|
||||
post :create, validate_introduction: true, format: 'json'
|
||||
response.should_not be_success
|
||||
response.status.should eq(422)
|
||||
json = JSON.parse(response.body)
|
||||
json['errors'].should have_key('biography')
|
||||
|
||||
end
|
||||
|
||||
it "basics" do
|
||||
post :create, validate_basics: true, format: 'json'
|
||||
response.should_not be_success
|
||||
response.status.should eq(422)
|
||||
json = JSON.parse(response.body)
|
||||
json['errors'].should have_key('instruments')
|
||||
json['errors'].should have_key('subjects')
|
||||
json['errors'].should have_key('genres')
|
||||
json['errors'].should have_key('languages')
|
||||
|
||||
post :create, instruments: [instrument1, instrument2], validate_basics: true, format: 'json'
|
||||
response.should_not be_success
|
||||
response.status.should eq(422)
|
||||
json = JSON.parse(response.body)
|
||||
json['errors'].should_not have_key('subjects')
|
||||
json['errors'].should_not have_key('instruments')
|
||||
json['errors'].should have_key('genres')
|
||||
json['errors'].should have_key('languages')
|
||||
end
|
||||
|
||||
it "pricing" do
|
||||
params = {
|
||||
prices_per_lesson: false,
|
||||
prices_per_month: false,
|
||||
lesson_duration_30: false,
|
||||
lesson_duration_45: false,
|
||||
lesson_duration_60: false,
|
||||
lesson_duration_90: false,
|
||||
lesson_duration_120: false,
|
||||
price_per_lesson_45_cents: 3000,
|
||||
price_per_lesson_120_cents: 3000,
|
||||
price_per_month_30_cents: 5000,
|
||||
validate_pricing: true,
|
||||
format: 'json'
|
||||
}
|
||||
post :create, params
|
||||
response.should_not be_success
|
||||
response.status.should eq(422)
|
||||
json = JSON.parse(response.body)
|
||||
json['errors'].should have_key('offer_pricing')
|
||||
json['errors'].should have_key('offer_duration')
|
||||
|
||||
# Add lesson duration and resubmit. We should only get one error now:
|
||||
params[:lesson_duration_45] = true
|
||||
post :create, params
|
||||
response.should_not be_success
|
||||
response.status.should eq(422)
|
||||
json = JSON.parse(response.body)
|
||||
json['errors'].should have_key('offer_pricing')
|
||||
json['errors'].should_not have_key('offer_duration')
|
||||
#puts "JSON.pretty_generate(json): #{JSON.pretty_generate(json)}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -259,6 +259,18 @@ FactoryGirl.define do
|
|||
factory :genre, :class => JamRuby::Genre do
|
||||
description { |n| "Genre #{n}" }
|
||||
end
|
||||
|
||||
factory :language, :class => JamRuby::Language do
|
||||
id { |n| "Language #{n}" }
|
||||
description { |n| "Language #{n}" }
|
||||
end
|
||||
|
||||
factory :subject, :class => JamRuby::Subject do
|
||||
id { |n| "Subject #{n}" }
|
||||
description { |n| "Subject #{n}" }
|
||||
end
|
||||
|
||||
|
||||
|
||||
factory :instrument, :class => JamRuby::Instrument do
|
||||
description { |n| "Instrument #{n}" }
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 484 B |
|
|
@ -0,0 +1,171 @@
|
|||
/*
|
||||
* Copyright (c) 2011 Raphael Schweikert, http://sabberworm.com/
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
(function() {
|
||||
var dataKey = 'jstarbox-data';
|
||||
var eventNamespace = '.jstarbox';
|
||||
var defaultOptions = {
|
||||
average: 0.5,
|
||||
stars: 5,
|
||||
buttons: 5, //false will allow any value between 0 and 1 to be set
|
||||
ghosting: false,
|
||||
changeable: true, // true, false, or "once"
|
||||
autoUpdateAverage: false
|
||||
};
|
||||
var methods = {
|
||||
destroy: function() {
|
||||
this.removeData(dataKey);
|
||||
this.unbind(eventNamespace).find('*').unbind(eventNamespace);
|
||||
this.removeClass('starbox');
|
||||
this.empty();
|
||||
},
|
||||
|
||||
getValue: function() {
|
||||
var data = this.data(dataKey);
|
||||
return data.opts.currentValue;
|
||||
},
|
||||
|
||||
setValue: function(val) {
|
||||
var data = this.data(dataKey);
|
||||
var size = arguments[1] || data.positioner.width();
|
||||
var include_ghost = arguments[2];
|
||||
if(include_ghost) {
|
||||
data.ghost.css({width: ""+(val*size)+"px"});
|
||||
}
|
||||
data.colorbar.css({width: ""+(val*size)+"px"});
|
||||
data.opts.currentValue = val;
|
||||
},
|
||||
|
||||
getOption: function(option) {
|
||||
var data = this.data(dataKey);
|
||||
return data.opts[option];
|
||||
},
|
||||
|
||||
setOption: function(option, value) {
|
||||
var data = this.data(dataKey);
|
||||
|
||||
if(option === 'changeable' && value === false) {
|
||||
data.positioner.triggerHandler('mouseleave');
|
||||
}
|
||||
|
||||
data.opts[option] = value;
|
||||
|
||||
if(option === 'stars') {
|
||||
data.methods.update_stars();
|
||||
} else if(option === 'average') {
|
||||
this.starbox('setValue', value, null, true);
|
||||
}
|
||||
},
|
||||
|
||||
markAsRated: function() {
|
||||
var data = this.data(dataKey);
|
||||
data.positioner.addClass('rated');
|
||||
}
|
||||
};
|
||||
jQuery.fn.extend({
|
||||
starbox: function(options) {
|
||||
if(options.constructor === String && methods[options]) {
|
||||
return methods[options].apply(this, Array.prototype.slice.call(arguments, 1)) || this;
|
||||
}
|
||||
options = jQuery.extend({}, defaultOptions, options);
|
||||
this.each(function(count) {
|
||||
var element = jQuery(this);
|
||||
|
||||
var opts = jQuery.extend({}, options);
|
||||
var data = {
|
||||
opts: opts,
|
||||
methods: {}
|
||||
};
|
||||
element.data(dataKey, data);
|
||||
|
||||
var positioner = data.positioner = jQuery('<div/>').addClass('positioner');
|
||||
|
||||
var stars = data.stars = jQuery('<div/>').addClass('stars').appendTo(positioner);
|
||||
var ghost = data.ghost = jQuery('<div/>').addClass('ghost').hide().appendTo(stars);
|
||||
var colorbar = data.colorbar = jQuery('<div/>').addClass('colorbar').appendTo(stars);
|
||||
var star_holder = data.star_holder = jQuery('<div/>').addClass('star_holder').appendTo(stars);
|
||||
|
||||
element.empty().addClass('starbox').append(positioner);
|
||||
data.methods.update_stars = function() {
|
||||
star_holder.empty();
|
||||
for(var i=0;i<opts.stars;i++) {
|
||||
var star = jQuery('<div/>').addClass('star').addClass('star-'+i).appendTo(star_holder);
|
||||
}
|
||||
// (Re-)Set initial value
|
||||
methods.setOption.call(element, 'average', opts.average);
|
||||
};
|
||||
data.methods.update_stars();
|
||||
|
||||
positioner.bind('mousemove'+eventNamespace, function(event) {
|
||||
if(!opts.changeable) return;
|
||||
if(opts.ghosting) {
|
||||
ghost.show();
|
||||
}
|
||||
var size = positioner.width();
|
||||
var x = event.layerX;
|
||||
if(x === undefined) {
|
||||
x = (event.pageX-positioner.offset().left);
|
||||
}
|
||||
var val = x/size;
|
||||
if(opts.buttons) {
|
||||
val *= opts.buttons;
|
||||
val = Math.floor(val);
|
||||
val += 1;
|
||||
val /= opts.buttons;
|
||||
}
|
||||
positioner.addClass('hover');
|
||||
methods.setValue.call(element, val, size);
|
||||
element.starbox('setValue', val, size);
|
||||
element.triggerHandler('starbox-value-moved', val);
|
||||
});
|
||||
|
||||
positioner.bind('mouseleave'+eventNamespace, function(event) {
|
||||
if(!opts.changeable) return;
|
||||
ghost.hide();
|
||||
positioner.removeClass('hover');
|
||||
methods.setValue.call(element, opts.average);
|
||||
});
|
||||
|
||||
positioner.bind('click'+eventNamespace, function(event) {
|
||||
if(!opts.changeable) return;
|
||||
|
||||
if(opts.autoUpdateAverage) {
|
||||
methods.markAsRated.call(element);
|
||||
methods.setOption.call(element, 'average', opts.currentValue);
|
||||
}
|
||||
|
||||
var new_average = element.triggerHandler('starbox-value-changed', opts.currentValue);
|
||||
if(!isNaN(parseFloat(new_average)) && isFinite(new_average)) {
|
||||
methods.setOption.call(element, 'average', new_average);
|
||||
}
|
||||
|
||||
if(opts.changeable === 'once') {
|
||||
methods.setOption.call(element, 'changeable', false);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
return this;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
.positioner {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.starbox .colorbar,
|
||||
.starbox .ghost {
|
||||
z-index: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.starbox .stars {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.starbox .stars .star_holder {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.starbox .stars .star_holder .star {
|
||||
display: inline-block;
|
||||
vertical-align: baseline;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
|
||||
/* Override with your own image and size… */
|
||||
.starbox .stars .star_holder .star {
|
||||
background-image: url('/assets/jstarbox-5-large.png');
|
||||
background-size:cover;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
/* Override with your own colours… */
|
||||
.starbox .stars { background: #ccc; }
|
||||
.starbox .rated .stars { background: #dcdcdc; }
|
||||
.starbox .rated.hover .stars { background: transparent; }
|
||||
.starbox .colorbar { background: #ed3618; }
|
||||
.starbox .hover .colorbar { background: #ffcc1c; }
|
||||
.starbox .rated .colorbar { background: #64b2ff; }
|
||||
.starbox .rated.hover .colorbar { background: #1e90ff; }
|
||||
.starbox .ghost { background: #a1a1a1; }
|
||||
Loading…
Reference in New Issue