~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:42:24 UTC
  • mto: This revision was merged to the branch mainline in revision 1247.
  • Revision ID: matt.giuca@gmail.com-20090512144224-fdj5g9dfuajuslxo
ivle-fetchsubmissions: Solo project now shows "Submitter" not "Author".

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