<?xml version="1.0" encoding="UTF-8"?>
<commit>
  <added type="array"/>
  <modified type="array">
    <modified>
      <diff>@@ -1634,6 +1634,66 @@ sub quit {
   if (ref Browser::main_window()) {Browser::main_window()-&gt;destroy}
 }
 
+=head4 edit_custom()
+
+Edit a custom property for the whole class. Takes no arguments.
+
+=cut
+
+sub edit_custom {
+  my $self = shift;
+  local $Words::words_prefix = &quot;b.edit_custom&quot;;
+
+  my $data = $self-&gt;{DATA};
+  my $gb = $data-&gt;{GB};
+
+  my ($pa,$ph) = $gb-&gt;list_defined_student_properties({'last'=&gt;1,'first'=&gt;1,'dropped'=&gt;1});
+    # Don't do first and last, which would require complicated special-casing.
+    # Don't do dropped, which is boolean, not string.
+    # Allow id, plus any other custom ones they've defined.
+  my @p = @$pa;
+  my @roster = $gb-&gt;student_keys();
+  my %old = ();
+  my $p;
+
+  my $callback2 = sub {
+    my $new = shift;
+    my $did_something = 0;
+    foreach my $who(@roster) {
+      if ($new-&gt;{$who} ne $old{$who}) {
+        $gb-&gt;set_student_property($who,$p,$new-&gt;{$who});
+        #print &quot;set $who,$p,$new-&gt;{$who}\n&quot;;
+        $did_something = 1;
+      }
+    }
+    if ($did_something) {
+      $self-&gt;is_modified(1);
+      $self-&gt;{STAGE}-&gt;{ROSTER}-&gt;refresh();
+    };
+  };
+
+  my $callback = sub {
+    my $h = shift;
+    local $Words::words_prefix = &quot;b.edit_custom&quot;;
+    $p =  $h-&gt;{'property'}; # property to edit
+    my @inputs2 = ();
+    foreach my $who(@roster) {
+      my ($first,$last) = $gb-&gt;name($who);
+      $old{$who} = $gb-&gt;get_student_property($who,$p);
+      push @inputs2,Input-&gt;new(KEY=&gt;$who,PROMPT=&gt;&quot;$last, $first&quot;,TYPE=&gt;'string',DEFAULT=&gt;$old{$who});
+    }
+    ExtraGUI::fill_in_form(INPUTS=&gt;\@inputs2,TITLE=&gt;(sprintf(w('edit_properties'),$p)),CALLBACK=&gt;$callback2,COLUMNS=&gt;2);
+  };
+
+  my $m = {};
+  foreach my $p(@p) {$m-&gt;{$p}=$p}
+  my @inputs = (
+    Input-&gt;new(KEY=&gt;'property',WIDGET_TYPE=&gt;'menu',ITEM_KEYS=&gt;$pa,ITEM_MAP=&gt;$m,DEFAULT=&gt;'id'),
+  );
+
+  ExtraGUI::fill_in_form(INPUTS=&gt;\@inputs,TITLE=&gt;w('property_to_edit'),CALLBACK=&gt;$callback,COLUMNS=&gt;1);
+}
+
 =head4 edit_student()
 
 Takes no arguments.
@@ -1655,21 +1715,44 @@ sub edit_student {
          # ...See comments on the similar test in add_or_drop().
 
   my ($first,$last) = $gb-&gt;name($student);
-  my %old = (
-    'last'=&gt;$last,
-    'first'=&gt;$first,
-    'id'=&gt;$gb-&gt;get_student_property($student,'id'),
-  );
+  my %old = ('last'=&gt;$last,'first'=&gt;$first);
+
+  my ($pa,$ph) = $gb-&gt;list_defined_student_properties(); # all the standard ones, plus any special ones that have ever been defined for any student
+  my @p = @$pa;
+
+  my @inputs = ();
+  my $n = 1;
+  foreach my $p(@p) { # typically goes last, first, id
+    my $name = ucfirst($p);
+    if ($n++&lt;=3) {$name = w($p)}
+    $old{$p} = $gb-&gt;get_student_property($student,$p) if $p ne 'last' &amp;&amp; $p ne 'first';
+    push @inputs,Input-&gt;new(KEY=&gt;$p,PROMPT=&gt;$name,TYPE=&gt;'string',DEFAULT=&gt;$old{$p});
+  }
+  push @inputs,Input-&gt;new(KEY=&gt;'_define_new',PROMPT=&gt;w('define_new'),TYPE=&gt;'string');
+  push @inputs,Input-&gt;new(KEY=&gt;'_new_value', PROMPT=&gt;w('new_value'),TYPE=&gt;'string');
 
   my $callback = sub {
     my $new = shift;
     my $did_something = 0;
-    foreach my $what(keys %old) {
-      if (!(exists $old{$what} &amp;&amp; $old{$what} eq $new-&gt;{$what})) {
-        $gb-&gt;set_student_property($student,$what,$new-&gt;{$what});
-        $did_something = 1;
+    my %create = ();
+    foreach my $what(keys %$new) {
+      if ($what eq '_define_new' || $what eq '_new_value') {
+        $create{$what} = $new-&gt;{$what};
+      }
+      else {
+        if (!($old{$what} eq $new-&gt;{$what})) {
+          $gb-&gt;set_student_property($student,$what,$new-&gt;{$what});
+          $did_something = 1;
+        }
       }
     }
+    if (exists $create{'_new_value'} &amp;&amp; ! exists $create{'_define_new'}) {
+      ExtraGUI::error_message(w('null_property'));
+    }
+    if (exists $create{'_define_new'}) {
+      $gb-&gt;set_student_property($student,$create{'_define_new'},$create{'_new_value'});
+      $did_something = 1;
+    }
     if ($did_something) {
       $self-&gt;is_modified(1);
       $roster-&gt;refresh();
@@ -1680,11 +1763,7 @@ sub edit_student {
     TITLE=&gt;(sprintf w(&quot;title&quot;),$name),
     CALLBACK=&gt;$callback,
     COLUMNS=&gt;2,
-    INPUTS=&gt;[
-          Input-&gt;new(KEY=&gt;&quot;last&quot;,PROMPT=&gt;w(&quot;last&quot;),TYPE=&gt;'string',DEFAULT=&gt;$old{'last'}),
-          Input-&gt;new(KEY=&gt;&quot;first&quot;,PROMPT=&gt;w(&quot;first&quot;),TYPE=&gt;'string',DEFAULT=&gt;$old{'first'}),
-          Input-&gt;new(KEY=&gt;&quot;id&quot;,PROMPT=&gt;w(&quot;id&quot;),TYPE=&gt;'string',DEFAULT=&gt;$old{'id'}),
-    ]
+    INPUTS=&gt;\@inputs,
   );
 }
 
@@ -2295,6 +2374,7 @@ sub menu_bar {
   &amp;$add_item(&quot;STUDENTS_MENUB&quot;,\@items,'reinstate','',sub{$self-&gt;add_or_drop('reinstate')});
   &amp;$add_item(&quot;STUDENTS_MENUB&quot;,\@items,'edit',w('edit_disabled'),sub{$self-&gt;edit_student()});
   &amp;$add_item(&quot;STUDENTS_MENUB&quot;,\@items,'drop',(sprintf w('drop'),''),sub{$self-&gt;add_or_drop('drop')});
+  &amp;$add_item(&quot;STUDENTS_MENUB&quot;,\@items,'edit_custom','',sub{$self-&gt;edit_custom()});
   $self-&gt;{STUDENTS_MENUB}
    = $bar-&gt;Menubutton(-text=&gt;w(&quot;students&quot;),-menuitems=&gt;\@items,-tearoff=&gt;0)-&gt;pack(-side=&gt;'left');
 </diff>
      <filename>BrowserWindow.pm</filename>
    </modified>
    <modified>
      <diff>@@ -569,6 +569,7 @@ sub fill_in_form {
 
   my $ncols = $args{COLUMNS};
   my $t;
+  # ?????????? does the same for ncols==1 versus ncols==2???? bug???
   if ($ncols==2) {
     $t = $box-&gt;Scrolled(&quot;Text&quot;,
          -scrollbars=&gt;'oe',</diff>
      <filename>ExtraGUI.pm</filename>
    </modified>
    <modified>
      <diff>@@ -1648,6 +1648,47 @@ sub get_student_property {
     return get_property($self-&gt;roster_private_method()-&gt;{$who},$prop);
 }
 
+=head3 list_defined_student_properties()
+
+Returns ($a,$h), where $a is a ref to an array containing
+all the student properties that have been defined so far,
+and $h is a hash whose keys are the elements of $a. As soon as a property has been defined for one student,
+it's considered to be defined in general.
+The optional argument is a hash ref whose keys are properties that should be not be returned;
+if defaults to {&quot;dropped&quot;=&gt;1}.
+The order of @$a is guaranteed to be  &quot;last&quot;,&quot;first&quot;,&quot;id&quot;, and &quot;dropped,&quot;
+followed by any others, in alphabetical order.
+The second optional argument, is passed to student_keys.
+
+=cut
+
+sub list_defined_student_properties {
+  my $self = shift;
+  my $ignored = {&quot;dropped&quot;=&gt;1};
+  if (@_) {$ignored = shift}
+  my $criteria = ''; # defined in student_keys()
+  if (@_) {$criteria = shift}
+  my @standard = ('last','first','id','dropped');
+  my %found = ();
+  my @result;
+  foreach my $standard(@standard) {
+    $found{$standard} = 1;
+    push @result,$standard unless exists $ignored-&gt;{$standard};
+  }
+  my @students = $self-&gt;student_keys($criteria);
+  my $r = $self-&gt;roster_private_method();
+  foreach my $who(@students) {
+    my %k = comma_delimited_to_hash($r-&gt;{$who});
+    foreach my $k(keys %k) {
+      push @result,$k unless exists $ignored-&gt;{$k} || exists $found{$k};
+      $found{$k} = 1;
+    }
+  }
+  my %h = {};
+  foreach my $x(@result) {$h{$x}=1}
+  return (\@result,\%h);
+}
+
 =head3 get_property()
 
 Avoid using this routine, and use get_property2() instead.</diff>
      <filename>GradeBook.pm</filename>
    </modified>
    <modified>
      <diff>@@ -105,11 +105,16 @@ STRING
 &quot;en.b.dialog.cancel&quot; =&gt; &quot;Cancel&quot;,
 &quot;en.b.dialog.confirm&quot; =&gt; &quot;Confirm&quot;,
 &quot;en.b.dialog.confirm_overwrite&quot; =&gt; &quot;The file %s already exists. Overwrite it?&quot;,
+&quot;en.b.edit_custom.property_to_edit&quot; =&gt; &quot;Property to Edit&quot;,
+&quot;en.b.edit_custom.edit_properties&quot; =&gt; &quot;Editing %s&quot;,
 &quot;en.b.edit_student.title&quot; =&gt; &quot;Edit Information for %s&quot;,
 &quot;en.b.edit_student.id&quot; =&gt; &quot;Student ID&quot;,
 &quot;en.b.edit_student.pwd&quot; =&gt; &quot;Password&quot;,
 &quot;en.b.edit_student.first&quot; =&gt; &quot;First Name&quot;,
 &quot;en.b.edit_student.last&quot; =&gt; &quot;Last Name&quot;,
+&quot;en.b.edit_student.define_new&quot; =&gt; &quot;New Student Property to Define&quot;,
+&quot;en.b.edit_student.new_value&quot; =&gt; &quot;Value of Newly Defined Property for This Student&quot;,
+&quot;en.b.edit_student.null_property&quot; =&gt; &quot;You must define a name for the new student property you're defining.&quot;,
 &quot;en.b.delete_assignment.confirm&quot; =&gt; &quot;Delete %s?&quot;,
 &quot;en.b.edit_assignment.title&quot; =&gt; &quot;Edit Information for %s&quot;,
 &quot;en.b.edit_assignment.due&quot; =&gt; &quot;Due Date&quot;,
@@ -158,6 +163,7 @@ STRING
 &quot;en.b.menus.reinstate&quot; =&gt;&quot;Reinstate&quot;,
 &quot;en.b.menus.drop&quot; =&gt;&quot;Drop %s&quot;,
 &quot;en.b.menus.edit&quot; =&gt;&quot;Edit Information for %s&quot;,
+&quot;en.b.menus.edit_custom&quot; =&gt;&quot;Edit Property for Whole Class&quot;,
 &quot;en.b.menus.edit_disabled&quot; =&gt;&quot;Edit Information&quot;,
 &quot;en.b.menus.assignments&quot; =&gt;&quot;Assignments&quot;,
 &quot;en.b.menus.new_assignment_blank&quot; =&gt;&quot;New Assignment&quot;,</diff>
      <filename>MyWords.pm</filename>
    </modified>
    <modified>
      <filename>get_dependencies_from_cpan.pl</filename>
    </modified>
    <modified>
      <filename>lmmath.sty</filename>
    </modified>
    <modified>
      <filename>manpage.pod</filename>
    </modified>
  </modified>
  <removed type="array"/>
  <parents type="array">
    <parent>
      <id>9a284342fc96f1ca8b533519a8c48fdc2f40d70c</id>
    </parent>
  </parents>
  <author>
    <name>Ben Crowell</name>
    <email>githubcrowell09@lightandmatter.com</email>
  </author>
  <url>http://github.com/bcrowell/opengrade/commit/ae3574dc790beb33445c3e17ed2e048fd3d248d1</url>
  <id>ae3574dc790beb33445c3e17ed2e048fd3d248d1</id>
  <committed-date>2009-08-01T18:51:50-07:00</committed-date>
  <authored-date>2009-08-01T18:51:50-07:00</authored-date>
  <message>custom student properties</message>
  <tree>eb21eba51e01680276356f04e1dd11627de3e39b</tree>
  <committer>
    <name>Ben Crowell</name>
    <email>githubcrowell09@lightandmatter.com</email>
  </committer>
</commit>
