* add rating dialogs for teacher/students and have them pop at end of lesson, be accessible from teacher rating profile pgae, and also from link in email

This commit is contained in:
Seth Call 2016-05-17 13:31:53 -05:00
parent 92b2e13ee8
commit 141736ad2f
17 changed files with 1229 additions and 963 deletions

View File

@ -17,9 +17,42 @@ module JamRuby
validates :target, presence: true validates :target, presence: true
validates :user_id, 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."} validates :target_id, uniqueness: {scope: :user_id, message: "There is already a review for this User and Target."}
validate :requires_lesson
after_save :reduce after_save :reduce
def requires_lesson
if target_type == 'JamRuby::User'
# you are rating a student
lesson = LessonSession.joins(:music_session).where('music_sessions.user_id = ?', target.id).where(teacher_id: user.id).first
if lesson.nil?
errors.add(:target, "You must have at least scheduled or been in a lesson with this student")
end
elsif target_type == "JamRuby::Teacher"
# you are rating a teacher
lesson = LessonSession.joins(:music_session).where('music_sessions.user_id = ?', user.id).where(teacher_id: target.user.id).first
if lesson.nil?
errors.add(:target, "You must have at least scheduled or been in a lesson with this teacher")
end
end
end
def self.create_or_update(params)
review = Review.where(user_id: params[:user].id).where(target_id: params[:target].id).where(target_type: params[:target].class.to_s).first
if review
review.description = params[:description]
review.rating = params[:rating]
review.save
else
review = Review.create(params)
end
review
end
def self.create(params) def self.create(params)
review = Review.new review = Review.new
review.target = params[:target] review.target = params[:target]
@ -81,7 +114,7 @@ module JamRuby
def reduce def reduce
ReviewSummary.transaction do ReviewSummary.transaction do
ReviewSummary.where(target_type: target_type, target_id: target_id).destroy_all ReviewSummary.where(target_type: target_type, target_id: target_id).delete_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") 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("deleted_at IS NULL")
@ -89,15 +122,22 @@ module JamRuby
.group("target_type, target_id") .group("target_type, target_id")
.each do |r| .each do |r|
wilson_score = Review.ci_lower_bound(r.pos_count, r.review_count) wilson_score = Review.ci_lower_bound(r.pos_count, r.review_count)
ReviewSummary.create!(
summary = ReviewSummary.create(
target_id: r.target_id, target_id: r.target_id,
target_type: r.target_type, target_type: r.target_type,
avg_rating: r.avg_rating, avg_rating: r.avg_rating,
wilson_score: wilson_score, wilson_score: wilson_score,
review_count: r.review_count review_count: r.review_count
) )
if summary.errors.any?
puts "review summary unable to be created #{summary.errors.inspect}"
raise "review summary unable to be created #{summary.errors.inspect}"
end
end end
end end
return true
end end
end end
end end

View File

@ -5,7 +5,7 @@ module JamRuby
validates :avg_rating, presence:true, numericality: true validates :avg_rating, presence:true, numericality: true
validates :review_count, presence:true, numericality: {only_integer: true} validates :review_count, presence:true, numericality: {only_integer: true}
validates :wilson_score, presence:true, numericality: {greater_than:0, less_than:1} validates :wilson_score, presence:true, numericality: {greater_than_or_equal_to:0, less_than_or_equal_to:1}
validates :target_id, presence:true, uniqueness:true validates :target_id, presence:true, uniqueness:true
class << self class << self

View File

@ -2113,11 +2113,11 @@ module JamRuby
end end
def has_rated_teacher(teacher) def has_rated_teacher(teacher)
teacher_rating(teacher).count > 0 teacher_rating(teacher)
end end
def has_rated_student(student) def has_rated_student(student)
student_rating(student).count > 0 Review.where(target_id: student.id).where(target_type: "JamRuby::User").count > 0
end end
def teacher_rating(teacher) def teacher_rating(teacher)
@ -2126,9 +2126,11 @@ module JamRuby
end end
Review.where(target_id: teacher.id).where(target_type: teacher.class.to_s) Review.where(target_id: teacher.id).where(target_type: teacher.class.to_s)
end end
def teacher_rating(teacher)
def student_rating(student) if teacher.is_a?(JamRuby::User)
Review.where(target_id: student.id).where(target_type: "JamRuby::User") teacher = teacher.teacher
end
Review.where(target_id: teacher.id).where(target_type: teacher.class.to_s)
end end
def has_rated_student(student) def has_rated_student(student)
@ -2140,7 +2142,7 @@ module JamRuby
end end
def ratings_url def ratings_url
"#{APP_CONFIG.external_root_url}/client?selected=ratings#/profile/teacher/#{id}" "#{APP_CONFIG.external_root_url}/client?tile=ratings#/profile/teacher/#{id}"
end end
def student_ratings_url def student_ratings_url

File diff suppressed because it is too large Load Diff

View File

@ -49,7 +49,6 @@ profileUtils = context.JK.ProfileUtils
@root = $(@getDOMNode()) @root = $(@getDOMNode())
@endOfList = @root.find('.end-of-payments-list') @endOfList = @root.find('.end-of-payments-list')
@contentBodyScroller = @root @contentBodyScroller = @root
@root = $(@getDOMNode())
@iCheckify() @iCheckify()
componentDidUpdate: (prevProps, prevState) -> componentDidUpdate: (prevProps, prevState) ->

View File

@ -2,10 +2,19 @@ context = window
@RateUserDialog = React.createClass({ @RateUserDialog = React.createClass({
mixins: [Reflux.listenTo(@AppStore, "onAppInit")] mixins: [ICheckMixin,
Reflux.listenTo(@AppStore, "onAppInit")]
teacher: false teacher: false
parseId:(id) -> getInitialState: () ->
{
id: null,
type: null,
student: null,
teacher: null,
rating: null,
}
parseId: (id) ->
if !id? if !id?
{id: null, type: null} {id: null, type: null}
else else
@ -17,17 +26,25 @@ context = window
beforeShow: (args) -> beforeShow: (args) ->
logger.debug("RateUserDialog.beforeShow", args.d1) logger.debug("RateUserDialog.beforeShow", args.d1)
@firstName = '' parsed = @parseId(args.d1)
@lastName = ''
@email = ''
@setState({target: null}) @setState({student: null, teacher: null, type: parsed.type, id: parsed.id, rating: null})
rest.getUserDetail({id: parsed.id}).done((response) => @userLookupDone(response)).fail((jqXHR) => @userLookupFail(jqXHR))
rest.getUserDetail({id: args.d1}).done((response) => @userLookupDone(response)).fail((jqXHR) => @userLookupFail(jqXHR))
afterHide: () -> afterHide: () ->
isRatingTeacher: () ->
!@isRatingStudent()
isRatingStudent: () ->
@state.type == 'student'
userLookupDone: (response) -> userLookupDone: (response) ->
@setState({target: response}) if @isRatingTeacher()
@setState({teacher: response})
else
@setState({student: response})
userLookupFail: (jqXHR) -> userLookupFail: (jqXHR) ->
@app.ajaxError(jqXHR, null, null) @app.ajaxError(jqXHR, null, null)
@ -38,50 +55,121 @@ context = window
'afterHide': @afterHide 'afterHide': @afterHide
}; };
@app.bindDialog('rate-user', dialogBindings); @app.bindDialog('rate-user-dialog', dialogBindings);
checkboxChanged: (e) ->
$target = $(e.target)
@setState({rating: $target.val()})
componentDidMount: () -> componentDidMount: () ->
@checkboxes = [{selector: 'input[name="rating"]', stateKey: 'rating'}]
@root = $(@getDOMNode()) @root = $(@getDOMNode())
getInitialState: () -> @iCheckify()
{inviteErrors: null}
componentDidUpdate: () ->
@iCheckify()
descriptionChanged: (e) ->
@setState({description: $(e.target).val()})
doCancel: (e) -> doCancel: (e) ->
e.preventDefault() e.preventDefault()
@app.layout.closeDialog('rate-user', true); @app.layout.cancelDialog('rate-user-dialog');
doRating: (e) -> doRating: (e) ->
e.preventDefault() e.preventDefault()
rest.createReview({id: target}) if @disabled()
return
createDone:(response) -> if @isRatingTeacher()
context.SchoolActions.addInvitation(@state.teacher, response) data =
context.JK.Banner.showNotice("invitation sent", "Your invitation has been sent!") {
@app.layout.closeDialog('invite-school-user') target_id: @state.id
target_type: 'JamRuby::Teacher'
}
else
data =
{
target_id: @state.id,
target_type: 'JamRuby::User',
}
createFail: (jqXHR) -> data.rating = @state.rating
data.description = @state.description
rest.createReview(data).done((response) => @createReviewDone(response)).fail((jqXHR) => @createReviewFail(jqXHR))
createReviewDone: (response) ->
if @isRatingTeacher()
context.JK.Banner.showNotice("teacher rated", "Thank you for taking the time to provide your feedback.")
else
context.JK.Banner.showNotice("student rated", "Thank you for taking the time to provide your feedback.")
@app.layout.closeDialog('rate-user-dialog')
createReviewFail: (jqXHR) ->
handled = false handled = false
if jqXHR.status == 422 if jqXHR.status == 422
errors = JSON.parse(jqXHR.responseText) response = JSON.parse(jqXHR.responseText)
@setState({inviteErrors: errors}) if response.errors.target?
handled = true @app.layout.notify({title: "not allowed", text: "you can not rate someone until you have had a lesson with them"})
handled = true
if !handled if !handled
@app.ajaxError(jqXHR, null, null) @app.ajaxError(jqXHR, null, null)
render: () -> disabled: () ->
!@state.rating? || (!@state.teacher? && !@state.student?)
if @state.user?.teacher? render: () ->
submitClasses = classNames({'button-orange': true, disabled: @disabled()})
if @isRatingTeacher()
title = 'Rate Teacher' title = 'Rate Teacher'
help = `<p>Please rate this teacher based on your experience with them.</p>` help = `<h2>Please rate this teacher based on your experience with them:</h2>`
descriptionPrompt = `<h2>Please help other students by explaining what you like or dont like about this teacher:</h2>`
choices =
`<div className="choices">
<div className="field">
<input type="radio" name="rating" value="5"/><label>Great teacher</label>
</div>
<div className="field">
<input type="radio" name="rating" value="4"/><label>Good teacher</label>
</div>
<div className="field">
<input type="radio" name="rating" value="3"/><label>Average teacher</label>
</div>
<div className="field">
<input type="radio" name="rating" value="2"/><label>Poor teacher</label>
</div>
<div className="field">
<input type="radio" name="rating" value="1"/><label>Terrible teacher</label>
</div>
</div>`
else else
title = 'Rate Student' title = 'Rate Student'
help = `<p>Please rate this student based on your experience with them.</p>` help = `<h2>Please rate this student based on your experience with them:</h2>`
descriptionPrompt = `<h2>Please help other teachers by explaining what you like or dont like about this student:</h2>`
choices =
`<div className="choices">
<div className="field">
<input type="radio" name="rating" value="5"/><label>Great student</label>
</div>
<div className="field">
<input type="radio" name="rating" value="4"/><label>Good student</label>
</div>
<div className="field">
<input type="radio" name="rating" value="3"/><label>Average student</label>
</div>
<div className="field">
<input type="radio" name="rating" value="2"/><label>Poor student</label>
</div>
<div className="field">
<input type="radio" name="rating" value="1"/><label>Terrible student</label>
</div>
</div>`
`<div> `<div>
<div className="content-head"> <div className="content-head">
@ -93,9 +181,17 @@ context = window
{help} {help}
{choices}
<div className="field description">
{descriptionPrompt}
<textarea name="description" placeholder="Enter a further bit of detail here" value={this.state.description}
onChange={this.descriptionChanged}></textarea>
</div>
<div className="actions"> <div className="actions">
<a onClick={this.doCancel} className="button-grey">CANCEL</a> <a onClick={this.doCancel} className="button-grey">CANCEL</a>
<a onClick={this.doRating} className="button-orange">SUBMIT RATING</a> <a onClick={this.doRating} className={submitClasses}>SUBMIT RATING</a>
</div> </div>
</div> </div>
</div>` </div>`

View File

@ -4,16 +4,9 @@ context = window
onLeave: (e) -> onLeave: (e) ->
e.preventDefault() e.preventDefault()
@rateSession()
SessionActions.leaveSession.trigger({location: '/client#/home'}) SessionActions.leaveSession.trigger({location: '/client#/home'})
rateSession: () ->
unless @rateSessionDialog?
@rateSessionDialog = new context.JK.RateSessionDialog(context.JK.app);
@rateSessionDialog.initialize();
@rateSessionDialog.showDialog();
render: () -> render: () ->
`<a className="session-leave button-grey right leave" onClick={this.onLeave}> `<a className="session-leave button-grey right leave" onClick={this.onLeave}>

View File

@ -1,6 +1,7 @@
context = window context = window
rest = context.JK.Rest() rest = context.JK.Rest()
logger = context.JK.logger logger = context.JK.logger
EVENTS = context.JK.EVENTS;
SubjectStore = context.SubjectStore SubjectStore = context.SubjectStore
InstrumentStore = context.InstrumentStore InstrumentStore = context.InstrumentStore
@ -117,8 +118,20 @@ proficiencyDescriptionMap = {
@visible = true @visible = true
logger.debug("TeacherProfile: afterShow") logger.debug("TeacherProfile: afterShow")
@setState({userId: e.id, user: null}) @setState({userId: e.id, user: null})
@updateProfileInfo(e.id)
if $.QueryString['tile']?
rewrite = true
@setState({selected: $.QueryString['tile']})
if rewrite
if window.history.replaceState #ie9 proofing
window.history.replaceState({}, "", "/client#/profile/teacher/#{e.id}")
updateProfileInfo: (id) ->
rest.getUserDetail({ rest.getUserDetail({
id: e.id, id: id,
show_teacher: true, show_teacher: true,
show_profile: true show_profile: true
}).done((response) => @userDetailDone(response)).fail(@app.ajaxError) }).done((response) => @userDetailDone(response)).fail(@app.ajaxError)
@ -549,6 +562,12 @@ proficiencyDescriptionMap = {
{this.musicSamples(user, teacher)} {this.musicSamples(user, teacher)}
</div>` </div>`
rateTeacher: (e) ->
@app.layout.showDialog('rate-user-dialog', {d1: "teacher_#{@state.user.id}"}).one(EVENTS.DIALOG_CLOSED, (e, data) =>
if !data.canceled
@updateProfileInfo(@state.userId)
)
ratings: () -> ratings: () ->
user = @state.user user = @state.user
teacher = user.teacher teacher = user.teacher
@ -581,7 +600,8 @@ proficiencyDescriptionMap = {
`<div className="ratings-block info-block"> `<div className="ratings-block info-block">
<h3>Ratings & Reviews</h3> <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> <h4>{user.first_name} Summary Rating: <div data-ratings={summary.avg_rating / 5} className="ratings-box hidden"/> <div className="review-count">({reviewCount})</div> <a onClick={this.rateTeacher} className="button-orange rate-teacher-btn">RATE TEACHER</a></h4>
{reviews} {reviews}
</div>` </div>`

View File

@ -21,11 +21,15 @@ teacherActions = window.JK.Actions.Teacher
$candidate = @root.find(selector) $candidate = @root.find(selector)
@iCheckIgnore = true @iCheckIgnore = true
if $candidate.attr('type') == 'radio' if $candidate.attr('type') == 'radio'
$found = @root.find(selector + '[value="' + choice + '"]') if choice?
$found.iCheck('check').attr('checked', true) $found = @root.find(selector + '[value="' + choice + '"]')
$found.iCheck('check').attr('checked', true)
else
$candidate.iCheck('uncheck').attr('checked', false)
else else
if choice if choice
$candidate.iCheck('check').attr('checked', true); $candidate.iCheck('check').attr('checked', true);
@ -54,5 +58,5 @@ teacherActions = window.JK.Actions.Teacher
if @checkboxChanged? if @checkboxChanged?
@checkboxChanged(e) @checkboxChanged(e)
else else
logger.warn("no checkbox changed implemented") logger.error("no checkbox changed defined")
} }

View File

@ -1072,6 +1072,19 @@ ConfigureTracksActions = @ConfigureTracksActions
@sessionUtils.SessionPageLeave() @sessionUtils.SessionPageLeave()
if @currentSession?.lesson_session?
if context.JK.currentUserId == @currentSession.lesson_session.teacher_id
@app.layout.showDialog('rate-user-dialog', {d1: 'student_' + @currentSession.lesson_session.student_id})
else
@app.layout.showDialog('rate-user-dialog', {d1: 'teacher_' + @currentSession.lesson_session.student_id})
else
unless @rateSessionDialog?
@rateSessionDialog = new context.JK.RateSessionDialog(context.JK.app);
@rateSessionDialog.initialize();
@rateSessionDialog.showDialog();
leaveSession: () -> leaveSession: () ->
if !@joinDeferred? || @joinDeferred?.state() == 'resolved' if !@joinDeferred? || @joinDeferred?.state() == 'resolved'

View File

@ -225,6 +225,12 @@
color:$ColorTextTypical; color:$ColorTextTypical;
} }
.rate-teacher-btn {
margin-left: 36px;
top: -2px;
position: relative;
}
.review { .review {
border-width:1px 0 0 0; border-width:1px 0 0 0;
border-color:$ColorTextTypical; border-color:$ColorTextTypical;

View File

@ -0,0 +1,52 @@
@import "client/common";
#rate-user-dialog {
width: 600px;
max-height:600px;
h2 {
color:white;
margin-bottom:10px;
font-size:16px;
}
.dialog-inner {
width: auto;
height:calc(100% - 29px)
}
.field {
margin-bottom:10px;
}
input {
display:inline-block;
}
label {
display:inline-block;
}
.iradio_minimal {
display:inline-block;
margin-right: 5px;
top: 4px;
}
textarea {
width:100%;
height:80px;
margin:0;
}
div[data-react-class="RateUserDialog"] {
}
.field.description {
margin-bottom:20px;
h2 {
margin-top: 20px;
}
}
.actions {
float:right;
margin:0 -13px 30px 0;
}
}

View File

@ -15,7 +15,21 @@ class ApiReviewsController < ApiController
# Create a review: # Create a review:
def create def create
@review = Review.create(params) target = User.find(params['target_id'])
if params[:target_type] == 'JamRuby::Teacher'
target = target.teacher
end
params[:target] = target
params[:user] = current_user
@review = Review.create_or_update(params)
puts "@review.errors #{@review.errors.inspect}"
if @review.errors.any?
respond_with_model(@review)
return
end
end end
# List reviews matching targets for given review summary: # List reviews matching targets for given review summary:

View File

@ -53,3 +53,4 @@
= render 'dialogs/chatDialog' = render 'dialogs/chatDialog'
= render 'dialogs/cancelLessonDialog' = render 'dialogs/cancelLessonDialog'
= render 'dialogs/rescheduleLessonDialog' = render 'dialogs/rescheduleLessonDialog'
= render 'dialogs/rateUserDialog'

View File

@ -0,0 +1,2 @@
.dialog.dialog-overlay-sm.top-parent layout='dialog' layout-id='rate-user-dialog' id='rate-user-dialog'
= react_component 'RateUserDialog', {}

View File

@ -107,4 +107,5 @@ SampleApp::Application.configure do
config.vst_enabled = true config.vst_enabled = true
config.verify_email_enabled = true config.verify_email_enabled = true
config.jamclass_enabled = true
end end

View File

@ -115,5 +115,6 @@ SampleApp::Application.configure do
:secret_key => 'sk_test_OkjoIF7FmdjunyNsdVqJD02D', :secret_key => 'sk_test_OkjoIF7FmdjunyNsdVqJD02D',
:source_customer => 'cus_88Vp44SLnBWMXq' :source_customer => 'cus_88Vp44SLnBWMXq'
} }
config.jamclass_enabled = true
end end