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