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

1165.3.48 by Matt Giuca
Added new script ivle-fetchsubmissions, which pulls submissions from students
1
#!/usr/bin/env python
2
# IVLE - Informatics Virtual Learning Environment
3
# Copyright (C) 2007-2009 The University of Melbourne
4
#
5
# This program is free software; you can redistribute it and/or modify
6
# it under the terms of the GNU General Public License as published by
7
# the Free Software Foundation; either version 2 of the License, or
8
# (at your option) any later version.
9
#
10
# This program is distributed in the hope that it will be useful,
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with this program; if not, write to the Free Software
17
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
18
19
# Program: Fetch Submissions
20
# Author:  Matt Giuca
21
22
# Script to retrieve all submissions for a particular project.
23
# Requires root to run.
24
1165.3.71 by Matt Giuca
ivle-fetchsubmissions: Added the creation of a text file which contains
25
from __future__ import with_statement
26
1165.3.48 by Matt Giuca
Added new script ivle-fetchsubmissions, which pulls submissions from students
27
import sys
28
import os
1165.3.58 by Matt Giuca
Now respects --zip, and creates Zip files.
29
import shutil
1165.3.48 by Matt Giuca
Added new script ivle-fetchsubmissions, which pulls submissions from students
30
import datetime
1165.3.71 by Matt Giuca
ivle-fetchsubmissions: Added the creation of a text file which contains
31
import codecs
1165.3.48 by Matt Giuca
Added new script ivle-fetchsubmissions, which pulls submissions from students
32
import optparse
1165.3.58 by Matt Giuca
Now respects --zip, and creates Zip files.
33
import zipfile
1165.3.60 by Matt Giuca
Added printing of full traceback when an error is caught.
34
import traceback
1165.3.48 by Matt Giuca
Added new script ivle-fetchsubmissions, which pulls submissions from students
35
1165.3.53 by Matt Giuca
ivle-fetchsubmissions: Now actually svn exports the submissions to the target
36
import pysvn
37
1165.3.48 by Matt Giuca
Added new script ivle-fetchsubmissions, which pulls submissions from students
38
if os.getuid() != 0:
39
    print >>sys.stderr, "Must run %s as root." % os.path.basename(sys.argv[0])
40
    sys.exit()
41
42
import ivle.config
43
import ivle.database
1165.3.75 by Matt Giuca
ivle/util.py: Added safe_rmtree, copied from Python2.6 shutil.rmtree (which
44
import ivle.util
1165.3.48 by Matt Giuca
Added new script ivle-fetchsubmissions, which pulls submissions from students
45
46
from ivle.database import Project, ProjectSet, Offering, Subject
47
1165.3.80 by Matt Giuca
ivle-fetchsubmissions: In a stunningly awful hack, detects pre-Python2.6 and
48
# Is Python version 2.6 or higher?
49
PYTHON26 = map(int, sys.version[:3].split('.')) >= [2, 6]
50
1165.3.72 by Matt Giuca
ivle-fetchsubmissions: Added --no-txt option to disable writing the text files.
51
def fetch_submission(submission, target, svnclient, config, zip=False,
1165.3.86 by Matt Giuca
ivle-fetchsubmissions: Added -v option to print out submissions to stdout.
52
    txt=False, verbose=False):
1165.3.48 by Matt Giuca
Added new script ivle-fetchsubmissions, which pulls submissions from students
53
    """Fetch a submission from a user's repository, and dump it in a given
54
    directory.
55
    @param submission: Submission object, detailing the submission.
56
    @param target: Target directory for the project (will create a
57
        subdirectory for each submission).
1165.3.58 by Matt Giuca
Now respects --zip, and creates Zip files.
58
    @param svnclient: pysvn.Client object.
1165.3.49 by Matt Giuca
ivle.database.Assessed: Added property is_group.
59
    @param config: Config object.
1165.3.58 by Matt Giuca
Now respects --zip, and creates Zip files.
60
    @param zip: If True, zips up the submission.
1165.3.72 by Matt Giuca
ivle-fetchsubmissions: Added --no-txt option to disable writing the text files.
61
    @param txt: If True, writes an extra text file with metadata about the
62
        submission.
1165.3.86 by Matt Giuca
ivle-fetchsubmissions: Added -v option to print out submissions to stdout.
63
    @param verbose: If True, writes the name of each submission to stdout.
1165.3.48 by Matt Giuca
Added new script ivle-fetchsubmissions, which pulls submissions from students
64
    """
65
    # submission_name is the name of the user or group who owns the repo
1165.3.74 by Matt Giuca
ivle-fetchsubmissions: Replaced use of .name with .short_name (or it breaks on User submissions).
66
    submission_name = submission.assessed.principal.short_name
1165.3.48 by Matt Giuca
Added new script ivle-fetchsubmissions, which pulls submissions from students
67
    # target_final is the directory to place the files in
1165.3.74 by Matt Giuca
ivle-fetchsubmissions: Replaced use of .name with .short_name (or it breaks on User submissions).
68
    target_final = os.path.join(target,
69
                                submission.assessed.principal.short_name)
1165.3.79 by Matt Giuca
Encode zip filenames as UTF-8, not ASCII.
70
    target_final = target_final.encode('utf-8')
1165.3.76 by Matt Giuca
ivle-fetchsubmissions: Rather than creating the target dir, now DELETES it
71
    if os.path.exists(target_final):
1165.3.82 by Matt Giuca
ivle-fetchsubmissions: No longer breaks if trying to remove the target tree and it is a file, rather than a directory.
72
        # Remove the existing file or directory before re-checking out
73
        if os.path.isdir(target_final):
74
            ivle.util.safe_rmtree(target_final)
75
        else:
76
            os.remove(target_final)
1165.3.49 by Matt Giuca
ivle.database.Assessed: Added property is_group.
77
    url = get_repo_url(submission, config)
1165.3.53 by Matt Giuca
ivle-fetchsubmissions: Now actually svn exports the submissions to the target
78
    revision = pysvn.Revision(pysvn.opt_revision_kind.number,
79
                              submission.revision)
80
    svnclient.export(url, target_final, force=True,
81
        revision=revision, recurse=True)
1165.3.49 by Matt Giuca
ivle.database.Assessed: Added property is_group.
82
1165.3.72 by Matt Giuca
ivle-fetchsubmissions: Added --no-txt option to disable writing the text files.
83
    if txt:
84
        # info_final is the directory to place the metadata info files in
85
        info_final = target_final + ".txt"
86
        write_submission_info(info_final, submission)
1165.3.71 by Matt Giuca
ivle-fetchsubmissions: Added the creation of a text file which contains
87
1165.3.58 by Matt Giuca
Now respects --zip, and creates Zip files.
88
    # If required, zip up the directory
89
    if zip:
90
        make_zip(target_final, target_final + ".zip")
91
        # Remove the target tree
1165.3.82 by Matt Giuca
ivle-fetchsubmissions: No longer breaks if trying to remove the target tree and it is a file, rather than a directory.
92
        if os.path.isdir(target_final):
93
            ivle.util.safe_rmtree(target_final)
94
        else:
95
            os.remove(target_final)
1165.3.58 by Matt Giuca
Now respects --zip, and creates Zip files.
96
1165.3.86 by Matt Giuca
ivle-fetchsubmissions: Added -v option to print out submissions to stdout.
97
    if verbose:
98
        print "Exported submission by: %s (%s)" % (
99
            submission.assessed.principal.short_name,
100
            submission.assessed.principal.display_name)
101
1165.3.49 by Matt Giuca
ivle.database.Assessed: Added property is_group.
102
def get_repo_url(submission, config):
103
    """Gets a local (file:) URL to the repository for a given submission.
1165.3.50 by Matt Giuca
ivle-fetchsubmissions: Now gets the URL including the path within the
104
    This will consult submission.path to find the path within the repository
105
    to check out.
1165.3.49 by Matt Giuca
ivle.database.Assessed: Added property is_group.
106
    @param submission: Submission object, detailing the submission.
107
    @param config: Config object.
108
    """
109
    # NOTE: This code is mostly copied from services/usrmgt-server
110
    if submission.assessed.is_group:
111
        # The offering this group is in
112
        offering = submission.assessed.project.project_set.offering
1165.3.74 by Matt Giuca
ivle-fetchsubmissions: Replaced use of .name with .short_name (or it breaks on User submissions).
113
        groupname = submission.assessed.principal.short_name
1165.3.49 by Matt Giuca
ivle.database.Assessed: Added property is_group.
114
        # The name of the repository directory within 'groups' is
115
        # SUBJECT_YEAR_SEMESTER_GROUP
116
        namespace = "_".join([offering.subject.short_name,
117
            offering.semester.year, offering.semester.semester, groupname])
118
        repo_path = os.path.join(config['paths']['svn']['repo_path'],
119
                                'groups', namespace)
120
    else:
121
        # The name of the repository directory within 'users' is the username
1165.3.74 by Matt Giuca
ivle-fetchsubmissions: Replaced use of .name with .short_name (or it breaks on User submissions).
122
        username = submission.assessed.principal.short_name
1165.3.49 by Matt Giuca
ivle.database.Assessed: Added property is_group.
123
        repo_path = os.path.join(config['paths']['svn']['repo_path'],
124
                                'users', username)
125
1165.3.50 by Matt Giuca
ivle-fetchsubmissions: Now gets the URL including the path within the
126
    path_in_repo = submission.path
127
    # Change an absolute path into a relative one (to the top of SVN)
128
    if path_in_repo[:1] == os.sep or path_in_repo[:1] == os.altsep:
129
        path_in_repo = path_in_repo[1:]
130
1165.3.49 by Matt Giuca
ivle.database.Assessed: Added property is_group.
131
    # Attach "file://" to the front of the absolute path, to make it a URL
1165.3.50 by Matt Giuca
ivle-fetchsubmissions: Now gets the URL including the path within the
132
    return "file://" + os.path.join(os.path.abspath(repo_path), path_in_repo)
1165.3.48 by Matt Giuca
Added new script ivle-fetchsubmissions, which pulls submissions from students
133
1165.3.58 by Matt Giuca
Now respects --zip, and creates Zip files.
134
def make_zip(source, dest):
135
    """Zip up a directory tree or file. The ZIP file will always contain just
136
    a single directory or file at the top level (it will not be a ZIP bomb).
1165.3.80 by Matt Giuca
ivle-fetchsubmissions: In a stunningly awful hack, detects pre-Python2.6 and
137
    XXX In Python 2.5 and earlier, this does NOT create empty directories
138
    (it's not possible with the Python2.5 version of zipfile).
1165.3.58 by Matt Giuca
Now respects --zip, and creates Zip files.
139
    @param source: Path to a directory or file to zip up.
140
    @param dest: Path to a zip file to create.
141
    """
142
    # NOTE: This code is mostly copied from ivle.zip (but very different)
143
    zip = zipfile.ZipFile(dest, 'w')
144
145
    # Write the source file/directory itself
146
    # (If this is a directory it will NOT recurse)
1165.3.80 by Matt Giuca
ivle-fetchsubmissions: In a stunningly awful hack, detects pre-Python2.6 and
147
    if PYTHON26 or not os.path.isdir(source):
148
        # Python < 2.6 errors if you add a directory
149
        # (This means you can't add an empty directory)
150
        zip.write(source, os.path.basename(source))
1165.3.58 by Matt Giuca
Now respects --zip, and creates Zip files.
151
152
    if os.path.isdir(source):
153
        # All paths within the zip file are relative to relativeto
154
        relativeto = os.path.dirname(source)
155
        # Write the top-level directory
156
        # Walk the directory tree
157
        def error(err):
158
            raise OSError("Could not access a file (zipping)")
159
        for (dirpath, dirnames, filenames) in \
160
            os.walk(source, onerror=error):
1165.3.81 by Matt Giuca
ivle.util: Backported os.path.relpath from Python2.6 (required).
161
            arc_dirpath = ivle.util.relpath(dirpath, relativeto)
1165.3.80 by Matt Giuca
ivle-fetchsubmissions: In a stunningly awful hack, detects pre-Python2.6 and
162
            # Python < 2.6 errors if you add a directory
163
            # (This means you can't add an empty directory)
164
            if PYTHON26:
165
                filenames = dirnames + filenames
166
            for filename in filenames:
1165.3.58 by Matt Giuca
Now respects --zip, and creates Zip files.
167
                zip.write(os.path.join(dirpath, filename),
168
                            os.path.join(arc_dirpath, filename))
169
1165.3.80 by Matt Giuca
ivle-fetchsubmissions: In a stunningly awful hack, detects pre-Python2.6 and
170
    if not PYTHON26:
171
        # XXX Write this _didModify attribute of zip, to trick it into writing
172
        # footer bytes even if there are no files in the archive (otherwise it
173
        # writes a 0-byte archive which is invalid).
174
        # Note that in Python2.6 we avoid this by always writing the top-level
175
        # file or directory, at least.
176
        zip._didModify = True
1165.3.58 by Matt Giuca
Now respects --zip, and creates Zip files.
177
    zip.close()
178
1165.3.71 by Matt Giuca
ivle-fetchsubmissions: Added the creation of a text file which contains
179
def write_submission_info(filename, submission):
180
    """Write human-readable meta-data about a submission to a file.
181
    @param filename: Filename to write to.
182
    @param submission: Submission object.
183
    """
184
    with codecs.open(filename, 'w', 'utf-8') as f:
185
        if submission.assessed.is_group:
186
            # A group project
187
            print >>f, "Group: %s (%s)" % (
188
                submission.assessed.principal.short_name,
189
                submission.assessed.principal.display_name)
190
        else:
191
            # A solo project
192
            # Only show the two fields if they are different (only in rare
193
            # circumstances)
1165.3.77 by Matt Giuca
ivle-fetchsubmissions: Solo project now shows "Submitter" not "Author".
194
            if submission.assessed.principal != submission.submitter:
195
                print >>f, "Author: %s (%s)" % (
196
                    submission.assessed.principal.short_name,
197
                    submission.assessed.principal.display_name)
198
        print >>f, "Submitter: %s (%s)" % (
199
            submission.submitter.short_name,
200
            submission.submitter.display_name)
1165.3.71 by Matt Giuca
ivle-fetchsubmissions: Added the creation of a text file which contains
201
        print >>f, "Date: %s" % (
202
            submission.date_submitted.strftime("%Y-%m-%d %H:%M:%S"))
203
        print >>f, "SVN Revision: %s" % submission.revision
204
        print >>f, "SVN Path: %s" % submission.path
205
1165.3.48 by Matt Giuca
Added new script ivle-fetchsubmissions, which pulls submissions from students
206
def main(argv=None):
207
    global store
208
    if argv is None:
209
        argv = sys.argv
210
211
    usage = """usage: %prog [options] subject projname
212
    (requires root)
1165.3.56 by Matt Giuca
Better instructions in the usage message.
213
    Retrieves all submissions for a given project. Places each submission in
214
    its own directory, in a subdirectory of '.'. Any errors are reported to
215
    stderr (otherwise is silent).
216
    subject/projname is the subject/project's short name.
217
    """
1165.3.48 by Matt Giuca
Added new script ivle-fetchsubmissions, which pulls submissions from students
218
219
    # Parse arguments
220
    parser = optparse.OptionParser(usage)
221
    parser.add_option("-s", "--semester",
222
        action="store", dest="semester", metavar="YEAR/SEMESTER",
223
        help="Semester of the subject's offering (eg. 2009/1). "
224
             "Defaults to the currently active semester.",
225
        default=None)
226
    parser.add_option("-d", "--dest",
227
        action="store", dest="dest", metavar="PATH",
228
        help="Destination directory (default to '.', creates a subdirectory, "
229
            "so will not pollute PATH).",
230
        default=".")
1165.3.57 by Matt Giuca
Added --zip option, to turn on zip files (no effect).
231
    parser.add_option("-z", "--zip",
232
        action="store_true", dest="zip",
233
        help="Store each submission in a Zip file.",
234
        default=False)
1165.3.86 by Matt Giuca
ivle-fetchsubmissions: Added -v option to print out submissions to stdout.
235
    parser.add_option("-v", "--verbose",
236
        action="store_true", dest="verbose",
237
        help="Print out the name of each submission as it is extracted.",
238
        default=False)
1165.3.72 by Matt Giuca
ivle-fetchsubmissions: Added --no-txt option to disable writing the text files.
239
    parser.add_option("--no-txt",
240
        action="store_false", dest="txt",
241
        help="Disable writing a text file with data about each submission.",
242
        default=True)
1165.3.48 by Matt Giuca
Added new script ivle-fetchsubmissions, which pulls submissions from students
243
    (options, args) = parser.parse_args(argv[1:])
244
245
    if len(args) < 2:
246
        parser.print_help()
247
        parser.exit()
248
249
    subject_name = unicode(args[0])
250
    project_name = unicode(args[1])
251
252
    if options.semester is None:
253
        year, semester = None, None
254
    else:
255
        try:
256
            year, semester = options.semester.split('/')
257
            if len(year) == 0 or len(semester) == 0:
258
                raise ValueError()
259
        except ValueError:
260
            parser.error('Invalid semester (must have form "year/semester")')
261
1165.3.53 by Matt Giuca
ivle-fetchsubmissions: Now actually svn exports the submissions to the target
262
    svnclient = pysvn.Client()
1165.3.49 by Matt Giuca
ivle.database.Assessed: Added property is_group.
263
    config = ivle.config.Config(plugins=False)
264
    store = ivle.database.get_store(config)
1165.3.48 by Matt Giuca
Added new script ivle-fetchsubmissions, which pulls submissions from students
265
266
    # Get the subject from the DB
267
    subject = store.find(Subject,
268
                     Subject.short_name == subject_name).one()
269
    if subject is None:
270
        print >>sys.stderr, "No subject with short name '%s'" % subject_name
271
        return 1
272
273
    # Get the offering from the DB
274
    if semester is None:
275
        # None specified - get the current offering from the DB
276
        offerings = list(subject.active_offerings())
277
        if len(offerings) == 0:
278
            print >>sys.stderr, ("No active offering for subject '%s'"
279
                                 % subject_name)
280
            return 1
281
        elif len(offerings) > 1:
282
            print >>sys.stderr, ("Multiple active offerings for subject '%s':"
283
                                 % subject_name)
284
            print >>sys.stderr, "Please use one of:"
285
            for offering in offerings:
286
                print >>sys.stderr, ("    --semester=%s/%s"
287
                    % (offering.semester.year, offering.semester.semester))
288
            return 1
289
        else:
290
            offering = offerings[0]
291
    else:
292
        # Get the offering for the specified semester
293
        offering = subject.offering_for_semester(year, semester)
294
        if offering is None:
295
            print >>sys.stderr, (
296
                "No offering for subject '%s' in semester %s/%s"
297
                % (subject_name, year, semester))
298
            return 1
299
300
    # Get the project from the DB
301
    project = store.find(Project,
302
                         Project.project_set_id == ProjectSet.id,
303
                         ProjectSet.offering == offering,
304
                         Project.short_name == project_name).one()
305
    if project is None:
306
        print >>sys.stderr, "No project with short name '%s'" % project_name
307
        return 1
308
309
    # Target directory is DEST/subject/year/semester/project
310
    target_dir = os.path.join(options.dest, subject_name,
311
        offering.semester.year, offering.semester.semester, project_name)
312
    if not os.path.exists(target_dir):
313
        os.makedirs(target_dir)
314
315
    for submission in project.latest_submissions:
1165.3.55 by Matt Giuca
ivle-fetchsubmissions: Added error handling for each submission (so as not to
316
        try:
1165.3.58 by Matt Giuca
Now respects --zip, and creates Zip files.
317
            fetch_submission(submission, target_dir, svnclient, config,
1165.3.86 by Matt Giuca
ivle-fetchsubmissions: Added -v option to print out submissions to stdout.
318
                             zip=options.zip, txt=options.txt,
319
                             verbose=options.verbose)
1165.3.55 by Matt Giuca
ivle-fetchsubmissions: Added error handling for each submission (so as not to
320
        except Exception, e:
321
            # Catch all exceptions (to ensure if one student has a problem, it
322
            # is reported, and we can continue)
1165.3.60 by Matt Giuca
Added printing of full traceback when an error is caught.
323
            print >>sys.stderr, "ERROR on submission for %s:" % (
324
                submission.assessed.principal.display_name)
1165.3.73 by William Grant
ivle-fetchsubmissions: Don't give traceback.print_exc() an extra arg.
325
            traceback.print_exc()
1165.3.48 by Matt Giuca
Added new script ivle-fetchsubmissions, which pulls submissions from students
326
327
if __name__ == "__main__":
328
    sys.exit(main(sys.argv))