CAT-SOOP is a flexible, programmable learning management system based on the Python programming language. https://catsoop.mit.edu
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

251 lines
8.4 KiB

  1. # This file is part of CAT-SOOP
  2. # Copyright (c) 2011-2020 by The CAT-SOOP Developers <catsoop-dev@mit.edu>
  3. #
  4. # This program is free software: you can redistribute it and/or modify it under
  5. # the terms of the GNU Affero General Public License as published by the Free
  6. # Software Foundation, either version 3 of the License, or (at your option) any
  7. # later version.
  8. #
  9. # This program is distributed in the hope that it will be useful, but WITHOUT
  10. # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  11. # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
  12. # details.
  13. #
  14. # You should have received a copy of the GNU Affero General Public License
  15. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. """
  17. Utilities for handling grouping of students
  18. """
  19. import random
  20. from . import user
  21. def list_groups(context, path):
  22. """
  23. **Parameters:**
  24. * `context`: the context associated with this request
  25. * `path`: a list of strings representing the path we're interested in
  26. **Returns:** a dictionary mapping group names to lists of group members
  27. """
  28. log = context["csm_cslog"]
  29. return log.most_recent("_groups", path, "groups", {})
  30. def get_section(context, course, username):
  31. """
  32. Helper function to determine a user's section, if any.
  33. **Parameters:**
  34. * `context`: the context associated with this request
  35. * `course`: the course
  36. * `username`: the user whose section we want to find
  37. **Returns:** a string; the user's section, or `'default'` if the user has
  38. no section
  39. """
  40. uinfo = user.read_user_file(context, course, username, {})
  41. cuc = context["cs_user_config"]
  42. secvar = cuc.get("section_variable", "section")
  43. defsec = cuc.get("default_section_name", "default")
  44. return str(uinfo.get(secvar, defsec))
  45. def get_group(context, path, username, groups=None, secnum=None):
  46. """
  47. Get a user's group for a page
  48. **Parameters:**
  49. * `context`: the context associated with this request
  50. * `path`: a list of strings representing the path we're interested in
  51. * `username`: the user whose group we want to find
  52. **Optional Parameters:**
  53. * `groups` (default `None`): a listing of groups (of the same form as the
  54. output from `catsoop.groups.list_groups`). If none is specified,
  55. `catsoop.groups.list_groups` is invoked.
  56. * `secnum` (default `None`): the section in which to look for groups. If
  57. none is specified, `catsoop.groups.get_section` is invoked).
  58. **Returns:** the section number and group to which the given user belongs, or
  59. None if they have not been assigned a group.
  60. """
  61. course = path[0]
  62. if groups is None:
  63. groups = list_groups(context, path)
  64. if secnum is None:
  65. secnum = get_section(context, course, username)
  66. for group in groups.get(secnum, {}):
  67. if username in groups[secnum][group]:
  68. return (secnum, group, groups[secnum][group])
  69. return None, None, None
  70. def add_to_group(context, path, username, group):
  71. """
  72. Adds the given user to the given group.
  73. **Parameters:**
  74. * `context`: the context associated with this request
  75. * `path`: a list of strings representing the path we're interested in
  76. * `username`: the user whose group we want to find
  77. * `group`: the name of the group to which the student should be added
  78. **Returns:** `None` on success, or an error message on failure.
  79. """
  80. course = path[0]
  81. log = context["csm_cslog"]
  82. section = get_section(context, course, username)
  83. preexisting_group = get_group(context, path, username)
  84. if preexisting_group != (None, None, None):
  85. return "%s is already assigned to a group (section %s group %s)" % (
  86. (username,) + preexisting_group[:2]
  87. )
  88. def _transformer(x):
  89. x[section] = x.get(section, {})
  90. x[section][group] = x[section].get(group, []) + [username]
  91. return x
  92. try:
  93. log.modify_most_recent("_groups", path, "groups", {}, _transformer)
  94. except:
  95. return "An error occured when assigning to group."
  96. def remove_from_group(context, path, username, group):
  97. """
  98. Removes the given user from the given group.
  99. **Parameters:**
  100. * `context`: the context associated with this request
  101. * `path`: a list of strings representing the path we're interested in
  102. * `username`: the user whose group we want to find
  103. * `group`: the name of the group from which the student should be removed
  104. **Returns:** `None` on success, or an error message on failure.
  105. """
  106. course = path[0]
  107. log = context["csm_cslog"]
  108. section = get_section(context, course, username)
  109. preexisting_group = get_group(context, path, username)
  110. if preexisting_group[:-1] != (section, group):
  111. return "%s is not assigned to section %s group %s." % (username, section, group)
  112. def _transformer(x):
  113. x[section] = x.get(section, {})
  114. x[section][group] = [i for i in x[section].get(group, []) if i != username]
  115. if len(x[section][group]) == 0:
  116. del x[section][group]
  117. return x
  118. try:
  119. log.modify_most_recent("_groups", path, "groups", {}, _transformer)
  120. except:
  121. return "An error occured when removing from group."
  122. def overwrite_groups(context, path, section, newdict):
  123. """
  124. Overwrites group assignments for the given section
  125. **Parameters:**
  126. * `context`: the context associated with this request
  127. * `path`: a list of strings representing the path we're interested in
  128. * `section`: the section whose groups should be replaced
  129. * `newdict`: the new group assignments, as a dictionary mapping group names
  130. to lists of group members' usernames
  131. **Returns:** `None` on success, or an error message on failure.
  132. """
  133. log = context["csm_cslog"]
  134. def _transformer(x):
  135. x[section] = newdict
  136. return x
  137. try:
  138. log.modify_most_recent("_groups", path, "groups", {}, _transformer)
  139. except:
  140. return "An error occured when overwriting groups."
  141. def make_all_groups(context, path, section):
  142. """
  143. Randomly assigns groups within the given section for the given page,
  144. overwriting any existing groups for that section and page.
  145. All users with a file in `__USERS__` who are members of the given section
  146. are considered.
  147. Several variables in the given `context` affect the way groups are
  148. assigned:
  149. * The students can optionally be separated into subcategories (such that
  150. each student will only be partnered with other within their
  151. subcategory) by specifying a function `cs_group_category(path,
  152. username) => category_name`.
  153. * The names to be given to the various groups can be specified in
  154. `cs_group_names`, which should be a list of strings.
  155. * Within each subcategory, students are randomly shuffled, and groups of
  156. size `cs_group_size` are made from the shuffled list. Students will
  157. _never_ be paired with students outside their subcategory, even is
  158. `cs_group_size` does not evenly divide the number of students in a
  159. category.
  160. **Parameters:**
  161. * `context`: the context associated with this request
  162. * `path`: a list of strings representing the path we're interested in
  163. * `section`: the section whose groups should be replaced
  164. **Returns:** `None` on success, or an error message on failure.
  165. """
  166. course = path[0]
  167. user = context["csm_user"]
  168. size = context.get("cs_group_size", 2)
  169. def cat(uname):
  170. f = context.get("cs_group_category", lambda path, uname: "all")
  171. return f(path, uname)
  172. group_names = context.get("cs_group_names", list(map(str, range(1000))))
  173. group_names = list(group_names)
  174. students = user.list_all_users(context, course)
  175. def filt(uinfo):
  176. return uinfo.get("role", None) == "Student" and str(
  177. uinfo.get("section", None)
  178. ) == str(section)
  179. cats = {}
  180. for s in students:
  181. if not filt(user.read_user_file(context, course, s, {})):
  182. continue
  183. c = cat(s)
  184. cats[c] = cats.get(c, []) + [s]
  185. output = {}
  186. for c in sorted(cats):
  187. if c is None:
  188. continue
  189. random.shuffle(cats[c])
  190. while len(cats[c]) > 0:
  191. out, cats[c] = cats[c][:size], cats[c][size:]
  192. g = group_names[len(output)]
  193. output[g] = out
  194. err = overwrite_groups(context, path, section, output)
  195. return err