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

« back to all changes in this revision

Viewing changes to bin/ivle-fetchsubmissions

Now respects --zip, and creates Zip files.

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