PageRenderTime 52ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/app/models/assignment.rb

https://github.com/danrosshoward/expertiza
Ruby | 899 lines | 627 code | 136 blank | 136 comment | 143 complexity | 5bef0ba349fda59c2e19290f68cc61ab MD5 | raw file
Possible License(s): GPL-2.0
  1. class Assignment < ActiveRecord::Base
  2. include DynamicReviewMapping
  3. belongs_to :course
  4. belongs_to :wiki_type
  5. # wiki_type needs to be removed. When an assignment is created, it needs to
  6. # be created as an instance of a subclass of the Assignment (model) class;
  7. # then Rails will "automatically" set the type field to the value that
  8. # designates an assignment of the appropriate type.
  9. has_many :participants, :class_name => 'AssignmentParticipant', :foreign_key => 'parent_id'
  10. has_many :participant_review_mappings, :class_name => 'ParticipantReviewResponseMap', :through => :participants, :source => :review_mappings
  11. has_many :users, :through => :participants
  12. has_many :due_dates
  13. has_many :teams, :class_name => 'AssignmentTeam', :foreign_key => 'parent_id'
  14. has_many :team_review_mappings, :class_name => 'TeamReviewResponseMap', :through => :teams, :source => :review_mappings
  15. has_many :invitations, :class_name => 'Invitation', :foreign_key => 'assignment_id'
  16. has_many :assignment_questionnaires
  17. has_many :questionnaires, :through => :assignment_questionnaires
  18. belongs_to :instructor, :class_name => 'User', :foreign_key => 'instructor_id'
  19. has_many :sign_up_topics, :foreign_key => 'assignment_id', :dependent => :destroy
  20. has_many :response_maps, :foreign_key => 'reviewed_object_id', :class_name => 'ResponseMap'
  21. # TODO A bug in Rails http://dev.rubyonrails.org/ticket/4996 prevents us from using this:
  22. # has_many :responses, :through => :response_maps, :source => 'response'
  23. validates_presence_of :name
  24. validates_uniqueness_of :scope => [:directory_path, :instructor_id]
  25. COMPLETE = "Complete"
  26. # Review Strategy information.
  27. RS_INSTRUCTOR_SELECTED = 'Instructor-Selected'
  28. RS_STUDENT_SELECTED = 'Student-Selected'
  29. RS_AUTO_SELECTED = 'Auto-Selected'
  30. REVIEW_STRATEGIES = [RS_INSTRUCTOR_SELECTED, RS_STUDENT_SELECTED, RS_AUTO_SELECTED]
  31. DEFAULT_MAX_REVIEWERS = 3
  32. # Returns a set of topics that can be reviewed.
  33. # We choose the topics if one of its submissions has received the fewest reviews so far
  34. def candidate_topics_to_review
  35. return nil if sign_up_topics.empty? # This is not a topic assignment
  36. contributor_set = Array.new(contributors)
  37. # Reject contributors that have not selected a topic, or have no submissions
  38. contributor_set.reject! { |contributor| signed_up_topic(contributor).nil? or !contributor.has_submissions? }
  39. # Reject contributions of topics whose deadline has passed
  40. contributor_set.reject! { |contributor| contributor.assignment.get_current_stage(signed_up_topic(contributor).id) == "Complete" or
  41. contributor.assignment.get_current_stage(signed_up_topic(contributor).id) == "submission" }
  42. # Filter the contributors with the least number of reviews
  43. # (using the fact that each contributor is associated with a topic)
  44. contributor = contributor_set.min_by { |contributor| contributor.review_mappings.count }
  45. min_reviews = contributor.review_mappings.count rescue 0
  46. contributor_set.reject! { |contributor| contributor.review_mappings.count > min_reviews + review_topic_threshold }
  47. candidate_topics = Set.new
  48. contributor_set.each { |contributor| candidate_topics.add(signed_up_topic(contributor)) }
  49. candidate_topics
  50. end
  51. def has_topics?
  52. @has_topics ||= !sign_up_topics.empty?
  53. end
  54. def assign_reviewer_dynamically(reviewer, topic)
  55. # The following method raises an exception if not successful which
  56. # has to be captured by the caller (in review_mapping_controller)
  57. contributor = contributor_to_review(reviewer, topic)
  58. contributor.assign_reviewer(reviewer)
  59. end
  60. # Returns a contributor to review if available, otherwise will raise an error
  61. def contributor_to_review(reviewer, topic)
  62. raise "Please select a topic" if has_topics? and topic.nil?
  63. raise "This assignment does not have topics" if !has_topics? and topic
  64. # This condition might happen if the reviewer waited too much time in the
  65. # select topic page and other students have already selected this topic.
  66. # Another scenario is someone that deliberately modifies the view.
  67. if topic
  68. raise "This topic has too many reviews; please select another one." unless candidate_topics_to_review.include?(topic)
  69. end
  70. contributor_set = Array.new(contributors)
  71. work = (topic.nil?) ? 'assignment' : 'topic'
  72. # 1) Only consider contributors that worked on this topic; 2) remove reviewer as contributor
  73. # 3) remove contributors that have not submitted work yet
  74. contributor_set.reject! do |contributor|
  75. signed_up_topic(contributor) != topic or # both will be nil for assignments with no signup sheet
  76. contributor.includes?(reviewer) or
  77. !contributor.has_submissions?
  78. end
  79. raise "There are no more submissions to review on this #{work}." if contributor_set.empty?
  80. # Reviewer can review each contributor only once
  81. contributor_set.reject! { |contributor| contributor.reviewed_by?(reviewer) }
  82. raise "You have already reviewed all submissions for this #{work}." if contributor_set.empty?
  83. # Reduce to the contributors with the least number of reviews ("responses") received
  84. min_contributor = contributor_set.min_by { |a| a.responses.count }
  85. min_reviews = min_contributor.responses.count
  86. contributor_set.reject! { |contributor| contributor.responses.count > min_reviews }
  87. # Pick the contributor whose most recent reviewer was assigned longest ago
  88. if min_reviews > 0
  89. # Sort by last review mapping id, since it reflects the order in which reviews were assigned
  90. # This has a round-robin effect
  91. # Sorting on id assumes that ids are assigned sequentially in the db.
  92. # .last assumes the database returns rows in the order they were created.
  93. # Added unit tests to ensure these conditions are both true with the current database.
  94. contributor_set.sort! { |a, b| a.review_mappings.last.id <=> b.review_mappings.last.id }
  95. end
  96. # Choose a contributor at random (.sample) from the remaining contributors.
  97. # Actually, we SHOULD pick the contributor who was least recently picked. But sample
  98. # is much simpler, and probably almost as good, given that even if the contributors are
  99. # picked in round-robin fashion, the reviews will not be submitted in the same order that
  100. # they were picked.
  101. return contributor_set.sample
  102. end
  103. def contributors
  104. #ACS Contributors are just teams, so removed check to see if it is a team assignment
  105. @contributors ||= teams #ACS
  106. end
  107. def review_mappings
  108. #ACS Reviews must be mapped just for teams, so removed check to see if it is a team assignment
  109. @review_mappings ||= team_review_mappings #ACS
  110. end
  111. def assign_metareviewer_dynamically(metareviewer)
  112. # The following method raises an exception if not successful which
  113. # has to be captured by the caller (in review_mapping_controller)
  114. response_map = response_map_to_metareview(metareviewer)
  115. response_map.assign_metareviewer(metareviewer)
  116. end
  117. # Returns a review (response) to metareview if available, otherwise will raise an error
  118. def response_map_to_metareview(metareviewer)
  119. response_map_set = Array.new(review_mappings)
  120. # Reject response maps without responses
  121. response_map_set.reject! { |response_map| !response_map.response }
  122. raise "There are no reviews to metareview at this time for this assignment." if response_map_set.empty?
  123. # Reject reviews where the metareviewer was the reviewer or the contributor
  124. response_map_set.reject! do |response_map|
  125. (response_map.reviewee == metareviewer) or (response_map.reviewer.includes?(metareviewer))
  126. end
  127. raise "There are no more reviews to metareview for this assignment." if response_map_set.empty?
  128. # Metareviewer can only metareview each review once
  129. response_map_set.reject! { |response_map| response_map.metareviewed_by?(metareviewer) }
  130. raise "You have already metareviewed all reviews for this assignment." if response_map_set.empty?
  131. # Reduce to the response maps with the least number of metareviews received
  132. response_map_set.sort! { |a, b| a.metareview_response_maps.count <=> b.metareview_response_maps.count }
  133. min_metareviews = response_map_set.first.metareview_response_maps.count
  134. response_map_set.reject! { |response_map| response_map.metareview_response_maps.count > min_metareviews }
  135. # Reduce the response maps to the reviewers with the least number of metareviews received
  136. reviewers = Hash.new # <reviewer, number of metareviews>
  137. response_map_set.each do |response_map|
  138. reviewer = response_map.reviewer
  139. reviewers.member?(reviewer) ? reviewers[reviewer] += 1 : reviewers[reviewer] = 1
  140. end
  141. reviewers = reviewers.sort { |a, b| a[1] <=> b[1] }
  142. min_metareviews = reviewers.first[1]
  143. reviewers.reject! { |reviewer| reviewer[1] == min_metareviews }
  144. response_map_set.reject! { |response_map| reviewers.member?(response_map.reviewer) }
  145. # Pick the response map whose most recent metareviewer was assigned longest ago
  146. response_map_set.sort! { |a, b| a.metareview_response_maps.count <=> b.metareview_response_maps.count }
  147. min_metareviews = response_map_set.first.metareview_response_maps.count
  148. if min_metareviews > 0
  149. # Sort by last metareview mapping id, since it reflects the order in which reviews were assigned
  150. # This has a round-robin effect
  151. response_map_set.sort! { |a, b| a.metareview_response_maps.last.id <=> b.metareview_response_maps.last.id }
  152. end
  153. # The first review_map is the best candidate to metareview
  154. return response_map_set.first
  155. end
  156. def is_using_dynamic_reviewer_assignment?
  157. if self.review_assignment_strategy == RS_AUTO_SELECTED or
  158. self.review_assignment_strategy == RS_STUDENT_SELECTED
  159. return true
  160. else
  161. return false
  162. end
  163. end
  164. def review_mappings
  165. #ACS Removed the if condition(and corressponding else) which differentiate assignments as team and individual assignments
  166. # to treat all assignments as team assignments
  167. TeamReviewResponseMap.find_all_by_reviewed_object_id(self.id)
  168. end
  169. def metareview_mappings
  170. mappings = Array.new
  171. self.review_mappings.each{
  172. | map |
  173. mmap = MetareviewResponseMap.find_by_reviewed_object_id(map.id)
  174. if mmap != nil
  175. mappings << mmap
  176. end
  177. }
  178. return mappings
  179. end
  180. def get_scores(questions)
  181. scores = Hash.new
  182. scores[:participants] = Hash.new
  183. self.participants.each{
  184. | participant |
  185. scores[:participants][participant.id.to_s.to_sym] = participant.get_scores(questions)
  186. }
  187. #ACS Removed the if condition(and corressponding else) which differentiate assignments as team and individual assignments
  188. # to treat all assignments as team assignments
  189. scores[:teams] = Hash.new
  190. index = 0
  191. self.teams.each{
  192. | team |
  193. scores[:teams][index.to_s.to_sym] = Hash.new
  194. scores[:teams][index.to_s.to_sym][:team] = team
  195. assessments = TeamReviewResponseMap.get_assessments_for(team)
  196. scores[:teams][index.to_s.to_sym][:scores] = Score.compute_scores(assessments, questions[:review])
  197. #... = ScoreCache.get_participant_score(team, id, questionnaire.display_type)
  198. index += 1
  199. }
  200. return scores
  201. end
  202. def get_contributor(contrib_id)
  203. if team_assignment
  204. return AssignmentTeam.find(contrib_id)
  205. else
  206. return AssignmentParticipant.find(contrib_id)
  207. end
  208. end
  209. # parameterized by questionnaire
  210. def get_max_score_possible(questionnaire)
  211. max = 0
  212. sum_of_weights = 0
  213. num_questions = 0
  214. questionnaire.questions.each { |question| #type identifies the type of questionnaire
  215. sum_of_weights += question.weight
  216. num_questions+=1
  217. }
  218. max = num_questions * questionnaire.max_question_score * sum_of_weights
  219. return max, sum_of_weights
  220. end
  221. def get_path
  222. if self.course_id == nil and self.instructor_id == nil
  223. raise "Path can not be created. The assignment must be associated with either a course or an instructor."
  224. end
  225. if self.wiki_type_id != 1
  226. raise PathError, "No path needed"
  227. end
  228. if self.course_id != nil && self.course_id > 0
  229. path = Course.find(self.course_id).get_path
  230. else
  231. path = RAILS_ROOT + "/pg_data/" + FileHelper.clean_path(User.find(self.instructor_id).name) + "/"
  232. end
  233. return path + FileHelper.clean_path(self.directory_path)
  234. end
  235. # Check whether review, metareview, etc.. is allowed
  236. # If topic_id is set, check for that topic only. Otherwise, check to see if there is any topic which can be reviewed(etc) now
  237. def check_condition(column,topic_id=nil)
  238. # the drop topic deadline should not play any role in picking the next due date
  239. # get the drop_topic_deadline_id to exclude it
  240. drop_topic_deadline_id = DeadlineType.find_by_name("drop_topic").id
  241. if self.staggered_deadline?
  242. # next_due_date - the nearest due date that hasn't passed
  243. if topic_id
  244. # next for topic
  245. next_due_date = TopicDeadline.find(:first, :conditions => ['topic_id = ? and due_at >= ? and deadline_type_id <> ?', topic_id, Time.now, drop_topic_deadline_id], :order => 'due_at')
  246. else
  247. # next for assignment
  248. next_due_date = TopicDeadline.find(:first, :conditions => ['assignment_id = ? and due_at >= ? and deadline_type_id <> ?', self.id, Time.now, drop_topic_deadline_id], :joins => {:topic => :assignment}, :order => 'due_at')
  249. end
  250. else
  251. next_due_date = DueDate.find(:first, :conditions => ['assignment_id = ? and due_at >= ? and deadline_type_id <> ?', self.id, Time.now, drop_topic_deadline_id], :order => 'due_at')
  252. end
  253. if next_due_date.nil?
  254. return false
  255. end
  256. # command pattern - get the attribute with the name in column
  257. # Here, column is usually something like 'review_allowed_id'
  258. right_id = next_due_date.send column
  259. right = DeadlineRight.find(right_id)
  260. #puts "DEBUG RIGHT_ID = " + right_id.to_s
  261. #puts "DEBUG RIGHT = " + right.name
  262. return (right and (right.name == "OK" or right.name == "Late"))
  263. end
  264. # Determine if the next due date from now allows for submissions
  265. def submission_allowed(topic_id=nil)
  266. return (check_condition("submission_allowed_id",topic_id) or check_condition("resubmission_allowed_id",topic_id))
  267. end
  268. # Determine if the next due date from now allows for reviews
  269. def review_allowed(topic_id=nil)
  270. return (check_condition("review_allowed_id",topic_id) or check_condition("rereview_allowed_id",topic_id))
  271. end
  272. # Determine if the next due date from now allows for metareviews
  273. def metareview_allowed(topic_id=nil)
  274. return check_condition("review_of_review_allowed_id",topic_id)
  275. end
  276. def delete(force = nil)
  277. begin
  278. maps = ParticipantReviewResponseMap.find_all_by_reviewed_object_id(self.id)
  279. maps.each{|map| map.delete(force)}
  280. rescue
  281. raise "At least one review response exists for #{self.name}."
  282. end
  283. begin
  284. maps = TeamReviewResponseMap.find_all_by_reviewed_object_id(self.id)
  285. maps.each{|map| map.delete(force)}
  286. rescue
  287. raise "At least one review response exists for #{self.name}."
  288. end
  289. begin
  290. maps = TeammateReviewResponseMap.find_all_by_reviewed_object_id(self.id)
  291. maps.each{|map| map.delete(force)}
  292. rescue
  293. raise "At least one teammate review response exists for #{self.name}."
  294. end
  295. self.invitations.each{|invite| invite.destroy}
  296. self.teams.each{| team | team.delete}
  297. self.participants.each {|participant| participant.delete}
  298. self.due_dates.each{ |date| date.destroy}
  299. # The size of an empty directory is 2
  300. # Delete the directory if it is empty
  301. begin
  302. directory = Dir.entries(RAILS_ROOT + "/pg_data/" + self.directory_path)
  303. rescue
  304. # directory is empty
  305. end
  306. if !is_wiki_assignment and !self.directory_path.empty? and !directory.nil?
  307. if directory.size == 2
  308. Dir.delete(RAILS_ROOT + "/pg_data/" + self.directory_path)
  309. else
  310. raise "Assignment directory is not empty"
  311. end
  312. end
  313. self.assignment_questionnaires.each{|aq| aq.destroy}
  314. self.destroy
  315. end
  316. # Generate emails for reviewers when new content is available for review
  317. #ajbudlon, sept 07, 2007
  318. def email(author_id)
  319. # Get all review mappings for this assignment & author
  320. participant = AssignmentParticipant.find(author_id)
  321. #ACS Removed the if condition(and corressponding else) which differentiate assignments as team and individual assignments
  322. # to treat all assignments as team assignments
  323. author = participant.team
  324. for mapping in author.review_mappings
  325. # If the reviewer has requested an e-mail deliver a notification
  326. # that includes the assignment, and which item has been updated.
  327. if mapping.reviewer.user.email_on_submission
  328. user = mapping.reviewer.user
  329. Mailer.deliver_message(
  330. {:recipients => user.email,
  331. :subject => "A new submission is available for #{self.name}",
  332. :body => {
  333. :obj_name => self.name,
  334. :type => "submission",
  335. :location => get_review_number(mapping).to_s,
  336. :first_name => ApplicationHelper::get_user_first_name(user),
  337. :partial_name => "update"
  338. }
  339. }
  340. )
  341. end
  342. end
  343. end
  344. # Get all review mappings for this assignment & reviewer
  345. # required to give reviewer location of new submission content
  346. # link can not be provided as it might give user ability to access data not
  347. # available to them.
  348. #ajbudlon, sept 07, 2007
  349. def get_review_number(mapping)
  350. reviewer_mappings = ResponseMap.find_all_by_reviewer_id(mapping.reviewer.id)
  351. review_num = 1
  352. for rm in reviewer_mappings
  353. if rm.reviewee.id != mapping.reviewee.id
  354. review_num += 1
  355. else
  356. break
  357. end
  358. end
  359. return review_num
  360. end
  361. # It appears that this method is not used at present!
  362. def is_wiki_assignment
  363. return (self.wiki_type_id > 1)
  364. end
  365. # Check to see if assignment is a microtask
  366. def is_microtask?
  367. return (self.microtask.nil?) ? False : self.microtask
  368. end
  369. #
  370. def self.is_submission_possible (assignment)
  371. # Is it possible to upload a file?
  372. # Check whether the directory text box is nil
  373. if assignment.directory_path != nil && assignment.wiki_type == 1
  374. return true
  375. # Is it possible to submit a URL (or a wiki page)
  376. elsif assignment.directory_path != nil && /(^$)|(^(http|https):\/\/[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(([0-9]{1,5})?\/.*)?$)/ix.match(assignment.directory_path)
  377. # In this case we have to check if the directory_path starts with http / https.
  378. return true
  379. # Is it possible to submit a Google Doc?
  380. # removed because google doc not implemented
  381. # elsif assignment.wiki_type == 4 #GOOGLE_DOC
  382. # return true
  383. else
  384. return false
  385. end
  386. end
  387. def is_google_doc
  388. # This is its own method so that it can be refactored later.
  389. # Google Document code should never directly check the wiki_type_id
  390. # and should instead always call is_google_doc.
  391. self.wiki_type_id == 4
  392. end
  393. #add a new participant to this assignment
  394. #manual addition
  395. # user_name - the user account name of the participant to add
  396. def add_participant(user_name)
  397. user = User.find_by_name(user_name)
  398. if (user == nil)
  399. raise "No user account exists with the name "+user_name+". Please <a href='"+url_for(:controller=>'users',:action=>'new')+"'>create</a> the user first."
  400. end
  401. participant = AssignmentParticipant.find_by_parent_id_and_user_id(self.id, user.id)
  402. if !participant
  403. newpart = AssignmentParticipant.create(:parent_id => self.id, :user_id => user.id, :permission_granted => user.master_permission_granted)
  404. newpart.set_handle()
  405. else
  406. raise "The user \""+user.name+"\" is already a participant."
  407. end
  408. end
  409. def create_node()
  410. parent = CourseNode.find_by_node_object_id(self.course_id)
  411. node = AssignmentNode.create(:node_object_id => self.id)
  412. if parent != nil
  413. node.parent_id = parent.id
  414. end
  415. node.save
  416. end
  417. def get_current_stage(topic_id=nil)
  418. if self.staggered_deadline?
  419. if topic_id.nil?
  420. return "Unknown"
  421. end
  422. end
  423. due_date = find_current_stage(topic_id)
  424. if due_date == nil or due_date == COMPLETE
  425. return COMPLETE
  426. else
  427. return DeadlineType.find(due_date.deadline_type_id).name
  428. end
  429. end
  430. def get_stage_deadline(topic_id=nil)
  431. if self.staggered_deadline?
  432. if topic_id.nil?
  433. return "Unknown"
  434. end
  435. end
  436. due_date = find_current_stage(topic_id)
  437. if due_date == nil or due_date == COMPLETE
  438. return due_date
  439. else
  440. return due_date.due_at.to_s
  441. end
  442. end
  443. def get_review_rounds
  444. due_dates = DueDate.find_all_by_assignment_id(self.id)
  445. rounds = 0
  446. for i in (0 .. due_dates.length-1)
  447. deadline_type = DeadlineType.find(due_dates[i].deadline_type_id)
  448. if deadline_type.name == "review" || deadline_type.name == "rereview"
  449. rounds = rounds + 1
  450. end
  451. end
  452. rounds
  453. end
  454. def find_current_stage(topic_id=nil)
  455. if self.staggered_deadline?
  456. due_dates = TopicDeadline.find(:all,
  457. :conditions => ["topic_id = ?", topic_id],
  458. :order => "due_at DESC")
  459. else
  460. due_dates = DueDate.find(:all,
  461. :conditions => ["assignment_id = ?", self.id],
  462. :order => "due_at DESC")
  463. end
  464. if due_dates != nil and due_dates.size > 0
  465. if Time.now > due_dates[0].due_at
  466. return COMPLETE
  467. else
  468. i = 0
  469. for due_date in due_dates
  470. if Time.now < due_date.due_at and
  471. (due_dates[i+1] == nil or Time.now > due_dates[i+1].due_at)
  472. return due_date
  473. end
  474. i = i + 1
  475. end
  476. end
  477. end
  478. end
  479. def assign_reviewers(mapping_strategy)
  480. #ACS Always assign reviewers for a team
  481. #removed check to see if it is a team assignment
  482. #defined in DynamicReviewMapping module
  483. assign_reviewers_for_team(mapping_strategy)
  484. end
  485. #this is for staggered deadline assignments or assignments with signup sheet
  486. def assign_reviewers_staggered(num_reviews,num_review_of_reviews)
  487. #defined in DynamicReviewMapping module
  488. message = assign_reviewers_automatically(num_reviews,num_review_of_reviews)
  489. return message
  490. end
  491. def get_current_due_date()
  492. due_date = self.find_current_stage()
  493. if due_date == nil or due_date == COMPLETE
  494. return COMPLETE
  495. else
  496. return due_date
  497. end
  498. end
  499. # Returns hash review_scores[reviewer_id][reviewee_id] = score
  500. def compute_reviews_hash
  501. review_questionnaire_id = get_review_questionnaire_id()
  502. @questions = Question.find(:all, :conditions =>["questionnaire_id = ?", review_questionnaire_id])
  503. @review_scores = Hash.new
  504. #ACS Removed the if condition(and corressponding else) which differentiate assignments as team and individual assignments
  505. # to treat all assignments as team assignments
  506. @response_type = "TeamReviewResponseMap"
  507. @myreviewers = ResponseMap.find(:all,:select => "DISTINCT reviewer_id", :conditions => ["reviewed_object_id = ? and type = ? ", self.id, @type] )
  508. @response_maps=ResponseMap.find(:all, :conditions =>["reviewed_object_id = ? and type = ?", self.id, @response_type])
  509. for response_map in @response_maps
  510. # Check if response is there
  511. @corresponding_response = Response.find(:first, :conditions =>["map_id = ?", response_map.id])
  512. @respective_scores = Hash.new
  513. if (@review_scores[response_map.reviewer_id] != nil)
  514. @respective_scores = @review_scores[response_map.reviewer_id]
  515. end
  516. if (@corresponding_response != nil)
  517. @this_review_score_raw = Score.get_total_score(:response => @corresponding_response, :questions => @questions, :q_types => Array.new)
  518. if(@this_review_score_raw >= 0.0)
  519. @this_review_score = ((@this_review_score_raw*100).round/100.0)
  520. end
  521. else
  522. @this_review_score = 0.0
  523. end
  524. @respective_scores[response_map.reviewee_id] = @this_review_score
  525. @review_scores[response_map.reviewer_id] = @respective_scores
  526. end
  527. return @review_scores
  528. end
  529. def get_review_questionnaire_id()
  530. @revqids = []
  531. @revqids = AssignmentQuestionnaire.find(:all, :conditions => ["assignment_id = ?",self.id])
  532. @revqids.each do |rqid|
  533. rtype = Questionnaire.find(rqid.questionnaire_id).type
  534. if( rtype == "ReviewQuestionnaire")
  535. @review_questionnaire_id = rqid.questionnaire_id
  536. end
  537. end
  538. return @review_questionnaire_id
  539. end
  540. def get_next_due_date()
  541. due_date = self.find_next_stage()
  542. if due_date == nil or due_date == COMPLETE
  543. return nil
  544. else
  545. return due_date
  546. end
  547. end
  548. def find_next_stage()
  549. due_dates = DueDate.find(:all,
  550. :conditions => ["assignment_id = ?", self.id],
  551. :order => "due_at DESC")
  552. if due_dates != nil and due_dates.size > 0
  553. if Time.now > due_dates[0].due_at
  554. return COMPLETE
  555. else
  556. i = 0
  557. for due_date in due_dates
  558. if Time.now < due_date.due_at and
  559. (due_dates[i+1] == nil or Time.now > due_dates[i+1].due_at)
  560. if (i > 0)
  561. return due_dates[i-1]
  562. else
  563. return nil
  564. end
  565. end
  566. i = i + 1
  567. end
  568. return nil
  569. end
  570. end
  571. end
  572. # Returns the number of reviewers assigned to a particular assignment
  573. def get_total_reviews_assigned
  574. self.response_maps.size
  575. end
  576. # get_total_reviews_assigned_by_type()
  577. # Returns the number of reviewers assigned to a particular assignment by the type of review
  578. # Param: type - String (ParticipantReviewResponseMap, etc.)
  579. def get_total_reviews_assigned_by_type(type)
  580. count = 0
  581. self.response_maps.each { |x| count = count + 1 if x.type == type}
  582. count
  583. end
  584. # Returns the number of reviews completed for a particular assignment
  585. def get_total_reviews_completed
  586. # TODO A bug in Rails http://dev.rubyonrails.org/ticket/4996 prevents us from using the proper syntax :
  587. # self.responses.size
  588. response_count = 0
  589. self.response_maps.each do |response_map|
  590. response_count = response_count + 1 unless response_map.response.nil?
  591. end
  592. response_count
  593. end
  594. # Returns the number of reviews completed for a particular assignment by type of review
  595. # Param: type - String (ParticipantReviewResponseMap, etc.)
  596. def get_total_reviews_completed_by_type(type)
  597. # TODO A bug in Rails http://dev.rubyonrails.org/ticket/4996 prevents us from using the proper syntax :
  598. # self.responses.size
  599. response_count = 0
  600. self.response_maps.each do |response_map|
  601. response_count = response_count + 1 if !response_map.response.nil? and response_map.type == type
  602. end
  603. response_count
  604. end
  605. # Returns the number of reviews completed for a particular assignment by type of review
  606. # Param: type - String (ParticipantReviewResponseMap, etc.)
  607. # Param: date - Filter reviews that were not created on this date
  608. def get_total_reviews_completed_by_type_and_date(type, date)
  609. # TODO A bug in Rails http://dev.rubyonrails.org/ticket/4996 prevents us from using the proper syntax :
  610. # self.responses.size
  611. response_count = 0
  612. self.response_maps.each do |response_map|
  613. if !response_map.response.nil? and response_map.type == type
  614. if (response_map.response.created_at.to_datetime.to_date <=> date) == 0 then
  615. response_count = response_count + 1
  616. end
  617. end
  618. end
  619. response_count
  620. end
  621. # Returns the percentage of reviews completed as an integer (0-100)
  622. def get_percentage_reviews_completed
  623. if get_total_reviews_assigned == 0 then 0
  624. else ((get_total_reviews_completed().to_f / get_total_reviews_assigned.to_f) * 100).to_i
  625. end
  626. end
  627. # Returns the average of all responses for this assignment as an integer (0-100)
  628. def get_average_score
  629. return 0 if get_total_reviews_assigned == 0
  630. sum_of_scores = 0
  631. self.response_maps.each do |response_map|
  632. if !response_map.response.nil? then
  633. sum_of_scores = sum_of_scores + response_map.response.get_average_score
  634. end
  635. end
  636. (sum_of_scores / get_total_reviews_completed).to_i
  637. end
  638. def get_score_distribution
  639. distribution = Array.new(101, 0)
  640. self.response_maps.each do |response_map|
  641. if !response_map.response.nil? then
  642. score = response_map.response.get_average_score.to_i
  643. distribution[score] += 1 if score >= 0 and score <= 100
  644. end
  645. end
  646. distribution
  647. end
  648. # Compute total score for this assignment by summing the scores given on all questionnaires.
  649. # Only scores passed in are included in this sum.
  650. def compute_total_score(scores)
  651. total = 0
  652. self.questionnaires.each do |questionnaire|
  653. total += questionnaire.get_weighted_score(self, scores)
  654. end
  655. return total
  656. end
  657. # Checks whether there are duplicate assignments of the same name by the same instructor.
  658. # If the assignments are assigned to courses, it's OK to have duplicate names in different
  659. # courses.
  660. def duplicate_name?
  661. if course
  662. Assignment.find(:all, :conditions => ['course_id = ? and instructor_id = ? and name = ?',
  663. course_id, instructor_id, name]).count > 1
  664. else
  665. Assignment.find(:all, :conditions => ['instructor_id = ? and name = ?',
  666. instructor_id, name]).count > 1
  667. end
  668. end
  669. def signed_up_topic(contributor)
  670. # The purpose is to return the topic that the contributor has signed up to do for this assignment.
  671. # Returns a record from the sign_up_topic table that gives the topic_id for which the contributor has signed up
  672. # Look for the topic_id where the creator_id equals the contributor id (contributor is a team or a participant)
  673. if !Team.find_by_name_and_id(contributor.name, contributor.id).nil?
  674. contributors_topic = SignedUpUser.find_by_creator_id(contributor.id)
  675. else
  676. contributors_topic = SignedUpUser.find_by_creator_id(contributor.user_id)
  677. end
  678. if !contributors_topic.nil?
  679. contributors_signup_topic = SignUpTopic.find_by_id(contributors_topic.topic_id)
  680. #returns the topic
  681. return contributors_signup_topic
  682. end
  683. end
  684. def self.export(csv, parent_id, options)
  685. @assignment = Assignment.find(parent_id)
  686. @questions = Hash.new
  687. questionnaires = @assignment.questionnaires
  688. questionnaires.each {
  689. |questionnaire|
  690. @questions[questionnaire.symbol] = questionnaire.questions
  691. }
  692. @scores = @assignment.get_scores(@questions)
  693. if(@scores[:teams].nil?)
  694. return csv
  695. end
  696. for index in 0 .. @scores[:teams].length - 1
  697. team = @scores[:teams][index.to_s.to_sym]
  698. for participant in team[:team].get_participants
  699. pscore = @scores[:participants][participant.id.to_s.to_sym]
  700. tcsv = Array.new
  701. tcsv << 'team'+index.to_s
  702. if (options["team_score"] == "true")
  703. if (team[:scores])
  704. tcsv.push(team[:scores][:max], team[:scores][:avg], team[:scores][:min], participant.fullname)
  705. else
  706. tcsv.push('---', '---', '---')
  707. end
  708. end
  709. if (options["submitted_score"])
  710. if (pscore[:review])
  711. tcsv.push(pscore[:review][:scores][:max], pscore[:review][:scores][:min], pscore[:review][:scores][:avg])
  712. else
  713. tcsv.push('---', '---', '---')
  714. end
  715. end
  716. if (options["metareview_score"])
  717. if (pscore[:metareview])
  718. tcsv.push(pscore[:metareview][:scores][:max], pscore[:metareview][:scores][:min], pscore[:metareview][:scores][:avg])
  719. else
  720. tcsv.push('---', '---', '---')
  721. end
  722. end
  723. if (options["author_feedback_score"])
  724. if (pscore[:feedback])
  725. tcsv.push(pscore[:feedback][:scores][:max], pscore[:feedback][:scores][:min], pscore[:feedback][:scores][:avg])
  726. else
  727. tcsv.push('---', '---', '---')
  728. end
  729. end
  730. if (options["teammate_review_score"])
  731. if (pscore[:teammate])
  732. tcsv.push(pscore[:teammate][:scores][:max], pscore[:teammate][:scores][:min], pscore[:teammate][:scores][:avg])
  733. else
  734. tcsv.push('---', '---', '---')
  735. end
  736. end
  737. tcsv.push(pscore[:total_score])
  738. csv << tcsv
  739. end
  740. end
  741. end
  742. def self.get_export_fields(options)
  743. fields = Array.new
  744. fields << "Team Name"
  745. if (options["team_score"] == "true")
  746. fields.push("Team Max", "Team Avg", "Team Min")
  747. end
  748. if (options["submitted_score"])
  749. fields.push("Submitted Max", "Submitted Avg", "Submitted Min")
  750. end
  751. if (options["metareview_score"])
  752. fields.push("Metareview Max", "Metareview Avg", "Metareview Min")
  753. end
  754. if (options["author_feedback_score"])
  755. fields.push("Author Feedback Max", "Author Feedback Avg", "Author Feedback Min")
  756. end
  757. if (options["teammate_review_score"])
  758. fields.push("Teammate Review Max", "Teammate Review Avg", "Teammate Review Min")
  759. end
  760. fields.push("Final Score")
  761. return fields
  762. end
  763. end