Skip to content

Commit

Permalink
Merge pull request #1072 from mumuki/feature-generalized-tips
Browse files Browse the repository at this point in the history
Feature generalized tips
  • Loading branch information
julian-berbel authored Jun 7, 2018
2 parents d494ec9 + 6bce5b3 commit adbfbbf
Show file tree
Hide file tree
Showing 34 changed files with 746 additions and 36 deletions.
8 changes: 8 additions & 0 deletions app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ def corollary_box(with_corollary)
end
end

def assistance_box(assignment)
if assignment.tips.present?
%Q{<div class="mu-tips-box">
#{Mumukit::Assistant::Narrator.random.compose_explanation_html assignment.tips}
</div>}.html_safe
end
end

def chapter_finished(chapter)
t :chapter_finished_html, chapter: link_to_path_element(chapter) if chapter
end
Expand Down
8 changes: 8 additions & 0 deletions app/models/assignment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,14 @@ def as_platform_json
}})
end

def tips
@tips ||= exercise.assist_with(self)
end

def increment_attemps!
self.attemps_count += 1 unless passed?
end

private

def update_submissions_count!
Expand Down
17 changes: 17 additions & 0 deletions app/models/concerns/assistable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module Assistable
extend ActiveSupport::Concern

included do
serialize :assistance_rules, Array
end

def assistant
Mumukit::Assistant.parse(assistance_rules)
end

def assist_with(assignment)
# not strictly necessary, but avoid going through
# all the assistence process when there are no rules
assistance_rules.blank? ? [] : assistant.assist_with(assignment)
end
end
6 changes: 1 addition & 5 deletions app/models/concerns/with_expectations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module WithExpectations
extend ActiveSupport::Concern

included do
serialize :expectations
serialize :expectations, Array
end

def expectations_yaml
Expand All @@ -16,8 +16,4 @@ def expectations_yaml=(yaml)
def expectations=(expectations)
self[:expectations] = expectations.map(&:stringify_keys)
end

def expectations
self[:expectations] || []
end
end
2 changes: 1 addition & 1 deletion app/models/concerns/with_status.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def passed?
end

def aborted?
status == :aborted
status.aborted?
end

def run_update!
Expand Down
4 changes: 2 additions & 2 deletions app/models/exercise.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ class Exercise < ApplicationRecord
include WithNumber,
WithAssignments,
FriendlyName,
WithLanguage
WithLanguage,
Assistable

include Submittable,
Questionable
Expand All @@ -13,7 +14,6 @@ class Exercise < ApplicationRecord
ParentNavigation

belongs_to :guide

defaults { self.submissions_count = 0 }

validates_presence_of :submissions_count,
Expand Down
4 changes: 3 additions & 1 deletion app/models/submission/submission.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ def save_submission!(assignment)
end

def save_results!(results, assignment)
assignment.update! results
assignment.assign_attributes results
assignment.increment_attemps!
assignment.save! results
end

def notify_results!(results, assignment)
Expand Down
8 changes: 5 additions & 3 deletions app/views/exercise_solutions/_results.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
<% if render_feedback?(assignment) %>
<div class="results-item">
<strong><%= t :feedback %>:</strong>

<div>
<%= assignment.feedback_html %>
</div>
Expand Down Expand Up @@ -64,6 +63,9 @@
<%= solution_download_link assignment %>
</div>

<%= corollary_box @exercise unless assignment.should_retry? %>
<% if assignment.should_retry? %>
<%= assistance_box assignment %>
<% else %>
<%= corollary_box @exercise %>
<% end %>
<%= render partial: 'exercise_solutions/results_button', locals: {assignment: assignment} %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddFailedSubmissionsCountToAssignments < ActiveRecord::Migration[5.1]
def change
add_column :assignments, :attemps_count, :integer, default: 0
end
end
5 changes: 5 additions & 0 deletions db/migrate/20180526141344_add_tips_rules_to_exercise.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddTipsRulesToExercise < ActiveRecord::Migration[5.1]
def change
add_column :exercises, :assistance_rules, :text
end
end
1 change: 1 addition & 0 deletions lib/mumuki/laboratory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ module Laboratory
end
end

require_relative '../mumukit/assistant'
require 'mumukit/inspection'
require 'mumukit/bridge'
require 'mumukit/content_type'
Expand Down
14 changes: 14 additions & 0 deletions lib/mumuki/laboratory/locales/narrator.en.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
en:
narrator:
retry_0: Let's try again!
retry_1: Keep on!
retry_2: Let's fix it!
introduction_0: "Oops, it didn't work :frowning:."
introduction_1: "Oops, it didn't work :frowning:."
introduction_2: "Oops, it didn't work :frowning:."
middle_0: Also, %{tip}.
middle_1: Also, %{tip}.
middle_2: Also, %{tip}.
ending_0: Finally, %{tip}.
ending_1: Finally, %{tip}.
ending_2: Finally, %{tip}.
14 changes: 14 additions & 0 deletions lib/mumuki/laboratory/locales/narrator.es.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
es:
narrator:
retry_0: ¡Intentemos de nuevo!
retry_1: 'Pero a no desesperar, intentemos otra vez :muscle:'
retry_2: ¡Corrijamos el problema!
introduction_0: 'Parece que algo no funcionó :see_no_evil:.'
introduction_1: 'Eh, ¿qué pasó acá :frowning:?'
introduction_2: 'Parece que algo no anduvo bien :sweat_smile:'
middle_0: "Además, %{tip}."
middle_1: "También %{tip}."
middle_2: "Por otro lado, %{tip}."
ending_0: "Por último, %{tip}."
ending_1: "Ah, algo más: %{tip}."
ending_2: "Y para cerrar, %{tip}."
14 changes: 14 additions & 0 deletions lib/mumuki/laboratory/locales/narrator.pt.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
pt:
narrator:
retry_0: Vamos tentar de novo!
retry_1: 'Mas não se desespere, vamos tentar novamente :muscle:'
retry_2: Corrigimos o problema!
introduction_0: 'Parece que algo não funcionou :see_no_evil:.'
introduction_1: 'Ei, o que aconteceu aqui :frowning:?'
introduction_2: 'Parece que algo não correu bem :sweat_smile:'
middle_0: "Além disso, %{tip}."
middle_1: "Também %{tip}."
middle_2: "Por outro lado, %{tip}."
ending_0: "Finalmente, %{tip}."
ending_1: "Oh, algo mais: %{tip}."
ending_2: "E para fechar, %{tip}."
4 changes: 3 additions & 1 deletion lib/mumuki/laboratory/status/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ def errored?
false
end

# False if and only if this status
# is `Mumuki::Laboratory::Status::Passed`
def should_retry?
group.should_retry?
true
end

def iconize
Expand Down
4 changes: 0 additions & 4 deletions lib/mumuki/laboratory/status/failed.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@ def self.failed?
true
end

def self.should_retry?
true
end

def self.iconize
{class: :danger, type: 'times-circle'}
end
Expand Down
4 changes: 0 additions & 4 deletions lib/mumuki/laboratory/status/passed_with_warnings.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@ def self.passed?
true
end

def self.should_retry?
true
end

def self.iconize
{class: :warning, type: 'exclamation-circle'}
end
Expand Down
4 changes: 0 additions & 4 deletions lib/mumuki/laboratory/status/unknown.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,4 @@ def self.to_i
def self.iconize
{class: :muted, type: :circle}
end

def self.should_retry?
false
end
end
30 changes: 30 additions & 0 deletions lib/mumukit/assistant.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
module Mumukit
# An assistant is used to generate dynamic feedback
# over a student's submission, based on rules.
#
# This feedback is composed of a list of markdown messages called _tips_,
# and the whole processes of creating this feedback is called _assistance_.
class Assistant
attr_accessor :rules

def initialize(rules)
@rules = rules
end

# Provides tips for the student for the given submission,
# based on the `rules`.
def assist_with(submission)
@rules
.select { |it| it.matches?(submission) }
.map { |it| it.message_for(submission.attemps_count) }
end

def self.parse(rules)
new rules.map { |it| Mumukit::Assistant::Rule.parse it }
end
end
end

require_relative './assistant/rule'
require_relative './assistant/message'
require_relative './assistant/narrator'
45 changes: 45 additions & 0 deletions lib/mumukit/assistant/message.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
module Mumukit::Assistant::Message
# Fixed messages are independent of the number
# of attemps
class Fixed
def initialize(text)
@text = text
end

def call(_)
@text
end
end

# Progressive messages depend on the number of attemps
# They work with exactly two or three messages:
#
# * the first message will be displayed in the first three attemps
# * the second message will be displayed in the fourth, fifth and sixth attemps
# * the third message will be displayed starting at the seventh attemp
#
class Progressive
def initialize(alternatives)
raise 'You need two or three alternatives' unless alternatives.size.between?(2, 3)
@alternatives = alternatives
end

def call(attemps_count)
case attemps_count
when (1..3) then @alternatives.first
when (4..6) then @alternatives.second
else @alternatives.last
end
end
end

def self.parse(text_or_alternatives)
if text_or_alternatives.is_a? String
Fixed.new text_or_alternatives
elsif text_or_alternatives.is_a? Array
Progressive.new text_or_alternatives
else
raise "Wrong message format #{text_or_alternatives}"
end
end
end
67 changes: 67 additions & 0 deletions lib/mumukit/assistant/narrator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# A narrator can turn tips - generated by `Mumukit::Assistant` -
# into a humanized text with the same information but
# a bit more friendly. This text is called _explanation_.
#
# The narrator uses some internationalized random phrases, and it does provide
# a seed as a construction argument to allow its testing.
class Mumukit::Assistant::Narrator
def initialize(seed)
@seed = seed
end

# Generated a markdown explanation using the seeded phrases. Uses `I18n` to get
# the appropriate locale.
def compose_explanation(tips)
"#{explanation_introduction_phrase}\n\n#{explanation_paragraphs(tips).join("\n\n")}\n\n#{retry_phrase}\n"
end

# Generates an html explantion.
# See `compose_explanation`
def compose_explanation_html(tips)
Mumukit::ContentType::Markdown.to_html compose_explanation(tips)
end

def retry_phrase
t :retry
end

def explanation_introduction_phrase
t :introduction
end

def explanation_paragraphs(tips)
tips.take(3).zip([:opening, :middle, :ending]).map do |tip, selector|
send "explanation_#{selector}_paragraph", tip
end
end

def explanation_opening_paragraph(tip)
"#{tip.capitalize}."
end

def explanation_middle_paragraph(tip)
t :middle, tip: tip
end

def explanation_ending_paragraph(tip)
t :ending, tip: tip
end

def self.random
new seed(*5.times.map { random_index })
end

def self.seed(r, i, o, m, e)
{ retry: r, introduction: i, opening: o, middle: m, ending: e }
end

private

def t(key, args={})
I18n.t "narrator.#{key}_#{@seed[key]}", args
end

def self.random_index
(0..2).to_a.sample
end
end
Loading

0 comments on commit adbfbbf

Please sign in to comment.