diff --git a/admin/app/controllers/email_controller.rb b/admin/app/controllers/email_controller.rb index 4c999fd30..45af1bb2e 100644 --- a/admin/app/controllers/email_controller.rb +++ b/admin/app/controllers/email_controller.rb @@ -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 \ No newline at end of file diff --git a/db/manifest b/db/manifest index d44b89a14..086b3281f 100755 --- a/db/manifest +++ b/db/manifest @@ -321,4 +321,8 @@ jam_track_sessions.sql jam_track_sessions_v2.sql email_screening.sql bounced_email_cleanup.sql -news.sql \ No newline at end of file +news.sql +profile_teacher.sql +populate_languages.sql +populate_subjects.sql +reviews.sql \ No newline at end of file diff --git a/db/up/populate_languages.sql b/db/up/populate_languages.sql new file mode 100644 index 000000000..885995b69 --- /dev/null +++ b/db/up/populate_languages.sql @@ -0,0 +1,72 @@ +insert into languages(description, id) values ('English','EN'); +insert into languages(description, id) values ('Afrikanns','AF'); +insert into languages(description, id) values ('Albanian','SQ'); +insert into languages(description, id) values ('Arabic','AR'); +insert into languages(description, id) values ('Armenian','HY'); +insert into languages(description, id) values ('Basque','EU'); +insert into languages(description, id) values ('Bengali','BN'); +insert into languages(description, id) values ('Bulgarian','BG'); +insert into languages(description, id) values ('Catalan','CA'); +insert into languages(description, id) values ('Cambodian','KM'); +insert into languages(description, id) values ('Chinese (Mandarin)','ZH'); +insert into languages(description, id) values ('Croation','HR'); +insert into languages(description, id) values ('Czech','CS'); +insert into languages(description, id) values ('Danish','DA'); +insert into languages(description, id) values ('Dutch','NL'); +insert into languages(description, id) values ('Estonian','ET'); +insert into languages(description, id) values ('Fiji','FJ'); +insert into languages(description, id) values ('Finnish','FI'); +insert into languages(description, id) values ('French','FR'); +insert into languages(description, id) values ('Georgian','KA'); +insert into languages(description, id) values ('German','DE'); +insert into languages(description, id) values ('Greek','EL'); +insert into languages(description, id) values ('Gujarati','GU'); +insert into languages(description, id) values ('Hebrew','HE'); +insert into languages(description, id) values ('Hindi','HI'); +insert into languages(description, id) values ('Hungarian','HU'); +insert into languages(description, id) values ('Icelandic','IS'); +insert into languages(description, id) values ('Indonesian','ID'); +insert into languages(description, id) values ('Irish','GA'); +insert into languages(description, id) values ('Italian','IT'); +insert into languages(description, id) values ('Japanese','JA'); +insert into languages(description, id) values ('Javanese','JW'); +insert into languages(description, id) values ('Korean','KO'); +insert into languages(description, id) values ('Latin','LA'); +insert into languages(description, id) values ('Latvian','LV'); +insert into languages(description, id) values ('Lithuanian','LT'); +insert into languages(description, id) values ('Macedonian','MK'); +insert into languages(description, id) values ('Malay','MS'); +insert into languages(description, id) values ('Malayalam','ML'); +insert into languages(description, id) values ('Maltese','MT'); +insert into languages(description, id) values ('Maori','MI'); +insert into languages(description, id) values ('Marathi','MR'); +insert into languages(description, id) values ('Mongolian','MN'); +insert into languages(description, id) values ('Nepali','NE'); +insert into languages(description, id) values ('Norwegian','NO'); +insert into languages(description, id) values ('Persian','FA'); +insert into languages(description, id) values ('Polish','PL'); +insert into languages(description, id) values ('Portuguese','PT'); +insert into languages(description, id) values ('Punjabi','PA'); +insert into languages(description, id) values ('Quechua','QU'); +insert into languages(description, id) values ('Romanian','RO'); +insert into languages(description, id) values ('Russian','RU'); +insert into languages(description, id) values ('Samoan','SM'); +insert into languages(description, id) values ('Serbian','SR'); +insert into languages(description, id) values ('Slovak','SK'); +insert into languages(description, id) values ('Slovenian','SL'); +insert into languages(description, id) values ('Spanish','ES'); +insert into languages(description, id) values ('Swahili','SW'); +insert into languages(description, id) values ('Swedish ','SV'); +insert into languages(description, id) values ('Tamil','TA'); +insert into languages(description, id) values ('Tatar','TT'); +insert into languages(description, id) values ('Telugu','TE'); +insert into languages(description, id) values ('Thai','TH'); +insert into languages(description, id) values ('Tibetan','BO'); +insert into languages(description, id) values ('Tonga','TO'); +insert into languages(description, id) values ('Turkish','TR'); +insert into languages(description, id) values ('Ukranian','UK'); +insert into languages(description, id) values ('Urdu','UR'); +insert into languages(description, id) values ('Uzbek','UZ'); +insert into languages(description, id) values ('Vietnamese','VI'); +insert into languages(description, id) values ('Welsh','CY'); +insert into languages(description, id) values ('Xhosa','XH'); diff --git a/db/up/populate_subjects.sql b/db/up/populate_subjects.sql new file mode 100644 index 000000000..fa433387a --- /dev/null +++ b/db/up/populate_subjects.sql @@ -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'); diff --git a/db/up/profile_teacher.sql b/db/up/profile_teacher.sql new file mode 100644 index 000000000..e4d0bd0b1 --- /dev/null +++ b/db/up/profile_teacher.sql @@ -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 +); + diff --git a/db/up/reviews.sql b/db/up/reviews.sql new file mode 100644 index 000000000..89a30ee32 --- /dev/null +++ b/db/up/reviews.sql @@ -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 +); \ No newline at end of file diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index c9eb23ed4..d655cff1e 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -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" diff --git a/ruby/lib/jam_ruby/models/genre.rb b/ruby/lib/jam_ruby/models/genre.rb index 1c847b249..3f81b8bbf 100644 --- a/ruby/lib/jam_ruby/models/genre.rb +++ b/ruby/lib/jam_ruby/models/genre.rb @@ -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 diff --git a/ruby/lib/jam_ruby/models/instrument.rb b/ruby/lib/jam_ruby/models/instrument.rb index d1b2d74c2..3f11a01e5 100644 --- a/ruby/lib/jam_ruby/models/instrument.rb +++ b/ruby/lib/jam_ruby/models/instrument.rb @@ -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 diff --git a/ruby/lib/jam_ruby/models/language.rb b/ruby/lib/jam_ruby/models/language.rb new file mode 100644 index 000000000..bb5d64316 --- /dev/null +++ b/ruby/lib/jam_ruby/models/language.rb @@ -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 diff --git a/ruby/lib/jam_ruby/models/review.rb b/ruby/lib/jam_ruby/models/review.rb new file mode 100644 index 000000000..7c98619d9 --- /dev/null +++ b/ruby/lib/jam_ruby/models/review.rb @@ -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 \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/review_summary.rb b/ruby/lib/jam_ruby/models/review_summary.rb new file mode 100644 index 000000000..fb3833f5d --- /dev/null +++ b/ruby/lib/jam_ruby/models/review_summary.rb @@ -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 \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/subject.rb b/ruby/lib/jam_ruby/models/subject.rb new file mode 100644 index 000000000..f20d75626 --- /dev/null +++ b/ruby/lib/jam_ruby/models/subject.rb @@ -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 diff --git a/ruby/lib/jam_ruby/models/teacher.rb b/ruby/lib/jam_ruby/models/teacher.rb new file mode 100644 index 000000000..8306c60ad --- /dev/null +++ b/ruby/lib/jam_ruby/models/teacher.rb @@ -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 diff --git a/ruby/lib/jam_ruby/models/teacher_experience.rb b/ruby/lib/jam_ruby/models/teacher_experience.rb new file mode 100644 index 000000000..1b311886a --- /dev/null +++ b/ruby/lib/jam_ruby/models/teacher_experience.rb @@ -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 diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index 6113ac615..0f14c26fd 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -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" diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb index 024ecdc20..27435b204 100644 --- a/ruby/spec/factories.rb +++ b/ruby/spec/factories.rb @@ -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 diff --git a/ruby/spec/jam_ruby/models/review_spec.rb b/ruby/spec/jam_ruby/models/review_spec.rb new file mode 100644 index 000000000..7a5851948 --- /dev/null +++ b/ruby/spec/jam_ruby/models/review_spec.rb @@ -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 \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/teacher_spec.rb b/ruby/spec/jam_ruby/models/teacher_spec.rb new file mode 100644 index 000000000..061f7fe3a --- /dev/null +++ b/ruby/spec/jam_ruby/models/teacher_spec.rb @@ -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 diff --git a/web/README.md b/web/README.md index 975118c93..730c0f2f5 100644 --- a/web/README.md +++ b/web/README.md @@ -1,5 +1,5 @@ + Jasmine Javascript Unit Tests ============================= -Open browser to localhost:3000/teaspoon - +Open browser to localhost:3000/teaspoon \ No newline at end of file diff --git a/web/app/assets/javascripts/accounts.js b/web/app/assets/javascripts/accounts.js index ac5d3fa18..aa14917cc 100644 --- a/web/app/assets/javascripts/accounts.js +++ b/web/app/assets/javascripts/accounts.js @@ -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() { diff --git a/web/app/assets/javascripts/accounts_profile.js b/web/app/assets/javascripts/accounts_profile.js index 9938d4cf0..c00b03e60 100644 --- a/web/app/assets/javascripts/accounts_profile.js +++ b/web/app/assets/javascripts/accounts_profile.js @@ -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); \ No newline at end of file +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/accounts_profile_experience.js b/web/app/assets/javascripts/accounts_profile_experience.js index de57e8695..d3d62ac60 100644 --- a/web/app/assets/javascripts/accounts_profile_experience.js +++ b/web/app/assets/javascripts/accounts_profile_experience.js @@ -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) { diff --git a/web/app/assets/javascripts/accounts_profile_interests.js b/web/app/assets/javascripts/accounts_profile_interests.js index 48674efd6..562b142dd 100644 --- a/web/app/assets/javascripts/accounts_profile_interests.js +++ b/web/app/assets/javascripts/accounts_profile_interests.js @@ -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) { diff --git a/web/app/assets/javascripts/accounts_profile_samples.js b/web/app/assets/javascripts/accounts_profile_samples.js index 0962d513b..60174c06d 100644 --- a/web/app/assets/javascripts/accounts_profile_samples.js +++ b/web/app/assets/javascripts/accounts_profile_samples.js @@ -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("Invalid username").show(); } + function websiteSuccessCallback($inputDiv) { + $inputDiv.addClass('error'); + $inputDiv.find('.error-text').remove(); + $inputDiv.append("Invalid URL").show(); + } + + function soundCloudSuccessCallback($inputDiv) { siteSuccessCallback($inputDiv, soundCloudRecordingValidator, $soundCloudSampleList, 'soundcloud'); } diff --git a/web/app/assets/javascripts/application.js b/web/app/assets/javascripts/application.js index 2153fb9c1..4b5b54a1d 100644 --- a/web/app/assets/javascripts/application.js +++ b/web/app/assets/javascripts/application.js @@ -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 diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index 10461d496..219df4f2c 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -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; diff --git a/web/app/assets/javascripts/profile.js b/web/app/assets/javascripts/profile.js index d99bb421f..7b64fadeb 100644 --- a/web/app/assets/javascripts/profile.js +++ b/web/app/assets/javascripts/profile.js @@ -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) { diff --git a/web/app/assets/javascripts/profile_utils.js b/web/app/assets/javascripts/profile_utils.js index 8629a738c..424c777dd 100644 --- a/web/app/assets/javascripts/profile_utils.js +++ b/web/app/assets/javascripts/profile_utils.js @@ -19,6 +19,8 @@ var NOT_SPECIFIED_TEXT = 'Not specified'; + profileUtils.NOT_SPECIFIED_TEXT = NOT_SPECIFIED_TEXT + var proficiencyDescriptionMap = { "1": "BEGINNER", "2": "INTERMEDIATE", diff --git a/web/app/assets/javascripts/react-components.js b/web/app/assets/javascripts/react-components.js index ee980b48e..d02622469 100644 --- a/web/app/assets/javascripts/react-components.js +++ b/web/app/assets/javascripts/react-components.js @@ -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 diff --git a/web/app/assets/javascripts/react-components/AgeRangeList.js.jsx.coffee b/web/app/assets/javascripts/react-components/AgeRangeList.js.jsx.coffee new file mode 100644 index 000000000..615a13514 --- /dev/null +++ b/web/app/assets/javascripts/react-components/AgeRangeList.js.jsx.coffee @@ -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(``) + + 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: () -> + `` +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/CheckBoxList.js.jsx.coffee b/web/app/assets/javascripts/react-components/CheckBoxList.js.jsx.coffee new file mode 100644 index 000000000..e514796c6 --- /dev/null +++ b/web/app/assets/javascripts/react-components/CheckBoxList.js.jsx.coffee @@ -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 `
` + + `
+
+ {object_options} +
+
` + + isChecked: (id) -> + this.state.selectedObjects? && id in this.state.selectedObjects + + + getInitialState: () -> + {selectedObjects:@props.selectedObjects} + + componentWillReceiveProps: (nextProps) -> + @setState({selectedObjects: nextProps.selectedObjects}) + +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/EditableList.js.jsx.coffee b/web/app/assets/javascripts/react-components/EditableList.js.jsx.coffee new file mode 100644 index 000000000..6b9b00b71 --- /dev/null +++ b/web/app/assets/javascripts/react-components/EditableList.js.jsx.coffee @@ -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 `
+
{displayValue}
+
+ X +
+
` + else + object_options.push `
None
` + + `
+
+ {object_options} +
+
` + +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/GenreCheckBoxList.js.jsx.coffee b/web/app/assets/javascripts/react-components/GenreCheckBoxList.js.jsx.coffee new file mode 100644 index 000000000..4e877aca9 --- /dev/null +++ b/web/app/assets/javascripts/react-components/GenreCheckBoxList.js.jsx.coffee @@ -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: () -> + `
+ +
` +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/InstrumentCheckBoxList.js.jsx.coffee b/web/app/assets/javascripts/react-components/InstrumentCheckBoxList.js.jsx.coffee new file mode 100644 index 000000000..9078d5b4d --- /dev/null +++ b/web/app/assets/javascripts/react-components/InstrumentCheckBoxList.js.jsx.coffee @@ -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: () -> + `
+ +
` +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/LanguageCheckBoxList.js.jsx.coffee b/web/app/assets/javascripts/react-components/LanguageCheckBoxList.js.jsx.coffee new file mode 100644 index 000000000..59fa3140f --- /dev/null +++ b/web/app/assets/javascripts/react-components/LanguageCheckBoxList.js.jsx.coffee @@ -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: () -> + `
+ +
` +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SubjectCheckBoxList.js.jsx.coffee b/web/app/assets/javascripts/react-components/SubjectCheckBoxList.js.jsx.coffee new file mode 100644 index 000000000..71e6d8406 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SubjectCheckBoxList.js.jsx.coffee @@ -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: () -> + `
+ +
` +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/TeacherExperienceEditableList.js.jsx.coffee b/web/app/assets/javascripts/react-components/TeacherExperienceEditableList.js.jsx.coffee new file mode 100644 index 000000000..3ac17dd47 --- /dev/null +++ b/web/app/assets/javascripts/react-components/TeacherExperienceEditableList.js.jsx.coffee @@ -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 ` + ` + 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) + + `
+
+
+
+ + +
+
+ + +
+
+ + + + {endDate} + +
+
+ +
+ +
+ {errors} +
+
` +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/TeacherProfile.js.jsx.coffee b/web/app/assets/javascripts/react-components/TeacherProfile.js.jsx.coffee new file mode 100644 index 000000000..d96ccd858 --- /dev/null +++ b/web/app/assets/javascripts/react-components/TeacherProfile.js.jsx.coffee @@ -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 + `{text}` + + editMusicProfileLink: (text, selected) -> + if @state.isSelf + `{text}` + + + teacherBio: (user, teacher) -> + if teacher.biography? + biography = teacher.biography + else + biography = 'No bio defined yet' + + biography = biography.replace(/\n/g, "
") + + `
+

Teacher Profile {this.editProfileLink('edit profile', 'introduction')}

+
+
+
+
` + + 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 `
+

Intro Video

+ +
+
+
+