File Coverage

blib/lib/Grades.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             package Grades;
2             {
3             $Grades::VERSION = '0.16';
4             }
5              
6             #Last Edit: 2014 2月 15, 16時23分02秒
7             #$Id: Grades.pm 1960 2014-02-15 08:27:09Z drbean $
8              
9 2     2   108486 use MooseX::Declare;
  0            
  0            
10              
11             package Grades::Script;
12             {
13             $Grades::Script::VERSION = '0.16';
14             }
15             use Moose;
16             with 'MooseX::Getopt';
17              
18             has 'man' => (is => 'ro', isa => 'Bool');
19             has 'help' => (is => 'ro', isa => 'Bool');
20             has 'league' => (metaclass => 'Getopt', is => 'ro', isa => 'Str',
21             cmd_flag => 'l',);
22             has 'exam' => ( metaclass => 'Getopt', is => 'ro', isa => 'Str',
23             cmd_flag => 'e',);
24             has 'session' => ( metaclass => 'Getopt', is => 'ro', isa => 'Str',
25             cmd_flag => 's',);
26             has 'beancan' => ( metaclass => 'Getopt', is => 'ro', isa => 'Int',
27             cmd_flag => 'n',);
28             has 'tables' => ( metaclass => 'Getopt', is => 'ro', isa => 'Str',
29             cmd_flag => 'g',);
30              
31              
32             has 'round' => ( metaclass => 'Getopt', is => 'ro', isa => 'Str',
33             cmd_flag => 'r',);
34              
35             # letters2score.pl
36             has 'exercise' => ( metaclass => 'Getopt', is => 'ro', isa => 'Str',
37             cmd_flag => 'x',);
38             has 'one' => ( metaclass => 'Getopt', is => 'ro', isa => 'Str',
39             cmd_flag => 'o',);
40             has 'two' => ( metaclass => 'Getopt', is => 'ro', isa => 'Str',
41             cmd_flag => 't',);
42              
43             has 'weights' => (metaclass => 'Getopt', is => 'ro', isa => 'Str',
44             cmd_flag => 'w',);
45             has 'player' => (metaclass => 'Getopt', is => 'ro', isa => 'Str',
46             cmd_flag => 'p',);
47              
48             package Grades;
49              
50             =head1 NAME
51              
52             Grades - A collocation of homework, classwork and exams
53              
54             =head1 SYNOPSIS
55              
56             use Grades;
57              
58             my $script = Grades::Script->new_with_options( league => getcwd );
59             my $league = League->new( id => $script->league );
60             my $grades = Grades->new( league => $league );
61              
62             $league->approach->meta->apply( $grades );
63             my $classworkgrades = $grades->classwork;
64             my $homeworkgrades = $grades->homework;
65             my $examgrades = $grades->examGrade;
66              
67             =head1 DESCRIPTION
68              
69             An alternative to a spreadsheet for grading students, using YAML files and scripts. The students are the players in a league ( class.) See the README and example emile league in t/emile in the distribution for the layout of the league directory in which homework, classwork and exam scores are recorded.
70              
71             Grades are a collocation of Classwork, Homework and Exams roles, but the Classwork role 'delegates' its methods to one of a number of approaches, each of which has a 'total' and 'totalPercent' method. Current approaches, or forms of curriculum, include Compcomp, Groupwork and Jigsaw.
72              
73             Keywords: gold stars, token economies, bean counter
74              
75             =cut
76              
77             =head1 ATTRIBUTES & METHODS
78              
79             =cut
80              
81             =head2 LEAGUE CLASS
82              
83             =cut
84              
85             class League {
86             use YAML qw/LoadFile DumpFile/;
87             use List::MoreUtils qw/any/;
88             use Grades::Types qw/PlayerName PlayerNames Members/;
89             use Try::Tiny;
90             use Carp;
91              
92             =head3 leagues
93              
94             The path to the league directory.
95              
96             =cut
97              
98             has 'leagues' => (is => 'ro', isa => 'Str', required => 1, lazy => 1,
99             default => '/home/drbean/022' );
100              
101             =head3 id
102              
103             Actually, it's a path to the league directory, below the $grades->leagues dir.
104              
105             =cut
106              
107             has 'id' => (is => 'ro', isa => 'Str', required => 1);
108              
109             =head3 yaml
110              
111             The content of the league configuration file.
112              
113             =cut
114              
115             has 'yaml' => (is => 'ro', isa => 'HashRef', lazy_build => 1);
116             method _build_yaml {
117             my $leaguedirs = $self->leagues;
118             my $league = $self->id;
119             $self->inspect( "$leaguedirs/$league/league.yaml" );
120             }
121              
122             =head3 name
123              
124             The name of the league (class).
125              
126             =cut
127              
128             has 'name' => (is => 'ro', isa => 'Str', lazy_build => 1);
129             method _build_name {
130             my $data = $self->yaml;
131             $data->{league};
132             }
133              
134              
135             =head3 field
136              
137             The field of the league (class). What is the subject or description, the area of endeavor?
138              
139             =cut
140              
141             has 'field' => (is => 'ro', isa => 'Str', lazy_build => 1);
142             method _build_field {
143             my $data = $self->yaml;
144             $data->{field};
145             }
146              
147              
148             =head3 approach
149              
150             The style of classwork competition, eg Compcomp, or Groupwork. This is the name of the class (think OOP) to which 'classwork' and other methods are delegated.
151              
152             =cut
153              
154             has 'approach' => (is => 'ro', isa => 'Str', lazy => 1,
155             default => sub { shift->yaml->{approach} } );
156              
157             =head3 members
158              
159             Hash refs of the players (students) in the league. The module assumes each of the members in the arrayref returned by this attribute is a hash ref containing an id and name of the member.
160              
161             =cut
162              
163             has 'members', is => 'ro', isa => Members, lazy_build => 1;
164             method _build_members {
165             my $data = $self->yaml;
166             $data->{member};
167             }
168              
169             =head3 session
170              
171             The first week in each session, like { 1 => 1, 2 => 5, 3 => 10, 4 => 14 }, monotonically increasing week numbers.
172              
173             =cut
174              
175             has 'session', (is => 'ro', isa => 'HashRef',
176             lazy => 1, default => sub { shift->yaml->{session} } );
177              
178              
179             =head3 absentees
180              
181             Students who have stopped coming to class and so won't be included in classwork scoring.
182              
183             =cut
184              
185             has 'absentees', (is => 'ro', isa => PlayerNames,
186             lazy => 1, default => sub { shift->yaml->{out} } );
187              
188              
189             =head3 transfer
190              
191             $oldleague = $newleague->transfer->{V9731059}
192              
193             Players who have transferred to this league from some other league at some point and the leagues they transferred from.
194              
195             =cut
196              
197             has 'transfer', (is => 'ro', isa => 'HashRef',
198             lazy => 1, default => sub { shift->yaml->{transfer} } );
199              
200              
201             =head3 is_member
202              
203             Whether the passed id is that of a member in the league (class).
204              
205             =cut
206              
207             method is_member (Str $id) {
208             my $data = $self->yaml;
209             any { $_->{id} eq $id } @{$data->{member}};
210             }
211              
212              
213             =head3 ided
214              
215             The id of the member with the given player name.
216              
217             =cut
218              
219             method ided( Str $player) {
220             my $members = $self->members;
221             my %ids = map { $_->{id} => $_->{name} }
222             grep { $_->{name} eq $player } @$members;
223             my @ids = keys %ids;
224             my @names = values %ids;
225             local $" = ', ';
226             carp @ids . " players named @names, with ids: @ids," unless @ids==1;
227             if ( @ids == 1 ) { return $ids[0] }
228             else { return $ids{$player}; }
229             }
230              
231             =head3 inspect
232              
233             Loads a YAML file.
234              
235             =cut
236              
237             method inspect (Str $file) {
238             my ($warning, $data);
239             try { $data = LoadFile $file }
240             catch { carp "Couldn't open $file," };
241             return $data;
242             }
243              
244             =head3 save
245              
246             Dumps a YAML file
247              
248             =cut
249              
250             method save (Str $file, HashRef $data) {
251             try { DumpFile $file, $data }
252             catch { warn "Couldn't save $data to $file," };
253             }
254              
255             }
256              
257              
258             =head2 PLAYER CLASS
259              
260             =cut
261              
262             class Player {
263             use List::MoreUtils qw/firstval/;
264             use List::Util qw/sum/;
265             use POSIX;
266              
267             =head3 league
268              
269             The league the player is in. This is required.
270              
271             =cut
272              
273             has 'league' => (is => 'ro', isa => 'League', required => 1);
274              
275             =head3 id
276              
277             The id of the player. This is required.
278              
279             =cut
280              
281             has 'id' => (is => 'ro', isa => 'Str', required => 1);
282              
283             =head3 id
284              
285             The name of the player.
286              
287             =cut
288              
289             has 'name' => (is => 'ro', isa => 'Str', lazy_build => 1);
290             method _build_name {
291             my $league = $self->league;
292             my $id = $self->id;
293             my $members = $league->members;
294             my $member = firstval { $_->{id} eq $id } @$members;
295             $member->{name};
296             }
297              
298             has 'Chinese' => (is => 'ro', isa => 'Str');
299             }
300              
301              
302             =head2 NONENTITY CLASS
303              
304             =cut
305              
306             class Nonentity extends Player {
307              
308             =head3 name
309              
310             The name is 'Bye'. The id is too, as a matter of fact.
311              
312             =cut
313              
314             has 'name' => (is => 'ro', isa => 'Str', required => 1 );
315              
316             }
317              
318              
319             =head2 GRADES CLASS
320              
321             =head2 Grades' Homework Methods
322             =cut
323              
324             role Homework {
325             use YAML qw/LoadFile DumpFile/;
326             use List::Util qw/min sum/;
327             use Scalar::Util qw/looks_like_number/;
328             use Carp;
329             use Grades::Types qw/PlayerId HomeworkResult HomeworkRound HomeworkRounds
330             RoundsResults/;
331              
332             =head3 hwdir
333              
334             The directory where the homework is.
335              
336             =cut
337              
338             has 'hwdir' => (is => 'ro', isa => 'Str', lazy_build => 1);
339             method _build_hwdir {
340             my $league = $self->league->id;
341             my $leaguedir = $self->league->leagues . "/" . $league;
342             my $basename = shift->league->yaml->{hw} || "exams";
343             my $hwdir = $leaguedir . '/' . $basename;
344             }
345              
346             =head3 rounds
347              
348             An arrayref of the rounds for which there are homework grades for players in the league, in round order, of the form, [1, 3 .. 7, 9 ..].
349              
350             =cut
351              
352             has 'rounds', (is => 'ro', isa => 'ArrayRef[Int]', lazy_build => 1);
353             method _build_rounds {
354             my $hwdir = $self->hwdir;
355             my @hw = glob "$hwdir/*.yaml";
356             [ sort {$a<=>$b} map m/^$hwdir\/(\d+)\.yaml$/, @hw ];
357             }
358              
359             =head3 roundIndex
360              
361             Given a round name (ie number), returns the ordinal position in which this round was played, with the first round numbered 0. Returns undef if the round was not played.
362              
363             =cut
364              
365             method roundIndex (Int $round) {
366             my $rounds = $self->rounds;
367             my $n = 0;
368             for ( @$rounds ) {
369             return $n if $_ eq $round;
370             $n++;
371             }
372             }
373              
374             =head3 roundfiles
375              
376             An hashref of the files with data for the rounds for which there are homework grades for players in the league, keyed on rounds.
377              
378             =cut
379              
380             has 'roundfiles', (is => 'ro', isa => 'HashRef[ArrayRef]', lazy_build => 1);
381             method _build_roundfiles {
382             my $hwdir = $self->hwdir;
383             my @hw = glob "$hwdir/*.yaml";
384             my @rounds = map m/^$hwdir\/(\d+)\.yaml$/, @hw;
385             +{ map { $_ => [ glob "$hwdir/${_}*.yaml" ] } @rounds }
386             }
387              
388             =head3 hwbyround
389              
390             A hashref of the homework grades for players in the league for each round.
391              
392             =cut
393              
394             has 'hwbyround', (is => 'ro', isa => RoundsResults, lazy_build => 1);
395             method _build_hwbyround {
396             my $hwdir = $self->hwdir;
397             my $rounds = $self->rounds;
398             my %results =
399             map { $_ => $self->inspect("$hwdir/$_.yaml") } @$rounds;
400             my %grades = map { $_ => $results{$_}{grade} } @$rounds;
401             return \%grades;
402             }
403              
404             =head3 hwMax
405              
406             The highest possible score in the homework
407              
408             =cut
409              
410             has 'hwMax' => (is => 'ro', isa => 'Int', lazy => 1, default =>
411             sub { shift->league->yaml->{hwMax} } );
412              
413             =head3 totalMax
414              
415             The total maximum points that a Player could have gotten to this point in the whole season. There may be more (or fewer) rounds played than expected, so the actual top possible score returned by totalMax may be more (or less) than the figure planned.
416              
417             =cut
418              
419             has 'totalMax' => (is => 'ro', isa => 'Int', lazy_build => 1);
420             method _build_totalMax {
421             my $rounds = $self->rounds;
422             my $hwMax = $self->hwMax;
423             $hwMax * @$rounds;
424             }
425              
426             =head3 rawscoresinRound
427              
428             Given a round, returns a hashref of the raw scores for that round, keyed on the names of the exercises. These are in files in the hwdir with names of the form ^\d+[_.]\w+\.yaml$
429              
430             =cut
431              
432             method rawscoresinRound (Int $round) {
433             my $hwdir = $self->hwdir;
434             my $files = $self->roundfiles->{$round};
435             my @ex = map m/^$hwdir\/$round([_.]\w+)\.yaml$/, @$files;
436             my $results = $self->inspect("$hwdir/$round.yaml");
437             return { $results->{exercise} => $results->{points} };
438             }
439              
440             =head3 hwforid
441              
442             Given a player's id, returns an array ref of the player's hw scores.
443              
444             =cut
445              
446             method hwforid( PlayerId $id) {
447             my $leagueId = $self->league->id;
448             my $hw = $self->hwbyround;
449             my $rounds = $self->rounds;
450             my @hwbyid;
451             for my $round (@$rounds) {
452             unless ( $hw->{$round} ) {
453             warn "No homework results in Round $round in $leagueId league";
454             next;
455             }
456             my $grade = $hw->{$round}->{$id};
457             if ( defined $grade and looks_like_number( $grade ) ) {
458             push @hwbyid, $grade;
459             }
460             elsif ( defined $grade and $grade =~ m/transfer/i ) {
461             my $oldleagueId = $self->league->transfer->{$id};
462             my $league = League->new( id => $oldleagueId );
463             my $grades = Grades->new({ league => $league });
464             my $transfergrade = $grades->hwbyround->{$round}->{$id};
465             warn
466             "$id transfered from $oldleagueId league but no homework there in round $round"
467             unless defined $transfergrade;
468             push @hwbyid, $transfergrade || 0;
469             }
470             else {
471             warn "No homework result for $id in Round $round in $leagueId league\n";
472             }
473             }
474             \@hwbyid;
475             }
476              
477             =head3 hwforidasHash
478              
479             Given a player's id, returns an hashref of the player's hw grades, keyed on the rounds.
480              
481             =cut
482              
483             method hwforidasHash (PlayerId $id) {
484             my $hw = $self->hwforid( $id );
485             my $rounds = $self->rounds;
486             my %hwbyid;
487             for my $i ( 0 .. $#$rounds ) {
488             my $round = $rounds->[$i];
489             $hwbyid{$round} = $hw->[$i];
490             if ( not defined $hw->[$i] ) { warn
491             "No homework result for $id in Round $round\n";}
492             }
493             \%hwbyid;
494             }
495              
496             =head3 homework
497              
498             Running total homework scores of the league.
499              
500             =cut
501              
502             method homework {
503             my $league = $self->league;
504             my $leagueId = $league->id;
505             my $players = $league->members;
506             my %players = map { $_->{id} => $_ } @$players;
507             my %idtotals;
508             for my $player ( keys %players ) {
509             my $homework = $self->hwforid( $player );
510             my $total = sum @$homework;
511             $idtotals{$player} = $total;
512             }
513             +{ map { $_ => $idtotals{$_} || 0 } keys %idtotals };
514             }
515              
516             =head3 homeworkPercent
517              
518             Running total homework scores of the league as percentages of the totalMax to that point, with a maximum of 100.
519              
520             =cut
521              
522             method homeworkPercent {
523             my $league = $self->league->id;
524             my $totalMax = $self->totalMax;
525             my $idtotals = $self->homework;
526             my %percent;
527             if ( $totalMax == 0 ) {
528             $percent{$_} = 0 for keys %$idtotals;
529             }
530             else {
531             %percent = map {
532             $_ => min( 100, 100 * $idtotals->{$_} / $totalMax )
533             || 0 } keys %$idtotals;
534             }
535             return \%percent;
536             }
537             }
538              
539              
540             =head2 Grades' Jigsaw Methods
541              
542             The jigsaw is a cooperative learning activity where all the players in a group get different information that together produces the 'big picture', and where they are each held responsible for the understanding of each of the other individual members of this big picture.
543              
544             =cut
545              
546             role Jigsaw {
547             use List::MoreUtils qw/any all/;
548             use Try::Tiny;
549             use Moose::Autobox;
550              
551             =head3 jigsawdirs
552              
553             The directory where the jigsaws are.
554              
555             =cut
556              
557             has 'jigsawdirs' => (is => 'ro', isa => 'Str', lazy_build => 1);
558             method _build_jigsawdirs {
559             my $league = $self->league->id;
560             my $leaguedir = $self->league->leagues . "/" . $league;
561             my $basename = shift->league->yaml->{jigsaw} || "exam";
562             my $jigsawdir = $leaguedir .'/' . $basename;
563             }
564              
565             =head3 config
566              
567             The round.yaml file with data about the jigsaw activity in the given round (directory.)
568              
569             =cut
570              
571             method config( Str $round) {
572             my $jigsaws = $self->jigsawdirs;
573             my $config;
574             try { $config = $self->inspect("$jigsaws/$round/round.yaml") }
575             catch { warn "No config file for $jigsaws/$round jigsaw" };
576             return $config;
577             }
578              
579             =head3 topic
580              
581             The topic of the quiz in the given jigsaw for the given group.
582              
583             =cut
584              
585             method topic ( Str $jigsaw, Str $group ) {
586             my $config = $self->config('Jigsaw', $jigsaw);
587             my $activity = $config->{activity};
588             for my $topic ( keys %$activity ) {
589             my $forms = $activity->{$topic};
590             for my $form ( keys %$forms ) {
591             my $tables = $forms->{$form};
592             return $topic if any { $_ eq $group } @$tables;
593             }
594             }
595             return;
596             }
597              
598             =head3 form
599              
600             The form of the quiz in the given jigsaw for the given group.
601              
602             =cut
603              
604             method form ( Str $jigsaw, Str $group ) {
605             my $config = $self->config('Jigsaw', $jigsaw);
606             my $activity = $config->{activity};
607             for my $topic ( keys %$activity ) {
608             my $forms = $activity->{$topic};
609             for my $form ( keys %$forms ) {
610             my $tables = $forms->{$form};
611             return $form if any { $_ eq $group } @$tables;
612             }
613             }
614             return;
615             }
616              
617             =head3 quizfile
618              
619             The file system location of the file with the quiz questions and answers for the given jigsaw.
620              
621             =cut
622              
623             method quizfile ( Str $jigsaw ) {
624             my $config = $self->config('Jigsaw', $jigsaw);
625             return $config->{text};
626             }
627              
628             =head3 quiz
629              
630             The quiz questions (as an anon array) in the given jigsaw for the given group.
631              
632             =cut
633              
634             method quiz ( Str $jigsaw, Str $group ) {
635             my $quizfile = $self->quizfile($jigsaw);
636             my $activity;
637             try { $activity = $self->inspect( $quizfile ) }
638             catch { warn "No $quizfile jigsaw content file" };
639             my $topic = $self->topic( $jigsaw, $group );
640             my $form = $self->form( $jigsaw, $group );
641             my $quiz = $activity->{$topic}->{jigsaw}->{$form}->{quiz};
642             }
643              
644             =head3 options
645              
646             $grades->options( '2/1', 'Purple', 0 ) # [ qw/Deborah Don Dovonna Sue/ ]
647              
648             The options (as an anon array) to the given question in the given jigsaw for the given group.
649              
650             =cut
651              
652             method options ( Str $jigsaw, Str $group, Int $question ) {
653             my $quiz = $self->quiz( $jigsaw, $group );
654             my $options = $quiz->[$question]->{option};
655             return $options || '';
656             }
657              
658             =head3 qn
659              
660             The number of questions in the given jigsaw for the given group.
661              
662             =cut
663              
664             method qn ( Str $jigsaw, Str $group ) {
665             my $quiz = $self->quiz( $jigsaw, $group );
666             warn "No quiz for $group group in jigsaw $jigsaw," unless $quiz;
667             return scalar @$quiz;
668             }
669              
670             =head3 responses
671              
672             The responses of the members of the given group in the given jigsaw (as an anon hash keyed on the ids of the members). In a file in the jigsaw directory called 'response.yaml'.
673              
674             =cut
675              
676              
677             method responses ( Str $jigsaw, Str $group ) {
678             my $jigsaws = $self->jigsawdirs;
679             my $responses = $self->inspect( "$jigsaws/$jigsaw/response.yaml" );
680             return $responses->{$group};
681             }
682              
683             =head3 jigsawGroups
684              
685             A hash ref of all the groups in the given jigsaw and the names of members of the groups, keyed on groupnames. There may be duplicated names if one player did the activity twice as an 'assistant' for a group with not enough players, and missing names if a player did not do the quiz.
686              
687             =cut
688              
689             method jigsawGroups (Str $jigsaw ) {
690             my $config = $self->config('Jigsaw', $jigsaw );
691             $config->{group};
692             }
693              
694             =head3 jigsawGroupMembers
695              
696             An array (was hash ref) of the names of the members of the given group in the given jigsaw, in order of the roles, A..D.
697              
698             =cut
699              
700             method jigsawGroupMembers (Str $jigsaw, Str $group) {
701             my $groups = $self->jigsawGroups( $jigsaw );
702             my $members = $groups->{$group};
703             }
704              
705             =head3 roles
706              
707             At the moment, just A .. D.
708              
709             =cut
710              
711             has 'roles' => (is => 'ro', isa => 'ArrayRef[Str]',
712             default => sub { [ qw/A B C D/ ] } );
713              
714              
715             =head3 idsbyRole
716              
717             Ids in array, in A-D role order
718              
719             =cut
720              
721              
722             method idsbyRole ( Str $jigsaw, Str $group ) {
723             my $members = $self->league->members;
724             my %namedMembers = map { $_->{name} => $_ } @$members;
725             my $namesbyRole = $self->jigsawGroupMembers( $jigsaw, $group );
726             my @idsbyRole = map { $namedMembers{$_}->{id} } @$namesbyRole;
727             return \@idsbyRole;
728             }
729              
730             =head3 assistants
731              
732             A array ref of all the players in the (sub)jigsaw who did the the activity twice to 'assist' groups with not enough (or absent) players, or individuals with no groups, or people who arrived late.
733              
734             =cut
735              
736             method assistants (Str $jigsaw) {
737             my $round = $self->config( $jigsaw );
738             $round->{assistants};
739             }
740              
741             =head3 jigsawGroupRole
742              
743             An hash ref of the roles of the members of the given group in the given jigsaw, keyed on the name of the player.
744              
745             =cut
746              
747             method jigsawGroupRole (Str $jigsaw, Str $group) {
748             my $members = $self->jigsawGroupMembers( $jigsaw, $group );
749             my %roles;
750             @roles{ @$members } = $self->roles->flatten;
751             return \%roles;
752             }
753              
754             =head3 id2jigsawGroupRole
755              
756             An hash ref of the roles of the members of the given group in the given jigsaw, keyed on the id of the player.
757              
758             =cut
759              
760             method id2jigsawGroupRole (Str $jigsaw, Str $group) {
761             my $members = $self->jigsawGroupMembers( $jigsaw, $group );
762             my @ids = map { $self->league->ided($_) } @$members;
763             my $roles = $self->roles;
764             my %id2role; @id2role{@ids} = @$roles;
765             return \%id2role;
766             }
767              
768             =head3 name2jigsawGroup
769              
770             An array ref of the group(s) to which the given name belonged in the given jigsaw. Normally, the array ref has only one element. But if the player was an assistant an array ref of more than one group is returned. If the player did not do the jigsaw, no groups are returned.
771              
772             =cut
773              
774             method name2jigsawGroup (Str $jigsaw, Str $name) {
775             my $groups = $self->jigsawGroups( $jigsaw );
776             my @memberships;
777             for my $id ( keys %$groups ) {
778             my $group = $groups->{$id};
779             push @memberships, $id if any { $_ eq $name } @$group;
780             }
781             return \@memberships;
782             }
783              
784             =head3 rawJigsawScores
785              
786             The individual scores on the given quiz of each member of the given group, keyed on their roles, no, ids, from the file called 'scores.yaml' in the given jigsaw dir. If the scores in that file have a key which is a role, handle that, but, yes, the keys of the hashref returned here are the players' ids.
787              
788             =cut
789              
790             method rawJigsawScores (Str $round, Str $group) {
791             my $data;
792             my $jigsaws = $self->jigsawdirs;
793             try { $data = $self->inspect( "$jigsaws/$round/scores.yaml"); }
794             catch { warn "No scores for $group group in jigsaw $round."; };
795             my $groupdata = $data->{letters}->{$group};
796             my $ids = $self->idsbyRole( $round, $group );
797             my $roles = $self->roles;
798             my @keys;
799             if (
800             any { my $key = $_; any { $_ eq $key } @$roles; } keys %$groupdata
801             ) {
802             @keys = @$roles;
803             }
804             else {
805             @keys = grep { my $id = $_; any { $_ eq $id } @$ids }
806             keys %$groupdata;
807             }
808             my %scores;
809             @scores{@keys} = @{$groupdata}{@keys};
810             return \%scores;
811             }
812              
813             =head3 chinese
814              
815             The number of times Chinese was used in the given round by all the groups. If there is no record of Chinese use, returns values of 0.
816              
817             =cut
818              
819             method chinese (Str $round) {
820             my $data;
821             my $jigsaws = $self->jigsawdirs;
822             try { $data = $self->inspect( "$jigsaws/$round/scores.yaml"); }
823             catch { warn "No scores in jigsaw $round."; };
824             my $chinese = $data->{Chinese};
825             my $groups = $self->jigsawGroups( $round );
826             $chinese->{ $_ } ||= 0 for keys %$groups;
827             return $chinese;
828             }
829              
830             =head3 jigsawDeduction
831              
832             Points deducted for undesirable performance elements (ie Chinese use) on the quiz of the given group in the given exam.
833              
834             =cut
835              
836             method jigsawDeduction (Str $jigsaw, Str $group) {
837             my $data;
838             my $jigsaws = $self->jigsawdirs;
839             try { $data = $self->inspect( "$jigsaws/$jigsaw/scores.yaml" ); }
840             catch { warn
841             "Deductions for $group group in $jigsaw jigsaw?" };
842             my $demerits = $data->{Chinese}->{$group};
843             return $demerits;
844             }
845              
846             }
847              
848              
849             =head2 Grades' Classwork Methods
850              
851             Classwork is work done in class with everyone and the teacher present. Two classwork approaches are Compcomp and Groupwork. Others are possible. Depending on the league's approach accessor, the methods are delegated to the appropriate Approach object.
852              
853             =cut
854              
855             class Classwork {
856             use Grades::Types qw/Results/;
857              
858             =head3 approach
859              
860             Delegatee handling classwork_total, classworkPercent
861              
862             =cut
863              
864             has 'approach' => ( is => 'ro', isa => 'Approach', required => 1,
865             handles => [ qw/
866             series beancans
867             all_events points
868             classwork_total classworkPercent / ] );
869              
870             }
871              
872             =head2 Classwork Approach
873              
874             Handles Classwork's classwork_total and classworkPercent methods. Calls the total or totalPercent methods of the class whose name is in the 'type' accessor.
875              
876             =cut
877              
878             class Approach {
879              
880             =head3 league
881              
882             The league (object) whose approach this is.
883              
884             =cut
885              
886             has 'league' => (is =>'ro', isa => 'League', required => 1,
887             handles => [ 'inspect' ] );
888              
889             =head3 groupworkdirs
890              
891             The directory under which there are subdirectories containing data for the group/pair-work sessions. Look first in 'groupwork', then 'compcomp' mappings, else use 'classwork' dir.
892              
893             =cut
894              
895             has 'groupworkdirs' => (is => 'ro', isa => 'Str', lazy_build => 1);
896             method _build_groupworkdirs {
897             my $league = $self->league;
898             my $id = $league->id;
899             my $leaguedir = $self->league->leagues . "/" . $id;
900             my $basename = $league->yaml->{groupwork} ||
901             $league->yaml->{compcomp} || "classwork";
902             my $groupworkdirs = $leaguedir .'/' . $basename;
903             }
904              
905             =head3 series
906              
907             The sessions (weeks) over the series (semester) in each of which there was a different grouping and results of players. This method returns an arrayref of the names (numbers) of the sessions, in numerical order, of the form, [1, 3 .. 7, 9, 10 .. 99 ]. Results are in sub directories of the same name, under groupworkdirs.
908              
909             =cut
910              
911             has 'series' =>
912             ( is => 'ro', isa => 'Maybe[ArrayRef[Int]]', lazy_build => 1 );
913             method _build_series {
914             my $dir = $self->groupworkdirs;
915             my @subdirs = grep { -d } glob "$dir/*";
916             [ sort { $a <=> $b } map m/^$dir\/(\d+)$/, @subdirs ];
917             }
918              
919             #=head3 all_events
920             #
921             #All the weeks, or sessions or lessons for which grade data is being assembled from for the grade component.
922             #
923             #=cut
924             #
925             # method all_events {
926             # my $league = $self->league;
927             # my $type = $league->approach;
928             # my $meta = $type->meta;
929             # my $total = $type->new( league => $league )->all_events;
930             # }
931             #
932             #=head3 points
933             #
934             #Week-by-weeks, or session scores for the individual players in the league.
935             #
936             #=cut
937             #
938             # method points (Str $week) {
939             # my $league = $self->league;
940             # my $type = $league->approach;
941             # my $meta = $type->meta;
942             # my $total = $type->new( league => $league )->points( $week );
943             # }
944             #
945             #=head3 classwork_total
946             #
947             #Calls the pluginned approach's classwork_total.
948             #
949             #=cut
950             #
951             # method classwork_total {
952             # my $league = $self->league;
953             # my $type = $league->approach;
954             # my $total = $type->new( league => $league )->total;
955             # }
956             #
957             =head3 classworkPercent
958              
959             Calls the pluginned approach's classworkPercent.
960              
961             =cut
962              
963             method classworkPercent {
964             my $league = $self->league;
965             my $type = $league->approach;
966             my $total = $type->new( league => $league )->totalPercent;
967             }
968             }
969              
970              
971             =head2 Grades' Compcomp Methods
972              
973             The comprehension question competition is a Swiss tournament regulated 2-partner conversation competition where players try to understand more of their opponent's information than their partners understand of theirs.
974              
975             =cut
976              
977             class Compcomp extends Approach {
978             use Try::Tiny;
979             use Moose::Autobox;
980             use List::Util qw/max min/;
981             use List::MoreUtils qw/any all/;
982             use Carp qw/carp/;
983             use Grades::Types qw/Results/;
984              
985             =head3 compcompdirs
986              
987             The directory under which there are subdirectories containing data for the Compcomp rounds.
988              
989             =cut
990              
991             has 'compcompdirs' => (is => 'ro', isa => 'Str', lazy_build => 1 );
992             method _build_compcompdirs {
993             my $leaguedir = $self->league->leagues . "/" . $self->league->id;
994             my $compcompdir = $leaguedir .'/' . shift->league->yaml->{compcomp};
995             }
996              
997             =head3 all_events
998              
999             The pair conversations over the series (semester). This method returns an arrayref of the numbers of the conversations, in numerical order, of the form, [1, 3 .. 7, 9, 10 .. 99 ]. Results are in sub directories of the same name, under compcompdirs.
1000              
1001             =cut
1002              
1003             has 'all_events' =>
1004             ( is => 'ro', isa => 'Maybe[ArrayRef[Int]]', lazy_build => 1 );
1005             method _build_all_events {
1006             my $dir = $self->compcompdirs;
1007             my @subdirs = grep { -d } glob "$dir/*";
1008             [ sort { $a <=> $b } map m/^$dir\/(\d+)$/, @subdirs ];
1009             }
1010              
1011             =head3 config
1012              
1013             The round.yaml file with data about the Compcomp activity for the given conversation (directory.)
1014              
1015             =cut
1016              
1017             method config( Str $round) {
1018             my $comp = $self->compcompdirs;
1019             my $file = "$comp/$round/round.yaml";
1020             my $config;
1021             try { $config = $self->inspect($file) }
1022             catch { warn "No config file for Compcomp round $round at $file" };
1023             return $config;
1024             }
1025              
1026             =head3 activities
1027              
1028             The activities which individual tables did in the given round. Keys are topics, keyed are forms. These, in turn, are keys of tables doing those topics and those forms.
1029              
1030             =cut
1031              
1032             method activities( Str $round ) {
1033             my $config = $self->config( $round );
1034             return $config->{activity};
1035             }
1036              
1037             =head3 tables
1038              
1039             The tables with players according to their roles for the given round, as an hash ref. In the 'group' or 'activities' mapping in the config file. Make sure each table has a unique table number. Some code here is same as in Swiss's round_table.pl and dblineup.rc.
1040              
1041             activities:
1042             drbean:
1043             1:
1044             - U9931007
1045             - U9933022
1046             novak:
1047             1:
1048             - U9931028
1049             - U9933045
1050              
1051             =cut
1052              
1053             method tables ( Str $round ) {
1054             my $config = $self->config($round);
1055             my (@pairs, %pairs, @dupes, $wantlist);
1056             my $groups = $config->{group};
1057             return $groups if $groups;
1058             my $activities = $config->{activity};
1059             for my $key ( keys %$activities ) {
1060             my $topic = $activities->{$key};
1061             for my $form ( keys %$topic ) {
1062             my $pairs = $topic->{$form};
1063             if ( ref( $pairs ) eq 'ARRAY' ) {
1064             $wantlist = 1;
1065             for my $pair ( @$pairs ) {
1066             my @players = values %$pair;
1067             my @roles = keys %$pair;
1068             push @pairs, $pair unless
1069             any { my @previous = values %$_;
1070             any { my $player=$_;
1071             any { $player eq $_ } @previous
1072             } @players
1073             } @pairs;
1074             }
1075             }
1076             else {
1077             for my $n ( keys %$pairs ) {
1078             my $pair = $pairs->{$n};
1079             my @twoplayers = values %$pair;
1080             die "Table number $n with players @twoplayers is dupe" if
1081             exists $pairs{$n} or
1082             any { my $player = $_; any { $player eq $_ } @dupes
1083             } @twoplayers;
1084             push @dupes, @twoplayers;
1085             $pairs{ $n } = $pair;
1086             }
1087             }
1088             }
1089             }
1090             return \@pairs if $wantlist;
1091             return \%pairs;
1092             }
1093              
1094             =head3 pair2table
1095              
1096             A player and opponent mapped to a table number.
1097              
1098             =cut
1099              
1100             method pair2table ( Str $player, Str $opponent, Str $round ) {
1101             my $table = $self->tables( $round );
1102             for my $n ( keys %$table ) {
1103             my $table = $table->{$n};
1104             my @pair = values %$table;
1105             if ( any { $_ eq $player } @pair ) {
1106             if ( any { $_ eq $opponent } @pair ) {
1107             return { $n => $table };
1108             }
1109             }
1110             }
1111             die "No table with player $player, opponent $opponent in round $round";
1112             }
1113              
1114             =head3 compQuizfile
1115              
1116             The file system location of the file with the quiz questions and answers for the given Compcomp activity.
1117              
1118             =cut
1119              
1120             method compQuizfile ( Str $round ) {
1121             my $config = $self->config($round);
1122             my $text = $config->{text};
1123             return $self->compcompdirs . "/../" . $text;
1124             }
1125              
1126             =head3 topicNames
1127              
1128             Returns the names of comp quiz topics as an arrayref.
1129              
1130             =cut
1131              
1132             method topicNames ( Str $round ) {
1133             my $config = $self->config($round);
1134             my $activities = $config->{activity};
1135             my @topics = keys %$activities;
1136             return \@topics;
1137             }
1138              
1139             =head3 compQuizAttempted
1140              
1141             Returns the comp quiz topics and their associated forms attempted by the given group in the round, as an arrayref of hashrefs keyed on 'topic' and 'form'.
1142              
1143             =cut
1144              
1145             method compQuizAttempted ( Str $round, Str $table ) {
1146             my $config = $self->config($round);
1147             my $activities = $config->{activity};
1148             my $selection = $self->compQuizSelection;
1149             my $attempted;
1150             for my $topic ( keys %$selection ) {
1151             my $forms = $selection->{$topic};
1152             for my $form ( keys %$forms ) {
1153             my $tables = $activities->{$topic}->{$form};
1154             push @$attempted, { topic => $topic, form => $form }
1155             if any { $table == $_ } @$tables;
1156             }
1157             }
1158             return $attempted;
1159             }
1160              
1161             =head3 compQuiz
1162              
1163             The compQuiz questions (as an anon array) in the given Compcomp activity for the given table.
1164              
1165             =cut
1166              
1167             method compQuiz ( Str $round, Str $table ) {
1168             my $quizfile = $self->compQuizfile($round);
1169             my $activity;
1170             try { $activity = $self->inspect( $quizfile ) }
1171             catch { warn "No $quizfile Compcomp content file" };
1172             my $topic = $self->compTopic( $round, $table );
1173             my $form = $self->compForm( $round, $table );
1174             my $quiz = $activity->{$topic}->{compcomp}->{$form}->{quiz};
1175             carp "No $topic, $form quiz in $quizfile," unless $quiz;
1176             return $quiz;
1177             }
1178              
1179             =head3 compTopic
1180              
1181             The topic of the quiz in the given Compcomp round for the given table. Each table has one and only one quiz.
1182              
1183             =cut
1184              
1185             method compTopic ( Str $round, Str $table ) {
1186             my $config = $self->config($round);
1187             my $activity = $config->{activity};
1188             for my $topic ( keys %$activity ) {
1189             my $forms = $activity->{$topic};
1190             for my $form ( keys %$forms ) {
1191             my $tables = $forms->{$form};
1192             return $topic if any { $_ eq $table } @$tables;
1193             }
1194             }
1195             carp "Topic? No quiz at table $table in round $round,";
1196             return;
1197             }
1198              
1199             =head3 compTopics
1200              
1201             The topics of the quiz in the given Compcomp round for the given table, as an array ref.
1202              
1203             =cut
1204              
1205             method compTopics ( Str $round, Str $table ) {
1206             my $config = $self->config($round);
1207             my $activity = $config->{activity};
1208             my %topics;
1209             for my $topic ( keys %$activity ) {
1210             my $forms = $activity->{$topic};
1211             for my $form ( keys %$forms ) {
1212             my $tables = $forms->{$form};
1213             $topics{ $topic } += 1 if any { $_ eq $table } @$tables;
1214             }
1215             }
1216             carp "Topic? No quiz at table $table in round $round," unless %topics;
1217             my @topics = keys %topics;
1218             return \@topics;
1219             }
1220              
1221             =head3 compForm
1222              
1223             The form of the quiz in the given Compcomp round for the given table. Each table has one and only one quiz.
1224              
1225             =cut
1226              
1227             method compForm ( Str $round, Str $table ) {
1228             my $config = $self->config($round);
1229             my $activity = $config->{activity};
1230             for my $topic ( keys %$activity ) {
1231             my $forms = $activity->{$topic};
1232             for my $form ( keys %$forms ) {
1233             my $tables = $forms->{$form};
1234             return $form if any { $_ eq $table } @$tables;
1235             }
1236             }
1237             carp "Form? No quiz at table $table in round $round,";
1238             return;
1239             }
1240              
1241             =head3 compForms
1242              
1243             The forms in the given Compcomp round for the given table, in the given quiz (topic), as an array ref.
1244              
1245             =cut
1246              
1247             method compForms ( Str $round, Str $table, Str $topic ) {
1248             my $config = $self->config($round);
1249             my $activity = $config->{activity};
1250             my $forms = $activity->{$topic};
1251             my @forms;
1252             for my $form ( keys %$forms ) {
1253             my $tables = $forms->{$form};
1254             push @forms, $form if any { $_ eq $table } @$tables;
1255             }
1256             carp "Form? No quiz at table $table in round $round," unless @forms;
1257             return \@forms;
1258             }
1259              
1260             =head3 compqn
1261              
1262             The number of questions in the given Compcomp quiz for the given pair.
1263              
1264             =cut
1265              
1266             method compqn ( Str $round, Str $table ) {
1267             my $quiz = $self->compQuiz( $round, $table );
1268             return scalar @$quiz;
1269             }
1270              
1271             =head3 idsbyCompRole
1272              
1273             Ids in array, in White, Black role order
1274              
1275             =cut
1276              
1277              
1278             method idsbyCompRole ( Str $round, Str $table ) {
1279             my $members = $self->league->members;
1280             my %namedMembers = map { $_->{name} => $_ } @$members;
1281             my $config = $self->config( $round );
1282             my $pair = $config->{group}->{$table};
1283             my @idsbyRole = @$pair{qw/White Black/};
1284             return \@idsbyRole;
1285             }
1286              
1287             =head3 scores
1288              
1289             The scores at the tables of the tournament in the given round (as an anon hash keyed on the ids of the members). In a file in the Compcomp round directory called 'result.yaml'.
1290              
1291             =cut
1292              
1293              
1294             method scores ( Str $round ) {
1295             my $comp = $self->compcompdirs;
1296             my $file = "$comp/$round/scores.yaml";
1297             my $results = $self->inspect( $file );
1298             return $results;
1299             }
1300              
1301             =head3 compResponses
1302              
1303             The responses of the members of the given pair in the given round (as an anon hash keyed on the ids of the members). In a file in the Compcomp round directory called 'response.yaml'.
1304              
1305             =cut
1306              
1307              
1308             method compResponses ( Str $round, Str $table ) {
1309             my $comp = $self->compcompdirs;
1310             my $file = "$comp/$round/response.yaml";
1311             my $responses = $self->inspect( $file );
1312             return { free => $responses->{free}->{$table},
1313             set => $responses->{set}->{$table} };
1314             }
1315              
1316             =head3 freeTotals
1317              
1318             The number of free questions each asked by White and Black.
1319              
1320             =cut
1321              
1322              
1323             method freeTotals ( Str $round, Str $table ) {
1324             my $response = $self->compResponses( $round, $table );
1325             my $player = $self->idsbyCompRole( $round, $table );
1326             my $topics = $self->compTopics( $round, $table );
1327             my @qn = (0,0);
1328             for my $topic ( @$topics ) {
1329             my $forms = $self->compForms( $round, $table, $topic );
1330             for my $form ( @$forms ) {
1331             for my $n ( 0,1 ) {
1332             my $points =
1333             $response->{free}->{$topic}->{$form}->{$player->[$n]}->{point};
1334             $qn[$n] += max ( grep { $points->{$_} ne 'Nil' }
1335             keys %$points ) || 0;
1336             }
1337             }
1338             }
1339             return \@qn;
1340             }
1341            
1342             =head3 lowerFreeTotal
1343              
1344             The lesser of the 2 numbers of free questions asked by either White and Black.
1345              
1346             =cut
1347              
1348             method lowerFreeTotal ( Str $round, Str $table ) {
1349             my $totals = $self->freeTotals( $round, $table );
1350             return min @$totals;
1351             }
1352            
1353             =head3 byer
1354              
1355             The id of the player with the Bye, or the empty string.
1356              
1357             =cut
1358              
1359             method byer ( Str $round ) {
1360             my $config = $self->config( $round );
1361             my $byer = $config->{bye};
1362             return $byer if $byer;
1363             return '';
1364             }
1365              
1366              
1367             =head3 transfer
1368              
1369             An array ref of the ids of the players who were playing in another league in the round, or the empty string.
1370              
1371             =cut
1372              
1373             method transfer ( Str $round ) {
1374             my $config = $self->config( $round );
1375             my $transfers = $config->{transfer} || '';
1376             return $transfers;
1377             }
1378              
1379              
1380             =head3 opponents
1381              
1382             The ids of opponents of the players in the given conversation.
1383              
1384             =cut
1385              
1386             method opponents ( Str $round ) {
1387             my $tables = $self->tables( $round );
1388             my %opponent;
1389             for my $n ( keys %$tables ) {
1390             $opponent{$tables->{$n}->{White}} = $tables->{$n}->{Black};
1391             $opponent{$tables->{$n}->{Black}} = $tables->{$n}->{White};
1392             }
1393             my $byer = $self->byer( $round );
1394             $opponent{ $byer } = 'bye' if $byer;
1395             my $transfers = $self->transfer( $round );
1396             @opponent{ @$transfers } = ( 'transfer' ) x @$transfers
1397             if ( $transfers and ref( $transfers ) eq 'ARRAY' );
1398             my $league = $self->league;
1399             my $members = $league->members;
1400             $opponent{$_->{id}} ||= 'unpaired' for @$members;
1401             return \%opponent;
1402             }
1403              
1404              
1405             =head3 correct
1406              
1407             The number of questions correct in the given conversation.
1408              
1409             =cut
1410              
1411             method correct ( Str $round ) {
1412             my $comp = $self->compcompdirs;
1413             my $file = "$comp/$round/scores.yaml";
1414             my $tables = $self->inspect( $file );
1415             my %correct;
1416             for my $table ( keys %$tables ) {
1417             my $scores = $tables->{$table};
1418             @correct{keys %$scores} = values %$scores;
1419             }
1420             return \%correct;
1421             }
1422              
1423              
1424             =head3 assistantPoints
1425              
1426             Assistants points are from config->{assistant} of form { Black => { U9933002 => 3, U9933007 => 4}, Yellow => { U9931007 => 4, U9933022 => 4 } }, and are the points for examiners with other responsibilities who are not participating in the round.
1427              
1428             =cut
1429              
1430             method assistantPoints ( Str $round ) {
1431             my $config = $self->config( $round );
1432             my $assistants = $config->{assistant};
1433             if ( $assistants ) {
1434             my %assistantPoints = map { %{ $assistants->{$_} } } keys %$assistants;
1435             # my %assistantPoints = map { $assistants->{$_}->flatten } keys %$assistants;
1436             die "@{ [keys %$assistants] }: assistant member mistakes." if any
1437             { not $self->league->is_member($_) } keys %assistantPoints;
1438             return \%assistantPoints;
1439             }
1440             }
1441              
1442             =head3 dispensation
1443              
1444             Dispensation points are from config->{dispensation} of same form as assistantPoints, { Black => { U9933002 => 3, U9933007 => 4}, Yellow => { U9931007 => 4, U9933022 => 4 } }.
1445              
1446             =cut
1447              
1448             method dispensation ( Str $round ) {
1449             my $config = $self->config( $round );
1450             my $dispensation = $config->{dispensation};
1451             if ( $dispensation ) {
1452             my %dispensation = map { %{ $dispensation->{$_} } } keys %$dispensation;
1453             # my %assistantPoints = map { $assistants->{$_}->flatten } keys %$assistants;
1454             die "@{ [keys %$dispensation] }: members?" if any
1455             { not $self->league->is_member($_) } keys %dispensation;
1456             return \%dispensation;
1457             }
1458             }
1459              
1460             =head3 payout
1461              
1462             If payprotocol field is 'meritPay', 1 question each: 0,1 or 2 pts. 2 question each: 1,2 or 3 pts. 3 question each: 2,3 or 4 pts. 4 question each: 3,4 or 5 pts.
1463              
1464             If the 'meritPay' payprotocol field ends in a number the specified number of questions each is required for the maximum points.
1465             =cut
1466              
1467             method payout ( Str $player, Str $opponent, Str $round ) {
1468             my $protocol = $self->config($round)->{payprotocol};
1469             my ($loss, $draw, $win) = (3,4,5);
1470             if ( defined $protocol and $protocol =~ m/^meritPay/ ) {
1471             (my $top_number = $protocol ) =~ s/^\D*(\d*)$/$1/;
1472             my $required = $top_number? $top_number: 4;
1473             my $table = $self->pair2table( $player, $opponent, $round );
1474             my $tableN = (keys %$table)[0];
1475             my $questionN = $self->lowerFreeTotal( $round, $tableN );
1476             my $unfulfilled = $required - $questionN;
1477             if ( $unfulfilled > 0 ) {
1478             $_ -= $unfulfilled for ($loss, $draw, $win);
1479             if ( $loss < 0 ) {
1480             $loss = 0; $draw = 0; $win = 1;
1481             }
1482             }
1483             }
1484             return { loss => $loss, draw => $draw, win => $win };
1485             }
1486              
1487              
1488             =head3 points
1489              
1490             The points of the players in the given conversation. 5 for a Bye, 1 for Late, 0 for Unpaired, 1 for a non-numerical number correct result, 5 for more correct, 3 for less correct, 4 for the same number correct. Transfers' results are computed from their results in the same round in their old league. Assistants points are from round.yaml, points for non-paired helpers.
1491              
1492             =cut
1493              
1494             method points ( Str $round ) {
1495             my $config = $self->config( $round );
1496             my $opponents = $self->opponents( $round );
1497             my $correct = $self->correct( $round );
1498             my $points;
1499             my $late; $late = $config->{late} if exists $config->{late};
1500             my $forfeit; $forfeit = $config->{forfeit} if exists $config->{forfeit};
1501             my $assists = $self->assistantPoints( $round );
1502             my $dispensed = $self->dispensation( $round );
1503             my $byer = $self->byer( $round );
1504             PLAYER: for my $player ( keys %$opponents ) {
1505             if ( defined $assists and any { $_ eq $player } keys %$assists){
1506             $points->{$player} = $assists->{$player};
1507             next PLAYER;
1508             }
1509             if ( defined $dispensed and any { $_ eq $player } keys %$dispensed){
1510             $points->{$player} = $dispensed->{$player};
1511             next PLAYER;
1512             }
1513             if ( any { defined } @$forfeit and any { $_ eq $player } @$forfeit){
1514             $points->{$player} = 0;
1515             next PLAYER;
1516             }
1517             if ( any { defined } @$late and any { $_ eq $player } @$late ) {
1518             $points->{$player} = 1;
1519             next PLAYER;
1520             }
1521             if ( $byer and $player eq $byer ) {
1522             $points->{$player} = 5;
1523             next PLAYER;
1524             }
1525             if ( $opponents->{$player} =~ m/unpaired/i ) {
1526             $points->{$player} = 0;
1527             next PLAYER;
1528             }
1529             if ( $opponents->{$player} =~ m/transfer/i ) {
1530             my $oldleagueId = $self->league->transfer->{$player};
1531             my $oldleague = League->new( id => $oldleagueId );
1532             my $oldgrades = Grades->new({ league => $oldleague });
1533             my $oldclasswork = $oldgrades->classwork;
1534             $points->{$player} = $oldclasswork->points($round)->{$player};
1535             next PLAYER;
1536             }
1537             my $other = $opponents->{$player};
1538             my $alterego = $opponents->{$other};
1539             die
1540             "${player}'s opponent is $other, but ${other}'s opponent is $alterego"
1541             unless $other and $alterego and $player eq $alterego;
1542             die "No $player quiz card in round $round?" unless exists
1543             $correct->{$player};
1544             my $ourcorrect = $correct->{$player};
1545             die "No $other card against $player in round $round?" unless
1546             exists $correct->{$other};
1547             my $theircorrect = $correct->{$other};
1548             if ( not defined $ourcorrect ) {
1549             $points->{$player} = 0;
1550             next PLAYER;
1551             }
1552             if ( $correct->{$player} !~ m/^\d+$/ ) {
1553             $points->{$player} = 1;
1554             next PLAYER;
1555             }
1556             if ( any { defined } @$forfeit and any { $_ eq $other } @$forfeit) {
1557             $points->{$player} = 5;
1558             next PLAYER;
1559             }
1560             my $grade = $self->payout( $player, $other, $round );
1561             $points->{$player} = $ourcorrect > $theircorrect? $grade->{win}:
1562             $ourcorrect < $theircorrect? $grade->{loss}: $grade->{draw};
1563             }
1564             return $points;
1565             }
1566              
1567              
1568             =head3 total
1569              
1570             The total over the conversations over the series.
1571              
1572             =cut
1573              
1574             has 'total' => ( is => 'ro', isa => Results, lazy_build => 1 );
1575             method _build_total {
1576             my $rounds = $self->all_events;
1577             my $members = $self->league->members;
1578             my @ids = map { $_->{id} } @$members;
1579             my $totals;
1580             @$totals{ @ids } = (0) x @ids;
1581             for my $round ( @$rounds ) {
1582             my $points = $self->points( $round );
1583             for my $id ( @ids ) {
1584             next unless defined $points->{$id};
1585             $totals->{$id} += $points->{$id};
1586             }
1587             }
1588             return $totals;
1589             }
1590              
1591              
1592             =head3 totalPercent
1593              
1594             The total over the conversations over the series expressed as a percentage of the possible score. The average should be 80 percent if every player participates in every comp.
1595              
1596             =cut
1597              
1598             has 'totalPercent' => ( is => 'ro', isa => Results, lazy_build => 1 );
1599             method _build_totalPercent {
1600             my $rounds = $self->all_events;
1601             my $n = scalar @$rounds;
1602             my $totals = $self->total;
1603             my %percentages = $n?
1604             map { $_ => $totals->{$_} * 100 / (5*$n) } keys %$totals:
1605             map { $_ => 0 } keys %$totals;
1606             return \%percentages;
1607             }
1608              
1609             }
1610              
1611              
1612             =head2 Grades' Exams Methods
1613             =cut
1614              
1615             role Exams {
1616             use List::Util qw/max sum/;
1617             use List::MoreUtils qw/any all/;
1618             use Carp;
1619             use Grades::Types qw/Exam/;
1620              
1621             =head3 examdirs
1622              
1623             The directory where the exams are.
1624              
1625             =cut
1626              
1627             has 'examdirs' => (is => 'ro', isa => 'Str', lazy_build => 1);
1628             method _build_examdirs {
1629             my $league = $self->league->id;
1630             my $leaguedir = $self->league->leagues . "/" . $league;
1631             my $basename = $self->league->yaml->{jigsaw} ||
1632             $self->league->yaml->{exams} || "exams";
1633             my $examdirs = $leaguedir .'/' . $basename;
1634             }
1635              
1636             =head3 examids
1637              
1638             An arrayref of the ids of the exams for which there are grades for players in the league, in numerical order, of the form, [1, 3 .. 7, 9, 10 .. 99 ]. Results are in sub directories of the same name, under examdir.
1639              
1640             =cut
1641              
1642             has 'examids',
1643             ( is => 'ro', isa => 'Maybe[ArrayRef[Int]]', lazy_build => 1 );
1644             method _build_examids {
1645             my $examdirs = $self->examdirs;
1646             my @exams = grep { -d } glob "$examdirs/[0-9] $examdirs/[1-9][0-9]";
1647             [ sort { $a <=> $b } map m/^$examdirs\/(\d+)$/, @exams ];
1648             }
1649              
1650             =head3 examrounds
1651              
1652             The rounds over which the given exam was conducted. Should be an array ref. If there were no rounds, ie the exam was conducted in one round, a null anonymous array is returned. The results for the rounds are in sub directories underneath the 'examid' directory named, in numerical order, 1 .. 99.
1653              
1654             =cut
1655              
1656             method examrounds( Str $exam ) {
1657             my $examdirs = $self->examdirs;
1658             my $examids = $self->examids;
1659             carp "No exam $exam in exams @$examids"
1660             unless any { $_ eq $exam } @$examids;
1661             my @rounds = glob "$examdirs/$exam/[0-9] $examdirs/$exam/[0-9][0-9]";
1662             [ sort { $a <=> $b } map m/^$examdirs\/$exam\/(\d+)$/, @rounds ];
1663             }
1664              
1665             =head3 examMax
1666              
1667             The maximum score possible in each individual exam. That is, what the exam is out of.
1668              
1669             =cut
1670              
1671             has 'examMax' => (is => 'ro', isa => 'Int', lazy => 1, required => 1,
1672             default => sub { shift->league->yaml->{examMax} } );
1673              
1674             =head3 exam
1675              
1676             $grades->exam($id)
1677              
1678             The scores of the players on an individual (round of an) exam (in a 'g.yaml file in the $id subdir of the league dir.
1679              
1680             =cut
1681              
1682             method exam ( Str $id ) {
1683             my $examdirs = $self->examdirs;
1684             my $exam = $self->inspect( "$examdirs/$id/g.yaml" );
1685             if ( is_Exam($exam) ) {
1686             return $exam ;
1687             }
1688             else {
1689             croak
1690             "Exam $id probably has undefined or non-numeric Exam scores, or possibly illegal PlayerIds." ;
1691             }
1692             }
1693              
1694             =head3 examResults
1695              
1696             A hash ref of the ids of the players and arrays of their results over the exam series, ie examids, in files named 'g.yaml', TODO but only if such a file exists in all examdirs. Otherwise, calculate from raw 'response.yaml' files. Croak if any result is larger than examMax.
1697              
1698             =cut
1699              
1700             has 'examResults' => ( is => 'ro', isa => 'HashRef', lazy_build => 1 );
1701             method _build_examResults {
1702             my $examids = $self->examids;
1703             my $members = $self->league->members;
1704             my @playerids = map { $_->{id} } @$members;
1705             my %results;
1706             for my $id ( @$examids ) {
1707             my $exam = $self->exam( $id );
1708             my $max = $self->examMax;
1709             for my $playerid ( @playerids ) {
1710             my $result = $exam->{$playerid};
1711             carp "No exam $id results for $playerid,"
1712             unless defined $result;
1713             croak "${playerid}'s $result greater than exam max, $max"
1714             if defined $result and $result > $max;
1715             my $results = $results{$playerid};
1716             push @$results, $result;
1717             $results{$playerid} = $results;
1718             }
1719             }
1720             return \%results;
1721             }
1722              
1723             =head3 examResultHash
1724              
1725             A hash ref of the ids of the players and hashrefs of their results for each exam. Croak if any result is larger than examMax.
1726              
1727             =cut
1728              
1729             has 'examResultHash' => (is => 'ro', isa => 'HashRef', lazy_build => 1);
1730             method _build_examResultHash {
1731             my $examids = $self->examids;
1732             my $examResults = $self->examResults;
1733             my %examResults;
1734             for my $id ( keys %$examResults ) {
1735             my $results = $examResults->{$id};
1736             my %results;
1737             @results{@$examids} = @$results;
1738             $examResults{$id} = \%results;
1739             }
1740             return \%examResults;
1741             }
1742              
1743             =head3 examResultsasPercent
1744              
1745             A hashref of the ids of the players and arrays of their results over the exams expressed as percentages of the maximum possible score for the exams.
1746              
1747             =cut
1748              
1749             has 'examResultsasPercent' => (is=>'ro', isa=>'HashRef', lazy_build=>1);
1750             method _build_examResultsasPercent {
1751             my $scores = $self->examResults;
1752             my @ids = keys %$scores;
1753             my $max = $self->examMax;
1754             my %percent = map { my $id = $_; my $myscores = $scores->{$id};
1755             $id => [ map { ($_||0) * (100/$max) } @$myscores ] } @ids;
1756             return \%percent;
1757             }
1758              
1759             =head3 examGrade
1760              
1761             A hash ref of the ids of the players and their total scores on exams.
1762              
1763             =cut
1764              
1765             has 'examGrade' => (is => 'ro', isa => 'HashRef', lazy_build => 1);
1766             method _build_examGrade {
1767             my $grades = $self->examResults;
1768             +{ map { my $numbers=$grades->{$_};
1769             $_ => sum(@$numbers) }
1770             keys %$grades };
1771             }
1772              
1773             =head3 examPercent
1774              
1775             A hash ref of the ids of the players and their total score on exams, expressed as a percentage of the possible exam score. This is the average of their exam scores.
1776              
1777             =cut
1778              
1779             has 'examPercent' => (is => 'ro', isa => 'HashRef', lazy_build => 1);
1780             method _build_examPercent {
1781             my $grades = $self->examResultsasPercent;
1782             my %totals = map {
1783             my $numbers=$grades->{$_};
1784             $_ => sum(@$numbers)/@{$numbers} } keys %$grades;
1785             return \%totals;
1786             }
1787              
1788             }
1789              
1790              
1791             =head2 Grades' Core Methods
1792              
1793             =cut
1794              
1795             class Grades with Homework with Exams with Jigsaw
1796              
1797             {
1798             # with 'Jigsaw'
1799             # => { -alias => { config => 'jigsaw_config' }, -excludes => 'config' };
1800             require Grades::Groupwork;
1801             use Carp;
1802             use Grades::Types qw/Weights/;
1803              
1804             =head3 BUILDARGS
1805              
1806             Have Moose find out the classwork approach the league has adopted and create an object of that approach for the classwork accessor. This is preferable to requiring the user to create the object and pass it at construction time.
1807              
1808             =cut
1809              
1810             around BUILDARGS (ClassName $class: HashRef $args) {
1811             my $league = $args->{league} or die "$args->{league} league?";
1812             my $approach = $league->approach or die "approach?";
1813             my $classwork = $approach->new( league => $league ) or die "classwork?";
1814             $args->{classwork} = $classwork;
1815             return $class->$orig({ league => $league, classwork => $classwork });
1816             }
1817             # around BUILDARGS(@args) { $self->$orig(@args) }
1818              
1819             =head3 classwork
1820              
1821             An accessor for the object that handles classwork methods. Required at construction time.
1822              
1823             =cut
1824              
1825             has 'classwork' => ( is => 'ro', isa => 'Approach', required => 1,
1826             handles => [ 'series', 'beancans',
1827             'points', 'all_events',
1828             'classwork_total', 'classworkPercent' ] );
1829              
1830             =head3 config
1831              
1832             The possible grades config files. Including Jigsaw, Compcomp.
1833              
1834             =cut
1835              
1836             method config ( $role, $round ) {
1837             my $config = "${role}::config"; $self->$config( $round );
1838             }
1839              
1840             =head3 league
1841              
1842             The league (object) whose grades these are.
1843              
1844             =cut
1845              
1846             has 'league' => (is =>'ro', isa => 'League', required => 1,
1847             handles => [ 'inspect' ] );
1848              
1849             =head3 weights
1850              
1851             An hash ref of the weights (expressed as a percentage) accorded to the three components, classwork, homework, and exams in the final grade.
1852              
1853             =cut
1854              
1855             has 'weights' => (is => 'ro', isa => Weights, lazy_build => 1 );
1856             method _build_weights { my $weights = $self->league->yaml->{weights}; }
1857              
1858              
1859             =head3 sprintround
1860              
1861             sprintf( '%.0f', $number). sprintf warns if $number is undef.
1862              
1863             =cut
1864              
1865             method sprintround (Maybe[Num] $number) {
1866             sprintf '%.0f', $number;
1867             }
1868              
1869             =head3 grades
1870              
1871             A hashref of student ids and final grades.
1872              
1873             =cut
1874              
1875             method grades {
1876             my $league = $self->league;
1877             my $members = $league->members;
1878             my $homework = $self->homeworkPercent;
1879             my $classcomponent = $league->approach;
1880             my $classwork = $self->classworkPercent;
1881             my $exams = $self->examPercent;
1882             my @ids = map { $_->{id} } @$members;
1883             my $weights = $self->weights;
1884             my %grades = map { $_ => $self->sprintround(
1885             $classwork->{$_} * $weights->{classwork} /100 +
1886             $homework->{$_} * $weights->{homework} /100 +
1887             $exams->{$_} * $weights->{exams} /100 )
1888             } @ids;
1889             \%grades;
1890             }
1891              
1892             }
1893              
1894             no Moose;
1895              
1896             __PACKAGE__->meta->make_immutable;
1897              
1898             1; # End of Grades
1899              
1900             =head1 AUTHOR
1901              
1902             Dr Bean, C<< <drbean, followed by the at mark (@), cpan, then a dot, and finally, org> >>
1903              
1904             =head1 BUGS
1905              
1906             Please report any bugs or feature requests to
1907             C<bug-grades at rt.cpan.org>, or through the web interface at
1908             L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Grades>.
1909             I will be notified, and then you'll automatically be notified of progress on
1910             your bug as I make changes.
1911              
1912             =head1 SUPPORT
1913              
1914             You can find documentation for this module with the perldoc command.
1915              
1916             perldoc Grades
1917              
1918             You can also look for information at:
1919              
1920             =over 4
1921              
1922             =item * AnnoCPAN: Annotated CPAN documentation
1923              
1924             L<http://annocpan.org/dist/Grades>
1925              
1926             =item * CPAN Ratings
1927              
1928             L<http://cpanratings.perl.org/d/Grades>
1929              
1930             =item * RT: CPAN's request tracker
1931              
1932             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Grades>
1933              
1934             =item * Search CPAN
1935              
1936             L<http://search.cpan.org/dist/Grades>
1937              
1938             =back
1939              
1940             =head1 COPYRIGHT & LICENSE
1941              
1942             Copyright 2009 Dr Bean, all rights reserved.
1943              
1944             This program is free software; you can redistribute it and/or modify it
1945             under the same terms as Perl itself.
1946              
1947              
1948             =cut
1949              
1950              
1951             # vim: set ts=8 sts=4 sw=4 noet:
1952             __END__