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

« back to all changes in this revision

Viewing changes to bin/ivle-fetchsubmissions

  • Committer: Matt Giuca
  • Date: 2009-05-12 14:36:08 UTC
  • mto: This revision was merged to the branch mainline in revision 1247.
  • Revision ID: matt.giuca@gmail.com-20090512143608-94oi7lkkzoougv09
ivle/util.py: Added safe_rmtree, copied from Python2.6 shutil.rmtree (which
    has a bugfix to avoid following symlinks). Don't use shutil.rmtree.
bin/ivle-fetchsubmissions: Replaced call to shutil.rmtree with safe_rmtree.

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