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.
 
 
 

207 lines
6.2 KiB

  1. #!/usr/bin/env python3
  2. # This file is part of CAT-SOOP
  3. # Copyright (c) 2011-2020 by The CAT-SOOP Developers <catsoop-dev@mit.edu>
  4. #
  5. # This program is free software: you can redistribute it and/or modify it under
  6. # the terms of the GNU Affero General Public License as published by the Free
  7. # Software Foundation, either version 3 of the License, or (at your option) any
  8. # later version.
  9. #
  10. # This program is distributed in the hope that it will be useful, but WITHOUT
  11. # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  12. # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
  13. # details.
  14. #
  15. # You should have received a copy of the GNU Affero General Public License
  16. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  17. import os
  18. import ast
  19. import sys
  20. import shlex
  21. import pprint
  22. import shutil
  23. import tempfile
  24. import subprocess
  25. from .. import cslog
  26. from .. import base_context
  27. LOGREAD_USAGE = """\
  28. Print a log in human readable format.
  29. There are two ways to use this command, either:
  30. catsoop logread FILE
  31. FILE: a file on disk representing a CAT-SOOP log
  32. or:
  33. catsoop logread USERNAME PATH LOGNAME
  34. USERNAME: the name of the user
  35. PATH: a path from the root, separated by slashes, including the course
  36. name, e.g. spring19/labs/lab01
  37. LOGNAME: the name of the log to read (likely either problemstate or
  38. problemactions)
  39. """
  40. LOGWRITE_USAGE = """\
  41. Overwrite the given log with the contents of a file. This operation is
  42. unconditional; not a good idea unless you're sure you want to do it!
  43. There are two ways to use this command, either:
  44. catsoop logwrite LOGFILE ENTRYFILE
  45. LOGFILE: a file on disk representing a binary CAT-SOOP log
  46. ENTRYFILE: a file on disk containing a log in human-readable form, or - to
  47. read the entry from stdin
  48. or:
  49. catsoop logread USERNAME PATH LOGNAME ENTRYFILE
  50. USERNAME: the name of the user
  51. PATH: a path from the root, separated by slashes, including the course
  52. name, e.g. spring19/labs/lab01
  53. LOGNAME: the name of the log to read (likely either problemstate or
  54. problemactions)
  55. ENTRYFILE: a file on disk containing a log in human-readable form, or - to
  56. read the entry from stdin
  57. """
  58. LOGEDIT_USAGE = """\
  59. Open a log entry for editing in a human readable format. Changes made
  60. to the file will be written back to the log. If $EDITOR is set, it will
  61. be used. Otherwise, we'll try a sensible default (vim, emacs, or nano).
  62. This is a small wrapper around logread and logwrite.
  63. There are two ways to use this command, either:
  64. catsoop logedit FILE
  65. FILE: a file on disk representing a CAT-SOOP log
  66. or:
  67. catsoop logedit USERNAME PATH LOGNAME
  68. USERNAME: the name of the user
  69. PATH: a path from the root, separated by slashes, including the course
  70. name, e.g. spring19/labs/lab01
  71. LOGNAME: the name of the log to read (likely either problemstate or
  72. problemactions)
  73. """
  74. def _find_log(args):
  75. if len(args) == 1:
  76. filename = os.path.realpath(args[0])
  77. base = os.path.join(os.path.realpath(base_context.cs_data_root), "_logs/")
  78. if not (filename.startswith(base) and filename.endswith(".log")):
  79. print(
  80. ("The given file is not a valid log file for this " "installation."),
  81. file=sys.stderr,
  82. )
  83. sys.exit(1)
  84. fields = filename[len(base) : -4].lstrip("/").split("/")
  85. if fields[0] != "_courses":
  86. username = fields[0]
  87. path = []
  88. else:
  89. username = fields[2]
  90. path = [fields[1]] + fields[3:-1]
  91. logname = fields[-1]
  92. else:
  93. username, path, logname = args
  94. path = path.split("/") if path else []
  95. return username, path, logname
  96. def log_read(args):
  97. if len(args) not in {1, 3} or "-h" in args or "--help" in args:
  98. print(LOGREAD_USAGE, file=sys.stderr)
  99. sys.exit(1)
  100. username, path, logname = _find_log(args)
  101. entries = cslog.read_log(username, path, logname)
  102. if not entries:
  103. print("ERROR: no log entries", file=sys.stderr)
  104. sys.exit(1)
  105. for entry in entries:
  106. pprint.pprint(entry)
  107. print()
  108. def log_write(args):
  109. if len(args) not in {2, 4} or "-h" in args or "--help" in args:
  110. print(LOGWRITE_USAGE, file=sys.stderr)
  111. sys.exit(1)
  112. username, path, logname = _find_log(args[:-1])
  113. entry_file = args[-1]
  114. if entry_file == "-":
  115. entries = sys.stdin.read().split("\n\n")
  116. else:
  117. with open(entry_file, "r") as f:
  118. entries = f.read().split("\n\n")
  119. entries = [ast.literal_eval(e) for e in entries if e]
  120. for ix, e in enumerate(entries):
  121. if ix == 0:
  122. func = cslog.overwrite_log
  123. else:
  124. func = cslog.update_log
  125. func(username, path, logname, e)
  126. def find_editor():
  127. if "EDITOR" in os.environ:
  128. return shlex.split(os.environ["EDITOR"])
  129. for cmd in ("editor", "vim", "emacs", "nano", "vi"):
  130. ed = shutil.which(cmd)
  131. if ed:
  132. return [ed]
  133. return None
  134. def log_edit(args):
  135. if len(args) not in {1, 3} or "-h" in args or "--help" in args:
  136. print(LOGREAD_USAGE, file=sys.stderr)
  137. sys.exit(1)
  138. ed = find_editor()
  139. if ed is None:
  140. print(
  141. ("Error: could not find a valid editor. Please set $EDITOR."),
  142. file=sys.stderr,
  143. )
  144. sys.exit(1)
  145. username, path, logname = _find_log(args)
  146. entries = cslog.read_log(username, path, logname)
  147. body = ""
  148. with tempfile.NamedTemporaryFile(delete=False, mode="r+") as f:
  149. # dump entries into the file
  150. for entry in entries:
  151. print(pprint.pformat(entry), file=f)
  152. print(file=f)
  153. f.flush()
  154. # open the file for editing
  155. subprocess.check_call(ed + [f.name])
  156. # seek to the start of the file, read the entries in
  157. f.seek(0)
  158. entries = f.read().split("\n\n")
  159. entries = [ast.literal_eval(e) for e in entries if e]
  160. for ix, e in enumerate(entries):
  161. if ix == 0:
  162. func = cslog.overwrite_log
  163. else:
  164. func = cslog.update_log
  165. func(username, path, logname, e)
  166. if __name__ == "__main__":
  167. main()