This commit is contained in:
Seth Call 2016-01-15 12:04:06 -06:00
commit 37417708b3
97 changed files with 40979 additions and 82 deletions

View File

@ -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

View File

@ -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

View File

@ -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');

View File

@ -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');

75
db/up/profile_teacher.sql Normal file
View File

@ -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
);

23
db/up/reviews.sql Normal file
View File

@ -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
);

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1,5 +1,5 @@
Jasmine Javascript Unit Tests
=============================
Open browser to localhost:3000/teaspoon
Open browser to localhost:3000/teaspoon

View File

@ -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() {

View File

@ -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);

View File

@ -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) {

View File

@ -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) {

View File

@ -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');
}

View File

@ -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

View File

@ -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;

View File

@ -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) {

View File

@ -19,6 +19,8 @@
var NOT_SPECIFIED_TEXT = 'Not specified';
profileUtils.NOT_SPECIFIED_TEXT = NOT_SPECIFIED_TEXT
var proficiencyDescriptionMap = {
"1": "BEGINNER",
"2": "INTERMEDIATE",

View File

@ -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

View File

@ -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>`
})

View File

@ -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})
})

View File

@ -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>`
})

View File

@ -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>`
})

View File

@ -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>`
})

View File

@ -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>`
})

View File

@ -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>`
})

View File

@ -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>`
})

View File

@ -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>`
})

View File

@ -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>
`
})

View File

@ -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>`
})

View File

@ -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>`
})

View File

@ -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>`
})

View File

@ -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>`
})

View File

@ -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">&nbsp;</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>`
})

View File

@ -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})
})

View File

@ -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>`
})

View File

@ -0,0 +1,5 @@
context = window
@GenreActions = Reflux.createActions({
})

View File

@ -0,0 +1,5 @@
context = window
@InstrumentActions = Reflux.createActions({
})

View File

@ -0,0 +1,5 @@
context = window
@LanguageActions = Reflux.createActions({
})

View File

@ -0,0 +1,12 @@
context = window
@ProfileActions = Reflux.createActions({
startTeacherEdit: {}
cancelTeacherEdit: {}
doneTeacherEdit: {}
startProfileEdit: {}
cancelProfileEdit: {}
doneProfileEdit: {}
editProfileNext: {}
})

View File

@ -0,0 +1,5 @@
context = window
@SubjectActions = Reflux.createActions({
})

View File

@ -0,0 +1,8 @@
context = window
@TeacherActions = Reflux.createActions({
load: {},
change: {}
})
context.JK.Actions.Teacher = TeacherActions

View File

@ -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({})
}

View File

@ -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}"
}

View File

@ -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]
}
)

View File

@ -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]
}
)

View File

@ -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]
}
)

View File

@ -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
}
)

View File

@ -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]
}
)

View File

@ -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)
)
}
)

View File

@ -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]

View File

@ -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);

View File

@ -492,9 +492,6 @@
}
}
label {
font-size: 1.05em;
}
}
#bands-screen {

View File

@ -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
*/

View File

@ -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;

View File

@ -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 {

View File

@ -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;
}
}
}
}

View File

@ -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 {
}
}
}
}

View File

@ -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;
}
}

View File

@ -77,6 +77,10 @@ body.web.individual_jamtrack {
}
}
.edit-link {
font-size:12px;
}
.video-container {
width: 420px;
padding-bottom: 53.33%;

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 -->

View File

@ -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

View File

@ -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', {}

View File

@ -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', {}

View File

@ -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', {}

View File

@ -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', {}

View File

@ -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'

View File

@ -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', {}

View File

@ -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

View File

@ -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

35911
web/dev_failures.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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;
}
});
})();

View File

@ -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; }