Skip to content
This repository

Student task status and study plan /wip #23

Open
wants to merge 46 commits into from

3 participants

Chris Geihsler Jordan Byron Vinicius Horewicz
Chris Geihsler
Collaborator

Hi everyone,

@wicz and I are still working on this, but we'd like some preliminary feedback.

We've got tasks, their statuses, and a link to the study plan being displayed the student page. The course page provides a link to the student page.

In the next day or two, we're going to finish up marking tasks as complete from the UI, and showing/editing study plans.

Feedback is welcome. Thanks!

and others added some commits March 21, 2012
Vinicius Horewicz Merge branch 'master' of github.com:mendicant-university/liskov
* 'master' of github.com:mendicant-university/liskov:
  Enable task removal
  Add database cleaners and move specs into their own support file
  Simplify discussion link generation
  highlight category
  add Discussion.search method and refactor DiscussionController
  Add a discussion list decorator
  horribly rough toggling of open/closed status
  hide archived discussions from main view
f077011
Chris Geihsler just started with student page e1b2dcd
Chris Geihsler forgot to add some stuff 6ade83a
Vinicius Horewicz Add link to students page 48ce60c
Vinicius Horewicz Resolve conflicts c4620b5
Chris Geihsler fixed test broken by new task factories c956ccb
Chris Geihsler DRY'd up how PersonDecorator are created and added filter chain in st…
…udents controller
4c036a1
Chris Geihsler naive passing student page test fcdbeb8
Vinicius Horewicz Merge branch 'master' of github.com:wicz/liskov
* 'master' of github.com:wicz/liskov:
  naive passing student page test
  DRY'd up how PersonDecorator are created and added filter chain in students controller
  fixed test broken by new task factories
852bbdb
Vinicius Horewicz Add distinct links for students and instructors ee5689c
Chris Geihsler refactored student page integration spec to use data attribs 2a45108
Chris Geihsler showing course task statuses b0a8abc
Chris Geihsler added constant to represent task is not completed 059448f
Chris Geihsler added failing spec for instructors seeing a task completion link 9cb0545
Chris Geihsler using correct assertion in student page test f388442
Chris Geihsler showing link to update a task only for instructors 5f2eaf9
Chris Geihsler much better use of decorators f22370d
Chris Geihsler link text should not show for instructors 7d70cdc
Chris Geihsler little bit of cleanup before being able to complete course tasks f2fec3a
Chris Geihsler added model logic to marks tasks as complete 116709b
Chris Geihsler ensure completing the same task twice does not create a new completed…
… task record
491784d
Vinicius Horewicz DRY find_course to ApplicationCtrlr cf3eb13
Vinicius Horewicz Add StudyPlan model 3bd977e
Vinicius Horewicz Adds single method to read from Clubhouse. Checks for Liskov permissions e129ef6
Vinicius Horewicz Always raise 404 when resource not found 08865ca
Vinicius Horewicz helper_method only what needed in views cf315ca
Vinicius Horewicz DRY find_student for student-dependent ctrlrs bc0188a
Vinicius Horewicz Refactoring and updating tests 183c29d
Vinicius Horewicz Add study plan /wip 48e52e5
Chris Geihsler merged from wicz/study-plans b534eac
Chris Geihsler merging with mendicant-university/liskov master c5ac30a
app/controllers/application_controller.rb
@@ -34,10 +29,27 @@ def login_path
34 29
     Rails.env.production? ? '/auth/github' : '/auth/developer'
35 30
   end
36 31
 
37  
-  private
  32
+  protected
3
Jordan Byron Owner

Why the change from private to protected?

Chris Geihsler Collaborator
seejee added a note March 26, 2012

fixed.

Vinicius Horewicz Collaborator
wicz added a note March 27, 2012

ops, looks the former java programmer inside me is still alive. :wink:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Jordan Byron
Owner

@geihsler & @wicz we need to talk about these tests. I'm really glad you are both writing tests, but a bunch of these are going to be thrown out once we restructure and finalize the views. That means I'll have failing tests anytime I make a minor change which isn't good.

What I think I'll do is after we merge this in, I'll create another pull request removing the unnecessary tests. Then in that pull request we can discuss why I removed them.

app/decorators/task_decorator.rb
... ...
@@ -0,0 +1,41 @@
  1
+class TaskDecorator < ApplicationDecorator
3
Jordan Byron Owner

This decorator is a bit confusing, because I'd expect the TaskDecorator to decorate plain old Task objects, but in this case it doesn't. Instead it wraps a Task object and StudentDecorator object, which in the class is called a participant. This is what I'd call a StudentTask and isn't really a decorator. I think creating a proper StudentTask class is the right way to go here. That class should be able to build all the tasks for a student, much like StudentDecorator#tasks does. It should also handle returning the status of the task and contain the code from CourseMembership that has to do with tasks. How does that sound? It should help consolidate all the student task code which is spread across 2 or 3 classes.

Chris Geihsler Collaborator
seejee added a note March 26, 2012

@jordanbyron good feedback.

The participant name comes from the fact that we were toying with renaming CourseMembership to Participant but decided not to. I'll rename that.

The StudentTask idea is an interesting one that, as you say, might consolidate the code from 2 or 3 classes. I see this class as a non-persisted model. Do you agree? Also, I think we'd then need a StudentTaskDecorator because #status and #complete_task_link are presentation logic.

I'll play with this in our repo and ask you to review a pull request.

Chris Geihsler Collaborator
seejee added a note March 26, 2012

@jordanbyron Please take a look at this pull request where I made some of the changes that you recommended. Overall, I like it. What do you think?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Chris Geihsler
Collaborator

@jordanbyron I'd like to talk about the tests as well. They've been very useful when developing these features, especially the integration tests. I'm curious to see which ones you think add to much friction to change. If a test is limiting change, than I'd like to find a way to make it a little more tolerant to change instead of deleting it. That being said, I'm not at all opposed to deleting tests that provide little or no value.

app/models/student_task.rb
((28 lines not shown))
  28
+
  29
+  def valid?
  30
+    @course_membership.valid?
  31
+  end
  32
+
  33
+  def complete?
  34
+    status != StudentTask::NOT_COMPLETE
  35
+  end
  36
+
  37
+  def status
  38
+    completed = get_completed_task
  39
+    completed ? completed.description : NOT_COMPLETE
  40
+  end
  41
+
  42
+  def complete(status)
  43
+    #TODO: is there a better way to do an upsert? - cg
1
Jordan Byron Owner

I think you're looking for:

course_membership.completed_tasks.find_or_create_by_task_id(:task_id => task_id, :description => status)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
app/models/student_task.rb
((40 lines not shown))
  40
+  end
  41
+
  42
+  def complete(status)
  43
+    #TODO: is there a better way to do an upsert? - cg
  44
+    completed = get_completed_task || completed_tasks.build(task_id: task_id)
  45
+    completed.description = status
  46
+    completed.save
  47
+  end
  48
+
  49
+  private
  50
+
  51
+  def completed_tasks
  52
+    @course_membership.completed_tasks
  53
+  end
  54
+
  55
+  def get_completed_task
1
Jordan Byron Owner

Let's rename this to just completed_task. The get part is implied and reminds me of java / C# :flushed:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
app/views/student_tasks/edit.html.haml
... ...
@@ -0,0 +1,7 @@
  1
+%p
  2
+  =@student_task.description
  3
+
  4
+=form_tag @student_task.update_url, method: :put do
  5
+  =label :student_task_, :status
  6
+  =text_field :student_task, :status 
  7
+  =submit_tag "Submit"
1
Jordan Byron Owner

This is a minor nit-pick, but add a space between the = operator and your ruby code. For example:

= form_tag do
  = lable_tag "Blah"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
app/decorators/student_decorator.rb
... ...
@@ -0,0 +1,25 @@
  1
+class StudentDecorator < ApplicationDecorator
  2
+  decorates :course_membership
  3
+
  4
+  def name
  5
+    course_membership.person.name
  6
+  end
  7
+
  8
+  def tasks
  9
+    course_membership.student_tasks.map {|st| StudentTaskDecorator.new(st)}
  10
+  end
  11
+
  12
+  def study_plan
  13
+    course_membership.study_plan || course_membership.create_study_plan
  14
+  end
3
Jordan Byron Owner

This is another place where find_or_create_by can be used

Chris Geihsler Collaborator
seejee added a note March 28, 2012

study_plan is a :has_one on CourseMembership. I looked through the API docs, and I couldn't find any way to find_or_create on a :has_one or would it be something like StudyPlan.find_or_create_by_course_membership(course_membership)?

Chris Geihsler Collaborator
seejee added a note March 28, 2012

Nevermind. I got it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Jordan Byron
Owner

This is coming along great guys. I really like the new StudentTask class. I made a few very small notes and once those are taken care of I think this is ready to merge. If either of you have any more questions just let me know. Great work! :rocket:

Chris Geihsler
Collaborator

Thanks for the review @jordanbyron! I believe I've addressed all of the concerns you had.

StudentTask really cleaned things up. Great suggestion!

Vinicius Horewicz
Collaborator
wicz commented March 28, 2012

Hey guys, I'd like to publicly thank @geihsler for all this work on this. You rock mate! :clap:

I'd like also to apologize for my absence. The last couple of days was like hell in work and with personal problems, so I couldn't help with anything. I hope tonight I'll be able to catch up with you and work on the remaining tasks.

added some commits March 28, 2012
Vinicius Horewicz Merge branch 'master' of github.com:wicz/liskov
* 'master' of github.com:wicz/liskov: (32 commits)
  using find_or_create for StudyPlan
  one more haml style fix
  using find_or_create in StudentTask now
  renamed get_completed_task to completed_task
  fixing spaces after = in haml
  renamed CompletedTasksController to StudentTasksController
  marking tasks as complete works
  moved NOT_COMPLETE const to StudentTask and renamed spec
  moved student task logic into StudentTask model
  changing protected to private
  Add study plan /wip
  Refactoring and updating tests
  DRY find_student for student-dependent ctrlrs
  helper_method only what needed in views
  Always raise 404 when resource not found
  Adds single method to read from Clubhouse. Checks for Liskov permissions
  Add PersonDecorator.from_github_name shortcut
  add simple gravatar functionality
  Remove superfluous performance test
  Add StudyPlan model
  ...
7f2136b
Vinicius Horewicz Parse study plan Markdown de433f9
Vinicius Horewicz Update study plan 420c242
Vinicius Horewicz Add md_preview dependency
Using my repo while the original can't work with rails 3.2
d1b7173
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 46 unique commits by 2 authors.

Mar 21, 2012
Vinicius Horewicz Merge branch 'master' of github.com:mendicant-university/liskov
* 'master' of github.com:mendicant-university/liskov:
  Enable task removal
  Add database cleaners and move specs into their own support file
  Simplify discussion link generation
  highlight category
  add Discussion.search method and refactor DiscussionController
  Add a discussion list decorator
  horribly rough toggling of open/closed status
  hide archived discussions from main view
f077011
Mar 24, 2012
Chris Geihsler just started with student page e1b2dcd
Chris Geihsler forgot to add some stuff 6ade83a
Vinicius Horewicz Add link to students page 48ce60c
Vinicius Horewicz Resolve conflicts c4620b5
Chris Geihsler fixed test broken by new task factories c956ccb
Chris Geihsler DRY'd up how PersonDecorator are created and added filter chain in st…
…udents controller
4c036a1
Chris Geihsler naive passing student page test fcdbeb8
Vinicius Horewicz Merge branch 'master' of github.com:wicz/liskov
* 'master' of github.com:wicz/liskov:
  naive passing student page test
  DRY'd up how PersonDecorator are created and added filter chain in students controller
  fixed test broken by new task factories
852bbdb
Vinicius Horewicz Add distinct links for students and instructors ee5689c
Mar 25, 2012
Chris Geihsler refactored student page integration spec to use data attribs 2a45108
Chris Geihsler showing course task statuses b0a8abc
Chris Geihsler added constant to represent task is not completed 059448f
Chris Geihsler added failing spec for instructors seeing a task completion link 9cb0545
Chris Geihsler using correct assertion in student page test f388442
Chris Geihsler showing link to update a task only for instructors 5f2eaf9
Chris Geihsler much better use of decorators f22370d
Chris Geihsler link text should not show for instructors 7d70cdc
Chris Geihsler little bit of cleanup before being able to complete course tasks f2fec3a
Chris Geihsler added model logic to marks tasks as complete 116709b
Chris Geihsler ensure completing the same task twice does not create a new completed…
… task record
491784d
Vinicius Horewicz DRY find_course to ApplicationCtrlr cf3eb13
Vinicius Horewicz Add StudyPlan model 3bd977e
Vinicius Horewicz Adds single method to read from Clubhouse. Checks for Liskov permissions e129ef6
Vinicius Horewicz Always raise 404 when resource not found 08865ca
Vinicius Horewicz helper_method only what needed in views cf315ca
Vinicius Horewicz DRY find_student for student-dependent ctrlrs bc0188a
Vinicius Horewicz Refactoring and updating tests 183c29d
Mar 26, 2012
Vinicius Horewicz Add study plan /wip 48e52e5
Chris Geihsler merged from wicz/study-plans b534eac
Chris Geihsler merging with mendicant-university/liskov master c5ac30a
Mar 27, 2012
Chris Geihsler changing protected to private 1faa27e
Chris Geihsler moved student task logic into StudentTask model bfc385c
Chris Geihsler moved NOT_COMPLETE const to StudentTask and renamed spec 8266369
Chris Geihsler Merge pull request #6 from wicz/student_task_decorator
Student task decorator
29885d2
Chris Geihsler marking tasks as complete works 90548ff
Chris Geihsler renamed CompletedTasksController to StudentTasksController 8a9fd61
Mar 28, 2012
Chris Geihsler fixing spaces after = in haml 13f9616
Chris Geihsler renamed get_completed_task to completed_task 2d2be24
Chris Geihsler using find_or_create in StudentTask now 18542e2
Chris Geihsler one more haml style fix 8cf0088
Chris Geihsler using find_or_create for StudyPlan 8680d21
Vinicius Horewicz Merge branch 'master' of github.com:wicz/liskov
* 'master' of github.com:wicz/liskov: (32 commits)
  using find_or_create for StudyPlan
  one more haml style fix
  using find_or_create in StudentTask now
  renamed get_completed_task to completed_task
  fixing spaces after = in haml
  renamed CompletedTasksController to StudentTasksController
  marking tasks as complete works
  moved NOT_COMPLETE const to StudentTask and renamed spec
  moved student task logic into StudentTask model
  changing protected to private
  Add study plan /wip
  Refactoring and updating tests
  DRY find_student for student-dependent ctrlrs
  helper_method only what needed in views
  Always raise 404 when resource not found
  Adds single method to read from Clubhouse. Checks for Liskov permissions
  Add PersonDecorator.from_github_name shortcut
  add simple gravatar functionality
  Remove superfluous performance test
  Add StudyPlan model
  ...
7f2136b
Apr 01, 2012
Vinicius Horewicz Parse study plan Markdown de433f9
Vinicius Horewicz Update study plan 420c242
Vinicius Horewicz Add md_preview dependency
Using my repo while the original can't work with rails 3.2
d1b7173
This page is out of date. Refresh to see the latest.

Showing 43 changed files with 667 additions and 77 deletions. Show diff stats Hide diff stats

  1. 2  Gemfile
  2. 10  Gemfile.lock
  3. 30  app/controllers/application_controller.rb
  4. 2  app/controllers/courses_controller.rb
  5. 14  app/controllers/sessions_controller.rb
  6. 27  app/controllers/student_tasks_controller.rb
  7. 6  app/controllers/students_controller.rb
  8. 18  app/controllers/study_plans_controller.rb
  9. 6  app/controllers/tasks_controller.rb
  10. 39  app/decorators/person_decorator.rb
  11. 25  app/decorators/student_decorator.rb
  12. 32  app/decorators/student_task_decorator.rb
  13. 7  app/decorators/study_plan_decorator.rb
  14. 6  app/models/completed_task.rb
  15. 8  app/models/course.rb
  16. 19  app/models/course_membership.rb
  17. 53  app/models/student_task.rb
  18. 2  app/models/study_plan.rb
  19. 11  app/views/course_memberships/_participants.html.haml
  20. 7  app/views/student_tasks/edit.html.haml
  21. 14  app/views/students/show.html.haml
  22. 3  app/views/study_plans/edit.html.haml
  23. 4  app/views/study_plans/show.html.haml
  24. 6  config/routes.rb
  25. 9  db/migrate/20120325183301_create_completed_tasks.rb
  26. 12  db/migrate/20120325210755_create_study_plans.rb
  27. 13  db/schema.rb
  28. 18  test/factories/course_memberships.rb
  29. 5  test/factories/courses.rb
  30. 22  test/factories/people.rb
  31. 11  test/factories/tasks.rb
  32. 15  test/functional/courses_controller_test.rb
  33. 29  test/functional/study_plans_controller_test.rb
  34. 6  test/functional/tasks_controller_test.rb
  35. 20  test/integration/participants_test.rb
  36. 91  test/integration/student_page_test.rb
  37. 34  test/integration/study_plans_test.rb
  38. 4  test/integration/tasks_test.rb
  39. 21  test/support/spec.rb
  40. 2  test/test_helper.rb
  41. 11  test/unit/course_membership_test.rb
  42. 53  test/unit/student_task_test.rb
  43. 17  test/unit/study_plan_test.rb
2  Gemfile
@@ -14,6 +14,8 @@ gem 'draper', :git => "git://github.com/jcasimir/draper.git"
14 14
 gem 'rainbow'
15 15
 gem 'highline', :require => false
16 16
 
  17
+gem "md_preview", git: "git://github.com/wicz/md_preview.git"
  18
+
17 19
 group :assets do
18 20
   gem 'sass-rails',   '~> 3.2.3'
19 21
   gem 'coffee-rails', '~> 3.2.1'
10  Gemfile.lock
@@ -5,6 +5,14 @@ GIT
5 5
     draper (0.9.5)
6 6
       activesupport (>= 2.3.10)
7 7
 
  8
+GIT
  9
+  remote: git://github.com/wicz/md_preview.git
  10
+  revision: f101da46f772686622eb0cdd08af504acc5bb8d6
  11
+  specs:
  12
+    md_preview (0.0.3)
  13
+      rails (>= 3.0.0)
  14
+      redcarpet (>= 2.0.0)
  15
+
8 16
 GEM
9 17
   remote: https://rubygems.org/
10 18
   specs:
@@ -143,6 +151,7 @@ GEM
143 151
     rake (0.9.2.2)
144 152
     rdoc (3.12)
145 153
       json (~> 1.4)
  154
+    redcarpet (2.1.1)
146 155
     ruby-progressbar (0.0.10)
147 156
     rubyzip (0.9.6.1)
148 157
     sass (3.1.15)
@@ -190,6 +199,7 @@ DEPENDENCIES
190 199
   haml
191 200
   highline
192 201
   jquery-rails
  202
+  md_preview!
193 203
   minitest (~> 2.11.2)
194 204
   omniauth-github
195 205
   pg
30  app/controllers/application_controller.rb
@@ -3,15 +3,10 @@ class ApplicationController < ActionController::Base
3 3
 
4 4
   before_filter :person_required
5 5
 
6  
-  helper_method :current_person, :signed_in?, :login_path
  6
+  helper_method :current_person
7 7
 
8 8
   def current_person
9  
-    begin
10  
-      @current_person ||= clubhouse_person(session[:person_github_nickname])
11  
-    rescue Clubhouse::Client::PersonNotFound
12  
-
13  
-    end
14  
-    @current_person
  9
+    @current_person ||= clubhouse_person(session[:person_github_nickname])
15 10
   end
16 11
 
17 12
   def signed_in?
@@ -36,8 +31,25 @@ def login_path
36 31
 
37 32
   private
38 33
 
39  
-  def clubhouse_person(github_nickname)
40  
-    PersonDecorator.from_github_name(github_nickname)
  34
+  def not_found
  35
+    raise ActionController::RoutingError.new('Not Found')
  36
+  end
  37
+
  38
+  def find_course
  39
+    course_id = params[:course_id] || params[:id]
  40
+    @course = Course.find(course_id)
  41
+  rescue ActiveRecord::RecordNotFound
  42
+    not_found
41 43
   end
42 44
 
  45
+  def find_student
  46
+    @course ||= find_course
  47
+    person = clubhouse_person(params[:student_id] || params[:id])
  48
+    membership = @course.membership_for(person) || not_found
  49
+    @student = StudentDecorator.new(membership)
  50
+  end
  51
+
  52
+  def clubhouse_person(github_nickname)
  53
+    PersonDecorator.from_github(github_nickname)
  54
+  end
43 55
 end
2  app/controllers/courses_controller.rb
... ...
@@ -1,7 +1,7 @@
1 1
 class CoursesController < ApplicationController
2 2
   def show
3 3
     @course         = CourseDecorator.find(params[:id])
4  
-    @members        = @course.course_memberships
  4
+    @participants   = @course.participants
5 5
     @tasks          = @course.tasks
6 6
   end
7 7
 end
14  app/controllers/sessions_controller.rb
@@ -2,15 +2,13 @@ class SessionsController < ApplicationController
2 2
   skip_before_filter :person_required
3 3
 
4 4
   def create
5  
-    begin
6  
-      person = clubhouse_person(auth_hash['info']['nickname'])
7  
-    rescue Clubhouse::Client::PersonNotFound
8  
-      flash[:error] = "Sorry, but we couldn't find your record in Clubhouse"
9  
-    end
10  
-
11  
-    # TODO check person's permissions for Liskov
  5
+    person = clubhouse_person(auth_hash['info']['nickname'])
12 6
 
13  
-    self.current_person = person
  7
+    if person && person.can_access_liskov?
  8
+      self.current_person = person
  9
+    else
  10
+      flash[:error] = "Sorry, but you need a Clubhouse ID and access to Liskov."
  11
+    end
14 12
 
15 13
     redirect_to request.env['omniauth.origin'] || root_path
16 14
   end
27  app/controllers/student_tasks_controller.rb
... ...
@@ -0,0 +1,27 @@
  1
+class StudentTasksController < ApplicationController
  2
+  before_filter :find_course, :find_student, :find_task
  3
+
  4
+  def edit
  5
+  end
  6
+
  7
+  def update
  8
+    status = params[:student_task][:status]
  9
+    result = @student_task.complete(status)
  10
+
  11
+    if(result)
  12
+      url = course_student_path(@course, @student)
  13
+      redirect_to(url, notice: "Task completed")
  14
+    else
  15
+      url = @student_task.edit_url 
  16
+      redirect_to(url, alert: "Task was not completed")
  17
+    end
  18
+  end
  19
+
  20
+  private
  21
+
  22
+  def find_task
  23
+    @student_task = @student.tasks.find { |st| st.task_id == params[:task_id].to_i }
  24
+    not_found unless @student_task
  25
+  end
  26
+
  27
+end
6  app/controllers/students_controller.rb
... ...
@@ -0,0 +1,6 @@
  1
+class StudentsController < ApplicationController
  2
+  before_filter :find_course, :find_student
  3
+
  4
+  def show
  5
+  end
  6
+end
18  app/controllers/study_plans_controller.rb
... ...
@@ -0,0 +1,18 @@
  1
+class StudyPlansController < ApplicationController
  2
+  before_filter :find_course, :find_student
  3
+
  4
+  def show
  5
+    @study_plan = StudyPlanDecorator.new(@student.study_plan)
  6
+  end
  7
+
  8
+  def edit
  9
+    @study_plan = @student.study_plan
  10
+  end
  11
+
  12
+  def update
  13
+    @student.study_plan.update_attribute(:content, params[:study_plan][:content])
  14
+    flash[:notice] = "Study plan updated."
  15
+
  16
+    redirect_to(course_student_plan_path(@course, @student))
  17
+  end
  18
+end
6  app/controllers/tasks_controller.rb
@@ -33,10 +33,4 @@ def check_permission
33 33
       redirect_to(@course, alert: "Unauthorized access")
34 34
     end
35 35
   end
36  
-
37  
-  def find_course
38  
-    @course = Course.find(params[:course_id])
39  
-  rescue ActiveRecord::RecordNotFound
40  
-    redirect_to(root_url, alert: "Couldn't find course")
41  
-  end
42 36
 end
39  app/decorators/person_decorator.rb
@@ -3,12 +3,43 @@ class PersonDecorator < ApplicationDecorator
3 3
 
4 4
   allows :name, :email, :github_nickname, :permissions, :gravatar_url
5 5
   
6  
-  def self.from_github_name(name)
7  
-    new(Clubhouse::Client::Person.new(name))
  6
+  def self.from_github(github_nickname)
  7
+      new(Clubhouse::Client::Person.new(github_nickname))
  8
+    rescue Clubhouse::Client::PersonNotFound
  9
+      return nil
  10
+  end
  11
+
  12
+  def can_access_liskov?
  13
+    permissions.include?('Liskov')
  14
+  end
  15
+
  16
+  def membership_for(course)
  17
+    course.membership_for(person)
  18
+  end
  19
+
  20
+  def role_for(course)
  21
+    membership_for(course).try(:role)
8 22
   end
9 23
 
10 24
   def has_role?(role, course)
11  
-    membership = course.course_memberships.for_person(person).first
12  
-    membership.has_role?(role) if membership
  25
+    role_for(course).to_s.capitalize == role.to_s.capitalize
  26
+    # membership = membership_for(course)
  27
+    # membership.has_role?(role) if membership
  28
+  end
  29
+
  30
+  def to_param
  31
+    github_nickname
  32
+  end
  33
+
  34
+  def ==(person)
  35
+    github_nickname == person.github_nickname
  36
+  end
  37
+
  38
+  def profile_url(course)
  39
+    if has_role?(:student, course)
  40
+      h.course_student_path(course, self)
  41
+    else
  42
+      "http://community.mendicantuniversity.org/people/#{person.github_nickname}"
  43
+    end
13 44
   end
14 45
 end
25  app/decorators/student_decorator.rb
... ...
@@ -0,0 +1,25 @@
  1
+class StudentDecorator < ApplicationDecorator
  2
+  decorates :course_membership
  3
+
  4
+  def name
  5
+    course_membership.person.name
  6
+  end
  7
+
  8
+  def tasks
  9
+    course_membership.student_tasks.map {|st| StudentTaskDecorator.new(st)}
  10
+  end
  11
+
  12
+  def study_plan
  13
+    StudyPlan.find_or_create_by_course_membership_id(course_membership)
  14
+  end
  15
+
  16
+  def to_param
  17
+    course_membership.person.to_param
  18
+  end
  19
+
  20
+  private 
  21
+
  22
+  def course
  23
+    course_membership.course
  24
+  end
  25
+end
32  app/decorators/student_task_decorator.rb
... ...
@@ -0,0 +1,32 @@
  1
+class StudentTaskDecorator < ApplicationDecorator
  2
+  decorates :student_task
  3
+
  4
+  def status
  5
+    student_task.complete? ? student_task.status : "Not complete"
  6
+  end
  7
+
  8
+  def completed_status_css
  9
+    student_task.complete? ? "complete" : "not-complete"
  10
+  end
  11
+
  12
+  def edit_link(current_person)
  13
+    if(current_person.has_role?(:instructor, student_task.course))
  14
+      h.link_to "Mark as complete", edit_url
  15
+    end
  16
+  end
  17
+
  18
+  def edit_url
  19
+    course  = student_task.course
  20
+    student = student_task.student
  21
+    task_id = student_task.task_id
  22
+    h.edit_course_student_student_tasks_path(course, student, task_id: task_id)
  23
+  end
  24
+
  25
+  def update_url
  26
+    course  = student_task.course
  27
+    student = student_task.student
  28
+    task_id = student_task.task_id
  29
+    h.course_student_student_tasks_path(course, student, task_id: task_id)
  30
+  end
  31
+
  32
+end
7  app/decorators/study_plan_decorator.rb
... ...
@@ -0,0 +1,7 @@
  1
+class StudyPlanDecorator < ApplicationDecorator
  2
+  decorates :study_plan
  3
+
  4
+  def content
  5
+    MdPreview::Parser.parse(study_plan.content)
  6
+  end
  7
+end
6  app/models/completed_task.rb
... ...
@@ -0,0 +1,6 @@
  1
+class CompletedTask < ActiveRecord::Base
  2
+  belongs_to :course_membership
  3
+  belongs_to :task
  4
+
  5
+  validates_presence_of :description
  6
+end
8  app/models/course.rb
@@ -3,8 +3,12 @@ class Course < ActiveRecord::Base
3 3
   has_many :course_memberships
4 4
   has_many :tasks
5 5
 
6  
-  def people
7  
-    @people ||= course_memberships.map {|cm| cm.person }
  6
+  def participants
  7
+    @participants ||= course_memberships.map { |cm| cm.person }
8 8
   end
9 9
 
  10
+  def membership_for(person)
  11
+    return nil unless person && person.github_nickname
  12
+    course_memberships.for_person(person).first
  13
+  end
10 14
 end
19  app/models/course_membership.rb
... ...
@@ -1,7 +1,10 @@
1 1
 class CourseMembership < ActiveRecord::Base
2 2
   ROLES = %w{Student Mentor Instructor}
3 3
 
4  
-  belongs_to :course
  4
+  belongs_to  :course
  5
+  has_one     :study_plan
  6
+
  7
+  has_many :completed_tasks
5 8
 
6 9
   validates_presence_of   :course_id, :role, :person_github_nickname
7 10
   validates_uniqueness_of :person_github_nickname, :scope => :course_id
@@ -10,22 +13,22 @@ class CourseMembership < ActiveRecord::Base
10 13
   scope :for_person, lambda { |person| where(person_github_nickname: person.github_nickname) }
11 14
 
12 15
   def person
13  
-    @person ||= Clubhouse::Client::Person.new(person_github_nickname)
14  
-  rescue Clubhouse::Client::PersonNotFound
15  
-    return nil
  16
+    @person ||= PersonDecorator.from_github(person_github_nickname)
16 17
   end
17 18
 
18 19
   def has_role?(has_role)
19 20
     has_role.to_s.capitalize == role.capitalize
20 21
   end
21 22
 
  23
+  def student_tasks
  24
+    StudentTask.build_for(self)
  25
+  end
  26
+
22 27
   private
23 28
 
24 29
   def person_permissions
25  
-    if person.nil?
26  
-      errors.add(:person_github_nickname, "is not valid")
27  
-    elsif person.permissions['Liskov'].nil?
28  
-      errors.add(:person_github_nickname, "does not have access to Liskov")
  30
+    unless person && person.can_access_liskov?
  31
+      errors.add(:person_github_nickname, "needs Clubhouse ID and access to Liskov")
29 32
     end
30 33
   end
31 34
 end
53  app/models/student_task.rb
... ...
@@ -0,0 +1,53 @@
  1
+class StudentTask
  2
+  NOT_COMPLETE = -1
  3
+
  4
+  def self.build_for(course_membership)
  5
+    course_membership.course.tasks.map {|t| new(course_membership, t) }
  6
+  end
  7
+
  8
+  def initialize(course_membership, task)
  9
+    @course_membership = course_membership
  10
+    @task = task
  11
+  end
  12
+
  13
+  def course
  14
+    @course_membership.course
  15
+  end
  16
+
  17
+  def task_id
  18
+    @task.id
  19
+  end
  20
+
  21
+  def description
  22
+    @task.description
  23
+  end
  24
+
  25
+  def student
  26
+    @course_membership.person
  27
+  end
  28
+
  29
+  def complete?
  30
+    status != NOT_COMPLETE
  31
+  end
  32
+
  33
+  def status
  34
+    completed_task ? completed_task.description : NOT_COMPLETE
  35
+  end
  36
+
  37
+  def complete(status)
  38
+    completed = completed_tasks.find_or_create_by_task_id(task_id: task_id)
  39
+    completed.description = status
  40
+    completed.save
  41
+  end
  42
+
  43
+  private
  44
+
  45
+  def completed_tasks
  46
+    @course_membership.completed_tasks
  47
+  end
  48
+
  49
+  def completed_task
  50
+    completed_tasks.where(task_id: task_id).first
  51
+  end
  52
+
  53
+end
2  app/models/study_plan.rb
... ...
@@ -0,0 +1,2 @@
  1
+class StudyPlan < ActiveRecord::Base
  2
+end
11  app/views/course_memberships/_participants.html.haml
... ...
@@ -1,9 +1,10 @@
1 1
 %h2 Participants
2 2
 
3 3
 %ul
4  
-  - @members.each do |membership|
5  
-    %p
6  
-      = image_tag membership.person.gravatar_url(25)
7  
-      = "#{membership.person.name} // #{membership.role}"
  4
+  - @participants.each do |participant|
  5
+    %li
  6
+      = image_tag participant.person.gravatar_url(25)
  7
+      = link_to(participant.name, participant.profile_url(@course))
  8
+      = "#{participant.role_for(@course)}"
8 9
       \-
9  
-      = link_to "Remove", course_membership_path(membership), :method => :delete
  10
+      = link_to "Remove", course_membership_path(participant.membership_for(@course)), :method => :delete
7  app/views/student_tasks/edit.html.haml
... ...
@@ -0,0 +1,7 @@
  1
+%p
  2
+  = @student_task.description
  3
+
  4
+= form_tag @student_task.update_url, method: :put do
  5
+  = label :student_task_, :status
  6
+  = text_field :student_task, :status 
  7
+  = submit_tag "Submit"
14  app/views/students/show.html.haml
... ...
@@ -0,0 +1,14 @@
  1
+%h1
  2
+  = @student.name
  3
+
  4
+%p= link_to("Study plan", course_student_plan_path(@course, @student))
  5
+
  6
+%h3
  7
+  Tasks:
  8
+
  9
+%table#tasks
  10
+  - @student.tasks.each do |t|
  11
+    %tr{:data => {:taskid => t.task_id}}
  12
+      %td.task= t.description
  13
+      %td.status{:class => t.completed_status_css }=  t.status
  14
+      %td.complete_link= t.edit_link(current_person)
3  app/views/study_plans/edit.html.haml
... ...
@@ -0,0 +1,3 @@
  1
+= form_for(@study_plan, url: course_student_plan_path(@course, @student)) do |f|
  2
+  = f.text_area :content
  3
+  = f.submit "Save changes"
4  app/views/study_plans/show.html.haml
... ...
@@ -0,0 +1,4 @@
  1
+- if current_person.has_role?(:instructor, @course)
  2
+  %p= link_to("Edit plan", edit_course_student_plan_path(@course, @student))
  3
+
  4
+%p= @study_plan.content
6  config/routes.rb
@@ -8,6 +8,10 @@
8 8
 
9 9
   resources :courses do
10 10
     resources :tasks, :discussions
  11
+    resources :students do
  12
+      resource :plan, controller: "study_plans"
  13
+      resource :student_tasks
  14
+    end
11 15
   end
12 16
   resources :course_memberships
13  
-end
  17
+end
9  db/migrate/20120325183301_create_completed_tasks.rb
... ...
@@ -0,0 +1,9 @@
  1
+class CreateCompletedTasks < ActiveRecord::Migration
  2
+  def change
  3
+    create_table :completed_tasks do |t|
  4
+      t.belongs_to :course_membership
  5
+      t.belongs_to :task
  6
+      t.string     :description
  7
+    end
  8
+  end
  9
+end
12  db/migrate/20120325210755_create_study_plans.rb
... ...
@@ -0,0 +1,12 @@
  1
+class CreateStudyPlans < ActiveRecord::Migration
  2
+  def up
  3
+    create_table(:study_plans) do |t|
  4
+      t.references  :course_membership
  5
+      t.text        :content
  6
+    end
  7
+  end
  8
+
  9
+  def down
  10
+    drop_table(:study_plans)
  11
+  end
  12
+end
13  db/schema.rb
@@ -11,7 +11,13 @@
11 11
 #
12 12
 # It's strongly recommended to check this file into your version control system.
13 13
 
14  
-ActiveRecord::Schema.define(:version => 20120317200349) do
  14
+ActiveRecord::Schema.define(:version => 20120325210755) do
  15
+
  16
+  create_table "completed_tasks", :force => true do |t|
  17
+    t.integer "course_membership_id"
  18
+    t.integer "task_id"
  19
+    t.string  "description"
  20
+  end
15 21
 
16 22
   create_table "course_memberships", :force => true do |t|
17 23
     t.integer  "course_id"
@@ -38,6 +44,11 @@
38 44
     t.boolean  "archived",   :default => false
39 45
   end
40 46
 
  47
+  create_table "study_plans", :force => true do |t|
  48
+    t.integer "course_membership_id"
  49
+    t.text    "content"
  50
+  end
  51
+
41 52
   create_table "tasks", :force => true do |t|
42 53
     t.integer  "course_id"
43 54
     t.string   "description"
18  test/factories/course_memberships.rb
... ...
@@ -1,7 +1,17 @@
1 1
 FactoryGirl.define do
2  
-  factory :course_membership do
  2
+  factory :student_membership, class: CourseMembership do
3 3
     course                  nil
4  
-    person_github_nickname  nil
5  
-    role                    nil
  4
+    person_github_nickname  "student"
  5
+    role                    "Student"
  6
+
  7
+    after_create do |ms, this_factory|
  8
+      ms.create_study_plan(content: "## Study Plan")
  9
+    end
6 10
   end
7  
-end
  11
+
  12
+  factory :instructor_membership, class: CourseMembership do
  13
+    course                  nil
  14
+    person_github_nickname  "instructor"
  15
+    role                    "Instructor"
  16
+  end
  17
+end
5  test/factories/courses.rb
@@ -2,5 +2,10 @@
2 2
   factory :webdev, class: Course do
3 3
     name "Web Development"
4 4
     description "MU's Web Development Course"
  5
+    after_create do |c|
  6
+      Factory(:community_service, :course => c)
  7
+      Factory(:personal_project , :course => c)
  8
+      Factory(:challenge        , :course => c)
  9
+    end
5 10
   end
6 11
 end
22  test/factories/people.rb
@@ -14,20 +14,20 @@
14 14
   end
15 15
 
16 16
   factory :instructor, parent: "person" do
  17
+    name            "Instructor"
17 18
     github_nickname "instructor"
18  
-    course_membership {
19  
-      FactoryGirl.create(:course_membership, course: course,
20  
-                          person_github_nickname: "#{github_nickname}",
21  
-                          role: "Instructor") if course
22  
-    }
  19
+
  20
+    after_build do |instructor, this_factory|
  21
+      FactoryGirl.create(:instructor_membership, course: instructor.course) if instructor.course
  22
+    end
23 23
   end
24 24
 
25 25
   factory :student, parent: "person" do
  26
+    name            "Student"
26 27
     github_nickname "student"
27  
-    course_membership {
28  
-      FactoryGirl.create(:course_membership, course: course,
29  
-                          person_github_nickname: "#{github_nickname}",
30  
-                          role: "Student") if course
31  
-    }
  28
+
  29
+    after_build do |student, this_factory|
  30
+      FactoryGirl.create(:student_membership, course: student.course) if student.course
  31
+    end
32 32
   end
33  
-end
  33
+end
11  test/factories/tasks.rb
... ...
@@ -0,0 +1,11 @@
  1
+FactoryGirl.define do
  2
+  factory :community_service, class: Task do
  3
+    description "Community Service"
  4
+  end
  5
+  factory :personal_project, class: Task do
  6
+    description "Personal Project"
  7
+  end
  8
+  factory :challenge, class: Task do
  9
+    description "Challenge"
  10
+  end
  11
+end
15  test/functional/courses_controller_test.rb
... ...
@@ -0,0 +1,15 @@
  1
+require "test_helper"
  2
+
  3
+describe CoursesController do
  4
+  before do
  5
+    @course   = FactoryGirl.create(:webdev)
  6
+    @student  = build_person(:student, @course)
  7
+    @controller.current_person = @student
  8
+  end
  9
+
  10
+  it "lists participants in course page" do
  11
+    get(:show, id: @course.id)
  12
+    assigns(:participants).size.must_equal 1
  13
+    assigns(:participants).must_include @student
  14
+  end
  15
+end
29  test/functional/study_plans_controller_test.rb
... ...
@@ -0,0 +1,29 @@
  1
+require "test_helper"
  2
+
  3
+describe StudyPlansController do
  4
+  before do
  5
+    @course   = FactoryGirl.create(:webdev)
  6
+    @student  = build_person(:student, @course)
  7
+    @controller.current_person = @student
  8
+  end
  9
+
  10
+  def study_plan_params(params = {})
  11
+    { course_id: @course.id,
  12
+      student_id: @student.github_nickname
  13
+    }.merge(params)
  14
+  end
  15
+
  16
+  it "automatically creates study plan on #show" do
  17
+    get(:show, study_plan_params)
  18
+    assigns(:study_plan).wont_equal nil
  19
+  end
  20
+
  21
+  it "updates study plan" do
  22
+    put(:update, study_plan_params(study_plan: { content: "New plan" }))
  23
+    plan = StudyPlan.last
  24
+
  25
+    plan.content.must_equal "New plan"
  26
+    flash[:notice].must_equal "Study plan updated."
  27
+    response.redirect?.must_equal true
  28
+  end
  29
+end
6  test/functional/tasks_controller_test.rb
@@ -19,9 +19,7 @@
19 19
     response.success?.must_equal true
20 20
   end
21 21
 
22  
-  it "redirects to root if can't find course" do
23  
-    get(:new, course_id: 'ohai!')
24  
-    response.redirect?.must_equal true
25  
-    flash[:alert].must_equal "Couldn't find course"
  22
+  it "raises 404 if can't find course" do
  23
+    proc { get(:new, course_id: 'ohai!') }.must_raise ActionController::RoutingError
26 24
   end
27 25
 end
20  test/integration/participants_test.rb
... ...
@@ -0,0 +1,20 @@
  1
+require "test_helper"
  2
+
  3
+describe "Participants Integration" do
  4
+  before do
  5
+    @course     = FactoryGirl.create(:webdev)
  6
+    @student    = build_person(:student, @course)
  7
+    @instructor = build_person(:instructor, @course)
  8
+
  9
+    sign_in(@student)
  10
+    click_link "Web Development"
  11
+  end
  12
+
  13
+  it "links student to her page in the course" do
  14
+    find_link("Student")["href"].must_equal course_student_path(@course, @student)
  15
+  end
  16
+
  17
+  it "links instructor to his community page" do
  18
+    find_link("Instructor")["href"].must_equal "http://community.mendicantuniversity.org/people/instructor"
  19
+  end
  20
+end
91  test/integration/student_page_test.rb
... ...
@@ -0,0 +1,91 @@
  1
+require "test_helper"
  2
+
  3
+describe "Student Page Integration" do
  4
+
  5
+  before do
  6
+    @course     = FactoryGirl.create(:webdev)
  7
+    @student    = build_person(:student, @course)
  8
+    @instructor = build_person(:instructor, @course)
  9
+  end
  10
+
  11
+  it "should display a read-only status for each course task" do
  12
+    sign_in @student
  13
+
  14
+    click_link "Web Development"
  15
+    click_link "Student"
  16
+
  17
+    @course.tasks.each do |t|
  18
+      within_task(t) do 
  19
+        assert_task_is_on_page(t)
  20
+        assert_task_is_incomplete(t)
  21
+        assert_complete_task_link_does_not_exist
  22
+      end
  23
+    end
  24
+  end
  25
+
  26
+  it "should allow an instructor to complete a task" do
  27
+    sign_in @instructor
  28
+
  29
+    click_link "Web Development"
  30
+    click_link "Student"
  31
+
  32
+    task = @course.tasks.first
  33
+
  34
+    within_task(task) do
  35
+      assert_complete_task_link_exists
  36
+      click_link "Mark as complete"
  37
+    end
  38
+
  39
+    fill_in("Status", with: "Puzzlenode")
  40
+    click_button "Submit"
  41
+
  42
+    within_task(task) do
  43
+      assert_task_has_status(task, "Puzzlenode")
  44
+    end
  45
+  end
  46
+
  47
+end
  48
+
  49
+private
  50
+
  51
+def within_task(task)
  52
+  within(:xpath, task_xpath(task)) do
  53
+    yield
  54
+  end
  55
+end
  56
+
  57
+def assert_complete_task_link_exists
  58
+  has_link?("Mark as complete").must_equal true, "The link to complete the task does not exist"
  59
+end
  60
+
  61
+def assert_complete_task_link_does_not_exist
  62
+  has_link?("Mark as complete").must_equal false, "The link to complete the task should not exist"
  63
+end
  64
+
  65
+def assert_task_has_status(task, status)
  66
+  has_content?(status).must_equal true, "Task '#{task.description}' does not have status '#{status}'"
  67
+end
  68
+
  69
+def assert_task_is_incomplete(task)
  70
+  task_incomplete?.must_equal true, "Task '#{task.description}' is not marked as incomplete"
  71
+end
  72
+
  73
+def assert_task_is_on_page(task)
  74
+  task_on_page?(task).must_equal true, "Could not find task '#{task.description}' on the page"
  75
+end
  76
+
  77
+def task_on_page?(task)
  78
+  has_content?(task.description)
  79
+end
  80
+
  81
+def task_on_page?(task)
  82
+  has_content?(task.description)
  83
+end
  84
+
  85
+def task_incomplete?
  86
+  has_content?("Not complete")
  87
+end
  88
+
  89
+def task_xpath(task)
  90
+  "//tr[@data-taskid='#{task.id}']"
  91
+end
34  test/integration/study_plans_test.rb
... ...
@@ -0,0 +1,34 @@
  1
+require "test_helper"
  2
+
  3
+describe "Study plans Integration" do
  4
+  before do
  5
+    @course     = FactoryGirl.create(:webdev)
  6
+    @student    = build_person(:student, @course)
  7
+    @instructor = build_person(:instructor, @course)
  8
+  end
  9
+
  10
+  it "cannot be edited by students" do
  11
+    sign_in(@student)
  12
+    click_link "Web Development"
  13
+    click_link "Student"
  14
+    click_link "Study plan"
  15
+    page.body.wont_include "Edit plan"
  16
+  end
  17
+
  18
+  it "can be edited by instructor" do
  19
+    sign_in(@instructor)
  20
+    click_link "Web Development"
  21
+    click_link "Student"
  22
+    click_link "Study plan"
  23
+    click_link "Edit plan"
  24
+    page.has_selector?("textarea#study_plan_content").must_equal true
  25
+  end
  26
+
  27
+  it "parses Markdown" do
  28
+    sign_in(@student)
  29
+    click_link "Web Development"
  30
+    click_link "Student"
  31
+    click_link "Study plan"
  32
+    page.body.must_include "<h2>Study Plan</h2>"
  33
+  end
  34
+end
4  test/integration/tasks_test.rb
@@ -13,9 +13,9 @@
13 13
 
14 14
     click_link "Web Development"
15 15
     click_link "Add Task"
16  
-    fill_in("Description", with: "Community Service")
  16
+    fill_in("Description", with: "A new course task")
17 17
     click_button "Create Task"
18