~azzar1/unity/add-show-desktop-key

« back to all changes in this revision

Viewing changes to bin/ivle-marks

  • Committer: William Grant
  • Date: 2009-05-19 06:08:41 UTC
  • Revision ID: grantw@unimelb.edu.au-20090519060841-0m3nm6w30rze8pul
Drop leading 0s from pretty hours, and lowercase AM/PM.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
#!/usr/bin/env python
2
2
# IVLE - Informatics Virtual Learning Environment
3
 
# Copyright (C) 2007-2008 The University of Melbourne
 
3
# Copyright (C) 2007-2009 The University of Melbourne
4
4
#
5
5
# This program is free software; you can redistribute it and/or modify
6
6
# it under the terms of the GNU General Public License as published by
18
18
 
19
19
# Program: Marks
20
20
# Author:  Matt Giuca
21
 
# Date:    17/4/2008
22
21
 
23
22
# Script to calculate the marks for all students for a particular subject.
24
23
# Requires root to run.
25
24
 
26
25
import sys
27
26
import os
28
 
import re
29
27
import csv
30
 
import time
31
 
from xml.dom import minidom
32
 
 
33
 
import ivle.db
34
 
import ivle.conf
 
28
import datetime
 
29
import optparse
35
30
 
36
31
if os.getuid() != 0:
37
32
    print >>sys.stderr, "Must run %s as root." % os.path.basename(sys.argv[0])
38
33
    sys.exit()
39
34
 
40
 
if len(sys.argv) <= 1:
41
 
    print >>sys.stderr, "Usage: %s subject" % os.path.basename(sys.argv[0])
42
 
    sys.exit()
43
 
 
44
 
# Regex for valid identifiers (subject/worksheet names)
45
 
re_ident = re.compile("[0-9A-Za-z_]+")
46
 
 
47
 
# This code copy/edited from www/apps/tutorial/__init__.py
48
 
def is_valid_subjname(subject):
49
 
    m = re_ident.match(subject)
50
 
    return m is not None and m.end() == len(subject)
51
 
 
52
 
subject = sys.argv[1]
53
 
 
54
 
# Subject names must be valid identifiers
55
 
if not is_valid_subjname(subject):
56
 
    print >>sys.stderr, "Invalid subject name: %s." % repr(subject)
57
 
    sys.exit()
 
35
import ivle.config
 
36
import ivle.database
 
37
import ivle.worksheet.utils
58
38
 
59
39
def get_userdata(user):
60
40
    """
63
43
    (This is not marks, it's other user data).
64
44
    """
65
45
    last_login = (None if user.last_login is None else
66
 
                    time.strftime("%d/%m/%y", user.last_login))
 
46
                    user.last_login.strftime("%d/%m/%y"))
67
47
    return [user.studentid, user.login, user.fullname, last_login]
 
48
 
68
49
userdata_header = ["Student ID", "Login", "Full name", "Last login"]
69
 
 
70
 
def get_assessable_worksheets(subject):
71
 
    """
72
 
    Given a subject name, returns a list of strings - the worksheet names (not
73
 
    primary key IDs) for all assessable worksheets for that subject.
74
 
    May raise Exceptions, which are fatal.
75
 
    """
76
 
    # NOTE: This code is copy/edited from
77
 
    # www/apps/tutorial/__init__.py:handle_subject_menu
78
 
    # Should be factored out of there.
79
 
 
80
 
    # Parse the subject description file
81
 
    # The subject directory must have a file "subject.xml" in it,
82
 
    # or it does not exist (raise exception).
83
 
    try:
84
 
        subjectfile = open(os.path.join(ivle.conf.subjects_base, subject,
85
 
            "subject.xml"))
86
 
    except:
87
 
        raise Exception("Subject %s not found." % repr(subject))
88
 
 
89
 
    assessable_worksheets = []
90
 
    # Read in data about the subject
91
 
    subjectdom = minidom.parse(subjectfile)
92
 
    subjectfile.close()
93
 
    # TEMP: All of this is for a temporary XML format, which will later
94
 
    # change.
95
 
    worksheetsdom = subjectdom.documentElement
96
 
    worksheets = []     # List of string IDs
97
 
    for worksheetdom in worksheetsdom.childNodes:
98
 
        if worksheetdom.nodeType == worksheetdom.ELEMENT_NODE:
99
 
            # (Note: assessable will default to False, unless it is explicitly
100
 
            # set to "true").
101
 
            if worksheetdom.getAttribute("assessable") == "true":
102
 
                assessable_worksheets.append(worksheetdom.getAttribute("id"))
103
 
 
104
 
    return assessable_worksheets
105
 
 
106
 
def get_marks_header(worksheets):
107
 
    """
108
 
    Given a list of strings - the assessable worksheets - returns a new list
109
 
    of strings - the column headings for the marks section of the CSV output.
110
 
    """
111
 
    return worksheets + ["Total %", "Mark"]
112
 
 
113
 
def get_marks_user(subject, worksheets, user):
114
 
    """
115
 
    Given a subject, a list of strings (the assessable worksheets), and a user
116
 
    object, returns the user's percentage for each worksheet, overall, and
 
50
def get_header(worksheets):
 
51
    """
 
52
    Given a list of Worksheet objects (the assessable worksheets), returns a
 
53
    list of strings -- the column headings for the marks section of the CSV
 
54
    output.
 
55
    """
 
56
    return (userdata_header + [ws.name for ws in worksheets]
 
57
            + ["Total %", "Mark"])
 
58
 
 
59
def get_marks_user(worksheets, user, as_of=None):
 
60
    """Gets marks for a particular user for a particular set of worksheets.
 
61
    @param worksheets: List of Worksheet objects to get marks for.
 
62
    @param user: User to get marks for.
 
63
    @param as_of: Optional datetime. If supplied, gets the marks as of as_of.
 
64
    @returns: The user's percentage for each worksheet, overall, and
117
65
    their final mark, as a list of strings, in a manner which corresponds to
118
66
    the headings produced by get_marks_header.
119
67
    """
120
 
    # NOTE: This code is copy/edited from
121
 
    # www/apps/tutorial/__init__.py:handle_subject_menu
122
 
    # Should be factored out of there.
123
 
 
124
68
    worksheet_pcts = []
125
69
    # As we go, calculate the total score for this subject
126
70
    # (Assessable worksheets only, mandatory problems only)
128
72
    problems_total = 0
129
73
 
130
74
    for worksheet in worksheets:
131
 
        try:
132
 
            # We simply ignore optional exercises here
133
 
            mand_done, mand_total, _, _ = (
134
 
                db.calculate_score_worksheet(user.login, subject,
135
 
                    worksheet))
 
75
        # We simply ignore optional exercises here
 
76
        mand_done, mand_total, _, _ = (
 
77
            ivle.worksheet.utils.calculate_score(store, user, worksheet,
 
78
                                                 as_of))
 
79
        if mand_total > 0:
136
80
            worksheet_pcts.append(float(mand_done) / mand_total)
137
 
            problems_done += mand_done
138
 
            problems_total += mand_total
139
 
        except ivle.db.DBException:
140
 
            # Worksheet is probably not in database yet
141
 
            pass
142
 
    problems_pct = float(problems_done) / problems_total
143
 
    problems_pct_int = (100 * problems_done) / problems_total
144
 
    # XXX Marks calculation (should be abstracted out of here!)
145
 
    # percent / 16, rounded down, with a maximum mark of 5
146
 
    max_mark = 5
147
 
    mark = min(problems_pct_int / 16, max_mark)
148
 
    return worksheet_pcts + [problems_pct, mark]
 
81
        else:
 
82
            # Avoid Div0, just give everyone 0 marks if there are none
 
83
            worksheet_pcts.append(0.0)
 
84
        problems_done += mand_done
 
85
        problems_total += mand_total
 
86
    percent, mark, _ = (
 
87
        ivle.worksheet.utils.calculate_mark(problems_done, problems_total))
 
88
    return worksheet_pcts + [float(percent)/100, mark]
149
89
 
150
 
def writeuser(subject, worksheets, user, csvfile):
 
90
def writeuser(worksheets, user, csvfile, cutoff=None):
151
91
    userdata = get_userdata(user)
152
 
    marksdata = get_marks_user(subject, worksheets, user)
153
 
    csvfile.writerow(userdata + marksdata)
154
 
 
155
 
try:
156
 
    # Get the list of assessable worksheets from the subject.xml file,
157
 
    # and the list of all users from the DB.
158
 
    worksheets = get_assessable_worksheets(subject)
159
 
    db = ivle.db.DB()
160
 
    list = db.get_users()
161
 
except Exception, message:
162
 
    print >>sys.stderr, "Error: " + str(message)
163
 
    sys.exit(1)
164
 
 
165
 
# Start writing the CSV file - header
166
 
csvfile = csv.writer(sys.stdout)
167
 
csvfile.writerow(userdata_header + get_marks_header(worksheets))
168
 
 
169
 
list.sort(key=lambda user: user.login)
170
 
for user in list:
171
 
    writeuser(subject, worksheets, user, csvfile)
172
 
 
 
92
    marksdata = get_marks_user(worksheets, user, cutoff)
 
93
    data = userdata + marksdata
 
94
    # CSV writer can't handle non-ASCII characters. Encode to UTF-8.
 
95
    data = [unicode(x).encode('utf-8') for x in data]
 
96
    csvfile.writerow(data)
 
97
 
 
98
def main(argv=None):
 
99
    global store
 
100
    if argv is None:
 
101
        argv = sys.argv
 
102
 
 
103
    usage = """usage: %prog [options] subject
 
104
    (requires root)
 
105
    Reports each student's marks for a given subject offering."""
 
106
 
 
107
    # Parse arguments
 
108
    parser = optparse.OptionParser(usage)
 
109
    parser.add_option("-s", "--semester",
 
110
        action="store", dest="semester", metavar="YEAR/SEMESTER",
 
111
        help="Semester of the subject's offering (eg. 2009/1). "
 
112
             "Defaults to the currently active semester.",
 
113
        default=None)
 
114
    parser.add_option("-c", "--cutoff",
 
115
        action="store", dest="cutoff", metavar="DATE",
 
116
        help="Cutoff date (calculate the marks as of this date). "
 
117
             "YYYY-MM-DD H:M:S.",
 
118
        default=None)
 
119
    (options, args) = parser.parse_args(argv[1:])
 
120
 
 
121
    if len(args) < 1:
 
122
        parser.print_help()
 
123
        parser.exit()
 
124
 
 
125
    subject_name = unicode(args[0])
 
126
 
 
127
    if options.semester is None:
 
128
        year, semester = None, None
 
129
    else:
 
130
        try:
 
131
            year, semester = options.semester.split('/')
 
132
            if len(year) == 0 or len(semester) == 0:
 
133
                raise ValueError()
 
134
        except ValueError:
 
135
            parser.error('Invalid semester (must have form "year/semester")')
 
136
 
 
137
    if options.cutoff is not None:
 
138
        try:
 
139
            cutoff = datetime.datetime.strptime(options.cutoff,
 
140
                                                "%Y-%m-%d %H:%M:%S")
 
141
        except ValueError:
 
142
            parser.error("Invalid date format: '%s' "
 
143
                         "(must be YYYY-MM-DD H:M:S)." % options.cutoff)
 
144
    else:
 
145
        cutoff = None
 
146
 
 
147
    store = ivle.database.get_store(ivle.config.Config(plugins=False))
 
148
 
 
149
    # Get the subject from the DB
 
150
    subject = store.find(ivle.database.Subject,
 
151
                     ivle.database.Subject.short_name == subject_name).one()
 
152
    if subject is None:
 
153
        print >>sys.stderr, "No subject with short name '%s'" % subject_name
 
154
        return 1
 
155
 
 
156
    # Get the offering from the DB
 
157
    if semester is None:
 
158
        # None specified - get the current offering from the DB
 
159
        offerings = list(subject.active_offerings())
 
160
        if len(offerings) == 0:
 
161
            print >>sys.stderr, ("No active offering for subject '%s'"
 
162
                                 % subject_name)
 
163
            return 1
 
164
        elif len(offerings) > 1:
 
165
            print >>sys.stderr, ("Multiple active offerings for subject '%s':"
 
166
                                 % subject_name)
 
167
            print >>sys.stderr, "Please use one of:"
 
168
            for offering in offerings:
 
169
                print >>sys.stderr, ("    --semester=%s/%s"
 
170
                    % (offering.semester.year, offering.semester.semester))
 
171
            return 1
 
172
        else:
 
173
            offering = offerings[0]
 
174
    else:
 
175
        # Get the offering for the specified semester
 
176
        offering = subject.offering_for_semester(year, semester)
 
177
        if offering is None:
 
178
            print >>sys.stderr, (
 
179
                "No offering for subject '%s' in semester %s/%s"
 
180
                % (subject_name, year, semester))
 
181
            return 1
 
182
 
 
183
    # Get the list of assessable worksheets
 
184
    worksheets = offering.worksheets.find(assessable=True)
 
185
 
 
186
    # Start writing the CSV file - header
 
187
    csvfile = csv.writer(sys.stdout)
 
188
    csvfile.writerow(get_header(worksheets))
 
189
 
 
190
    # Get all users enrolled in this offering
 
191
    users = store.find(ivle.database.User,
 
192
                   ivle.database.User.id == ivle.database.Enrolment.user_id,
 
193
                   offering.id == ivle.database.Enrolment.offering).order_by(
 
194
                        ivle.database.User.login)
 
195
    for user in users:
 
196
        writeuser(worksheets, user, csvfile, cutoff)
 
197
 
 
198
if __name__ == "__main__":
 
199
    sys.exit(main(sys.argv))