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

« back to all changes in this revision

Viewing changes to bin/ivle-fetchsubmissions

  • Committer: William Grant
  • Date: 2009-05-13 03:15:26 UTC
  • mto: This revision was merged to the branch mainline in revision 1247.
  • Revision ID: grantw@unimelb.edu.au-20090513031526-l3knlkpgnldp6dgj
Attempting to view an SVN log when the repo is empty will not crash now.

svnlogservice now returns a 404 if the revision isn't found.

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