3 # Copyright (C) 2012 W. Trevor King <wking@tremily.us>
5 # This file is part of pygrader.
7 # pygrader is free software: you can redistribute it and/or modify it under the
8 # terms of the GNU General Public License as published by the Free Software
9 # Foundation, either version 3 of the License, or (at your option) any later
12 # pygrader is distributed in the hope that it will be useful, but WITHOUT ANY
13 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14 # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License along with
17 # pygrader. If not, see <http://www.gnu.org/licenses/>.
19 """Manage grades from the command line
22 import configparser as _configparser
23 from email.mime.text import MIMEText as _MIMEText
24 import email.utils as _email_utils
25 import inspect as _inspect
26 import logging as _logging
27 import logging.handlers as _logging_handlers
28 import os.path as _os_path
31 import pgp_mime as _pgp_mime
33 import pygrader as _pygrader
34 from pygrader import __version__
35 from pygrader import LOG as _LOG
36 from pygrader import color as _color
37 from pygrader.email import test_smtp as _test_smtp
38 from pygrader.email import Responder as _Responder
39 from pygrader.mailpipe import mailpipe as _mailpipe
40 from pygrader.storage import initialize as _initialize
41 from pygrader.storage import load_course as _load_course
42 from pygrader.tabulate import tabulate as _tabulate
43 from pygrader.template import assignment_email as _assignment_email
44 from pygrader.template import course_email as _course_email
45 from pygrader.template import student_email as _student_email
46 from pygrader.todo import print_todo as _todo
49 if __name__ == '__main__':
50 from argparse import ArgumentParser as _ArgumentParser
52 parser = _ArgumentParser(description=__doc__)
54 '-v', '--version', action='version',
55 version='%(prog)s {}'.format(_pgp_mime.__version__))
57 '-d', '--base-dir', dest='basedir', default='.',
58 help='Base directory containing grade data')
60 '-e', '--encoding', dest='encoding', default='utf-8',
61 help=('Override the default file encoding selection '
62 '(useful when running from procmail)'))
64 '-c', '--color', default=False, action='store_const', const=True,
65 help='Color printed output with ANSI escape sequences')
67 '-V', '--verbose', default=0, action='count',
68 help='Increase verbosity')
70 '-s', '--syslog', default=False, action='store_const', const=True,
71 help='Log to syslog (rather than stderr)')
72 subparsers = parser.add_subparsers(title='commands')
74 smtp_parser = subparsers.add_parser(
75 'smtp', help=_test_smtp.__doc__.splitlines()[0])
76 smtp_parser.set_defaults(func=_test_smtp)
77 smtp_parser.add_argument(
79 help='Your address (email author)')
80 smtp_parser.add_argument(
81 '-t', '--target', dest='targets', action='append',
82 help='Address for the email recipient')
84 initialize_parser = subparsers.add_parser(
85 'initialize', help=_initialize.__doc__.splitlines()[0])
86 initialize_parser.set_defaults(func=_initialize)
87 initialize_parser.add_argument(
88 '-D', '--dry-run', default=False, action='store_const', const=True,
89 help="Don't actually send emails, create files, etc.")
91 tabulate_parser = subparsers.add_parser(
92 'tabulate', help=_tabulate.__doc__.splitlines()[0])
93 tabulate_parser.set_defaults(func=_tabulate)
94 tabulate_parser.add_argument(
95 '-s', '--statistics', default=False, action='store_const', const=True,
96 help='Calculate mean and standard deviation for each assignment')
98 email_parser = subparsers.add_parser(
99 'email', help='Send emails containing grade information')
100 email_parser.add_argument(
101 '-D', '--dry-run', default=False, action='store_const', const=True,
102 help="Don't actually send emails, create files, etc.")
103 email_parser.add_argument(
105 help='Your name (email author), defaults to course robot')
106 email_parser.add_argument(
107 '--cc', action='append', help='People to carbon copy')
108 email_subparsers = email_parser.add_subparsers(title='type')
109 assignment_parser = email_subparsers.add_parser(
110 'assignment', help=_assignment_email.__doc__.splitlines()[0])
111 assignment_parser.set_defaults(func=_assignment_email)
112 assignment_parser.add_argument(
113 'assignment', help='Name of the target assignment')
114 student_parser = email_subparsers.add_parser(
115 'student', help=_student_email.__doc__.splitlines()[0])
116 student_parser.set_defaults(func=_student_email)
117 student_parser.add_argument(
118 '-o', '--old', default=False, action='store_const', const=True,
119 help='Include already-notified information in emails')
120 student_parser.add_argument(
121 '-s', '--student', dest='student',
122 help='Explicitly select the student to notify (instead of everyone)')
123 course_parser = email_subparsers.add_parser(
124 'course', help=_course_email.__doc__.splitlines()[0])
125 course_parser.set_defaults(func=_course_email)
126 course_parser.add_argument(
127 '-t', '--target', dest='targets', action='append',
128 help='Name, alias, or group for the email recipient(s)')
130 mailpipe_parser = subparsers.add_parser(
131 'mailpipe', help=_mailpipe.__doc__.splitlines()[0])
132 mailpipe_parser.set_defaults(func=_mailpipe)
133 mailpipe_parser.add_argument(
134 '-D', '--dry-run', default=False, action='store_const', const=True,
135 help="Don't actually send emails, create files, etc.")
136 mailpipe_parser.add_argument(
137 '-m', '--mailbox', choices=['maildir', 'mbox'],
138 help=('Instead of piping a message in via stdout, you can also read '
139 'directly from a mailbox. This option specifies the format of '
140 'your target mailbox.'))
141 mailpipe_parser.add_argument(
142 '-i', '--input', dest='input_', metavar='INPUT',
143 help='Path to the mailbox containing messages to be processed')
144 mailpipe_parser.add_argument(
146 help=('Path to the mailbox that will recieve successfully processed '
147 'messages. If not given, successfully processed messages will '
148 'be left in the input mailbox'))
149 mailpipe_parser.add_argument(
150 '-l', '--max-late', default=0, type=float,
151 help=('Grace period in seconds before an incoming assignment is '
152 'actually marked as late'))
153 mailpipe_parser.add_argument(
154 '-r', '--respond', default=False, action='store_const', const=True,
155 help=('Send automatic response emails to acknowledge incoming '
157 mailpipe_parser.add_argument(
158 '-t', '--trust-email-infrastructure',
159 default=False, action='store_const', const=True,
160 help=('Send automatic response emails even if the target has not '
161 'registered a PGP key.'))
163 todo_parser = subparsers.add_parser(
164 'todo', help=_todo.__doc__.splitlines()[0])
165 todo_parser.set_defaults(func=_todo)
166 todo_parser.add_argument(
167 'source', help='Name of source file/directory')
168 todo_parser.add_argument(
169 'target', help='Name of target file/directory')
172 # p.add_option('-t', '--template', default=None)
174 args = parser.parse_args()
177 _LOG.setLevel(max(_logging.DEBUG, _LOG.level - 10*args.verbose))
178 _pgp_mime.LOG.setLevel(_LOG.level)
180 syslog = _logging_handlers.SysLogHandler(address="/dev/log")
181 syslog.setFormatter(_logging.Formatter('%(name)s: %(message)s'))
182 for handler in list(_LOG.handlers):
183 _LOG.removeHandler(handler)
184 _LOG.addHandler(syslog)
185 for handler in list(_pgp_mime.LOG.handlers):
186 _pgp_mime.LOG.removeHandler(handler)
187 _pgp_mime.LOG.addHandler(syslog)
188 _color.USE_COLOR = args.color
190 _pygrader.ENCODING = args.encoding
192 config = _configparser.ConfigParser()
194 _os_path.expanduser(_os_path.join('~', '.config', 'smtplib.conf')),
195 ], encoding=_pygrader.ENCODING)
197 func_args = _inspect.getargspec(args.func).args
200 if 'basedir' in func_args:
201 kwargs['basedir'] = args.basedir
203 if 'course' in func_args:
204 course = _load_course(basedir=args.basedir)
205 active_groups = course.active_groups()
206 kwargs['course'] = course
207 if hasattr(args, 'assignment'):
208 kwargs['assignment'] = course.assignment(name=args.assignment)
209 if hasattr(args, 'cc') and args.cc:
210 kwargs['cc'] = [course.person(name=cc) for cc in args.cc]
211 for attr in ['author', 'student']:
212 if hasattr(args, attr):
213 name = getattr(args, attr)
214 if name is None and attr == 'author':
215 kwargs[attr] = course.robot
217 kwargs[attr] = course.person(name=name)
218 for attr in ['targets']:
219 if hasattr(args, attr):
220 people = getattr(args, attr)
222 people = ['professors'] # for the course email
224 for person in people:
225 if person in active_groups:
226 kwargs[attr].extend(course.find_people(group=person))
228 kwargs[attr].extend(course.find_people(name=person))
229 for attr in ['dry_run', 'mailbox', 'output', 'input_', 'max_late',
230 'old', 'statistics', 'trust_email_infrastructure']:
231 if hasattr(args, attr):
232 kwargs[attr] = getattr(args, attr)
233 elif args.func == _test_smtp:
234 for attr in ['author', 'targets']:
235 if hasattr(args, attr):
236 kwargs[attr] = getattr(args, attr)
237 elif args.func == _todo:
238 for attr in ['source', 'target']:
239 if hasattr(args, attr):
240 kwargs[attr] = getattr(args, attr)
242 if args.func == _mailpipe:
243 kwargs['continue_after_invalid_message'] = True
245 if 'use_color' in func_args:
246 kwargs['use_color'] = args.color
248 if ('smtp' in func_args and
249 not kwargs.get('dry_run', False) and
250 'smtp' in config.sections()):
251 params = _pgp_mime.get_smtp_params(config)
252 kwargs['smtp'] = _pgp_mime.get_smtp(*params)
255 if hasattr(args, 'respond') and getattr(args, 'respond'):
256 kwargs['respond'] = _Responder(
257 smtp=kwargs.get('smtp', None),
258 dry_run=kwargs.get('dry_run', False))
260 _LOG.debug('execute {} with {}'.format(args.func, kwargs))
262 ret = args.func(**kwargs)
264 smtp = kwargs.get('smtp', None)
266 _LOG.info('disconnect from SMTP server')