@RequestMapping("/bulk-invite")
  public String bulkInvite(@RequestParam("invitees") MultipartFile invitees) throws Exception {
    if (!invitees.isEmpty()) {
      Map<String, InvitationGroup> groupsByName = new HashMap<String, InvitationGroup>();
      ViewQuery query =
          new ViewQuery().designDocId("_design/groups").viewName("groups").includeDocs(true);
      for (InvitationGroup existing : db.queryView(query, InvitationGroup.class)) {
        groupsByName.put(standardizeName(existing.getGroupName()), existing);
      }

      BufferedReader r = new BufferedReader(new InputStreamReader(invitees.getInputStream()));
      for (String row = r.readLine(); row != null; row = r.readLine()) {
        String[] cells = row.split(",");
        if (cells[0].equals("Group Name")) continue;
        if (!(StringUtils.hasText(cells[0])
            && StringUtils.hasText(cells[1])
            && StringUtils.hasText(cells[2]))) throw new RuntimeException("Malformed row: " + row);
        if (cells[0].startsWith("\"") && cells[0].endsWith("\""))
          cells[0] = cells[0].substring(1, cells[0].length() - 1);
        InvitationGroup group = groupsByName.get(cells[0].trim());
        if (group == null) {
          group = new InvitationGroup();
          group.setGroupName(cells[0].trim());
          throw new RuntimeException("Only updates now! Couldn't find " + cells[0]);
        }
        group.setEmail(cells[1].trim());
        group.setLanguage(cells[2].trim());
        group.setInvitedTours(StringUtils.hasText(cells[3]));
        group.setInvitedRehearsal(StringUtils.hasText(cells[4]));
        if (group.getInvitees() != null) group.getInvitees().clear();
        for (int i = 5; i < cells.length; ++i) {
          if (StringUtils.hasText(cells[i].trim())) {
            Invitee invitee = new Invitee();
            invitee.setName(cells[i].trim());
            group.addInvitee(invitee);
          }
        }
        if (group.getInvitees().size() == 0)
          throw new RuntimeException("Group with no invitees: " + row);
        if (group.getId() == null) {
          group.setId(randomId());
          db.create(group);
          System.out.println("Created " + group.getGroupName());
        } else {
          db.update(group);
          System.out.println("Updated " + group.getId() + " " + group.getGroupName());
        }
      }
    }
    return "redirect:admin";
  }
 @RequestMapping("/fixRehearsal")
 public void fixRehearsal() throws Exception {
   ViewQuery query =
       new ViewQuery().designDocId("_design/groups").viewName("groups").includeDocs(true);
   List<InvitationGroup> groups = db.queryView(query, InvitationGroup.class);
   for (InvitationGroup group : groups) {
     boolean changed = false;
     for (Invitee inv : group.getInvitees()) {
       if (inv.getAttendingFriday() != null && inv.getAttendingFriday()) {
         if (inv.getAttendingRehearsal() == null || !inv.getAttendingRehearsal()) {
           inv.setAttendingRehearsal(true);
           changed = true;
         }
       }
     }
     if (group.isInvitedTours() && !group.isInvitedRehearsal()) {
       group.setInvitedRehearsal(true);
       changed = true;
     } else {
       System.out.println("Not invited to rehearsal: " + group.getGroupName());
     }
     if (changed) db.update(group);
   }
 }