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

« back to all changes in this revision

Viewing changes to bin/ivle-fetchsubmissions

  • Committer: William Grant
  • Date: 2010-02-23 08:55:42 UTC
  • mto: This revision was merged to the branch mainline in revision 1674.
  • Revision ID: grantw@unimelb.edu.au-20100223085542-r8xw14bxxoraza51
Permit underscores in all names.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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
 
 
25
from __future__ import with_statement
 
26
 
 
27
import sys
 
28
import os
 
29
import shutil
 
30
import datetime
 
31
import codecs
 
32
import optparse
 
33
import zipfile
 
34
import traceback
 
35
import locale
 
36
 
 
37
import pysvn
 
38
 
 
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
 
45
import ivle.util
 
46
 
 
47
from ivle.database import Project, ProjectSet, Offering, Subject
 
48
 
 
49
# Set locale to UTF-8
 
50
locale.setlocale(locale.LC_CTYPE, "en_US.UTF-8")
 
51
 
 
52
# Is Python version 2.6 or higher?
 
53
PYTHON26 = map(int, sys.version[:3].split('.')) >= [2, 6]
 
54
 
 
55
def fetch_submission(submission, target, svnclient, config, zip=False,
 
56
    txt=False, verbose=False):
 
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).
 
62
    @param svnclient: pysvn.Client object.
 
63
    @param config: Config object.
 
64
    @param zip: If True, zips up the submission.
 
65
    @param txt: If True, writes an extra text file with metadata about the
 
66
        submission.
 
67
    @param verbose: If True, writes the name of each submission to stdout.
 
68
    """
 
69
    # submission_name is the name of the user or group who owns the repo
 
70
    submission_name = submission.assessed.principal.short_name
 
71
    # target_final is the directory to place the files in
 
72
    target_final = os.path.join(target,
 
73
                                submission.assessed.principal.short_name)
 
74
    target_final = target_final.encode('utf-8')
 
75
    if os.path.exists(target_final):
 
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)
 
81
    url = get_repo_url(submission, config)
 
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)
 
86
 
 
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)
 
91
 
 
92
    # If required, zip up the directory
 
93
    if zip:
 
94
        make_zip(target_final, target_final + ".zip")
 
95
        # Remove the target tree
 
96
        if os.path.isdir(target_final):
 
97
            ivle.util.safe_rmtree(target_final)
 
98
        else:
 
99
            os.remove(target_final)
 
100
 
 
101
    if verbose:
 
102
        print "Exported submission by: %s (%s)" % (
 
103
            submission.assessed.principal.short_name,
 
104
            submission.assessed.principal.display_name)
 
105
 
 
106
def get_repo_url(submission, config):
 
107
    """Gets a local (file:) URL to the repository for a given submission.
 
108
    This will consult submission.path to find the path within the repository
 
109
    to check out.
 
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
 
117
        groupname = submission.assessed.principal.short_name
 
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
 
126
        username = submission.assessed.principal.short_name
 
127
        repo_path = os.path.join(config['paths']['svn']['repo_path'],
 
128
                                'users', username)
 
129
 
 
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
 
 
135
    # Attach "file://" to the front of the absolute path, to make it a URL
 
136
    return "file://" + os.path.join(os.path.abspath(repo_path), path_in_repo)
 
137
 
 
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).
 
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).
 
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)
 
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))
 
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):
 
165
            arc_dirpath = ivle.util.relpath(dirpath, relativeto)
 
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:
 
171
                zip.write(os.path.join(dirpath, filename),
 
172
                            os.path.join(arc_dirpath, filename))
 
173
 
 
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
 
181
    zip.close()
 
182
 
 
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)
 
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)
 
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
 
 
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)
 
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
    """
 
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=".")
 
235
    parser.add_option("-z", "--zip",
 
236
        action="store_true", dest="zip",
 
237
        help="Store each submission in a Zip file.",
 
238
        default=False)
 
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)
 
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)
 
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
 
 
266
    svnclient = pysvn.Client()
 
267
    config = ivle.config.Config(plugins=False)
 
268
    store = ivle.database.get_store(config)
 
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:
 
320
        try:
 
321
            fetch_submission(submission, target_dir, svnclient, config,
 
322
                             zip=options.zip, txt=options.txt,
 
323
                             verbose=options.verbose)
 
324
        except Exception, e:
 
325
            # Catch all exceptions (to ensure if one student has a problem, it
 
326
            # is reported, and we can continue)
 
327
            print >>sys.stderr, "ERROR on submission for %s:" % (
 
328
                submission.assessed.principal.display_name)
 
329
            traceback.print_exc()
 
330
 
 
331
if __name__ == "__main__":
 
332
    sys.exit(main(sys.argv))