Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/Pootle-2.0.0/local_apps/pootle_app/views/language/tp_common.py
blob: b15c526688ad06023d4d1fa4a18972a8c67b71ca (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright 2009 Zuza Software Foundation
#
# This file is part of Pootle.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <http://www.gnu.org/licenses/>.

import logging
import os
import StringIO
import subprocess
import zipfile
import datetime

from django import forms
from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import PermissionDenied

from translate.storage import factory, versioncontrol

from pootle_app.lib import view_handler
from pootle_app.project_tree import scan_translation_project_files
from pootle_app.models import Submission
from pootle_app.models.profile     import get_profile
from pootle_app.models.permissions import check_permission
from pootle_app.models.signals import post_file_upload
from pootle_app.views.language import item_dict
from pootle_app.views.language import search_forms
from pootle_app.views.top_stats import gentopstats
from pootle_store.models import Store
from pootle_store.util import absolute_real_path, relative_real_path

def top_stats(translation_project):
    return gentopstats(lambda query: query.filter(translation_project=translation_project))

def get_children(request, translation_project, directory, links_required=None):
    search = search_forms.search_from_request(request)
    return [item_dict.make_directory_item(request, child_dir, links_required=links_required)
            for child_dir in directory.child_dirs.all()] + \
           [item_dict.make_store_item(request, child_store, links_required=links_required)
            for child_store in directory.filter_stores(search).all()]

def unix_to_host_path(p):
    return os.sep.join(p.split('/'))

def host_to_unix_path(p):
    return '/'.join(p.split(os.sep))

def get_upload_path(translation_project, relative_root_dir, local_filename):
    """gets the path of a translation file being uploaded securely,
    creating directories as neccessary"""
    if os.path.basename(local_filename) != local_filename or local_filename.startswith("."):
        raise ValueError(_("Invalid/insecure file name: %s", local_filename))
    # XXX: Leakage of the project layout information outside of
    # project_tree.py! The rest of Pootle shouldn't have to care
    # whether something is GNU-style or not.
    if translation_project.file_style == "gnu" and not translation_project.is_template_project:
        if os.path.splitext(local_filename)[0] != translation_project.language.code:
            raise ValueError(_("Invalid GNU-style file name: %(local_filename)s. It must match '%(langcode)s.%(filetype)s'.",
                             { 'local_filename': local_filename,
                               'langcode': translation_project.language.code,
                               'filetype': translation_project.project.localfiletype,
                               }))
    dir_path = os.path.join(translation_project.real_path, unix_to_host_path(relative_root_dir))
    return relative_real_path(os.path.join(dir_path, local_filename))

def get_local_filename(translation_project, upload_filename):
    base, ext = os.path.splitext(upload_filename)
    new_ext = translation_project.project.localfiletype
    if new_ext == 'po' and translation_project.is_template_project:
        new_ext = 'pot'
    return '%s.%s' % (base, new_ext)

def unzip_external(request, relative_root_dir, django_file, overwrite):
    from tempfile import mkdtemp, mkstemp
    # Make a temporary directory to hold a zip file and its unzipped contents
    tempdir = mkdtemp(prefix='pootle')
    # Make a temporary file to hold the zip file
    tempzipfd, tempzipname = mkstemp(prefix='pootle', suffix='.zip')
    try:
        # Dump the uploaded file to the temporary file
        try:
            os.write(tempzipfd, django_file.read())
        finally:
            os.close(tempzipfd)
        # Unzip the temporary zip file
        if subprocess.call(["unzip", tempzipname, "-d", tempdir]):
            raise zipfile.BadZipfile(_("Error while extracting archive"))
        # Enumerate the temporary directory...
        for basedir, dirs, files in os.walk(tempdir):
            for fname in files:
                # Read the contents of a file...
                fcontents = open(os.path.join(basedir, fname), 'rb').read()
                # Get the filesystem path relative to the temporary directory
                relative_host_dir = basedir[len(tempdir)+len(os.sep):]
                # Construct a full UNIX path relative to the current
                # translation project URL by attaching a UNIXified
                # 'relative_host_dir' to the root relative path
                # (i.e. the path from which the user is uploading the
                # ZIP file.
                sub_relative_root_dir = os.path.join(relative_root_dir, host_to_unix_path(relative_host_dir))
                try:
                    upload_file(request, sub_relative_root_dir, fname, fcontents, overwrite)
                except ValueError, e:
                    logging.error("error adding %s\t%s", fname, e)
    finally:
        # Clean up temporary file and directory used in try-block
        import shutil
        os.unlink(tempzipname)
        shutil.rmtree(tempdir)

def unzip_python(request, relative_root_dir, django_file, overwrite):
    django_file.seek(0)
    archive = zipfile.ZipFile(django_file, 'r')
    # TODO: find a better way to return errors...
    try:
        for filename in archive.namelist():
            try:
                if filename[-1] != '/':
                    sub_relative_root_dir = os.path.join(relative_root_dir, os.path.dirname(filename))
                    newfile = StringIO.StringIO(archive.read(filename))
                    newfile.name = os.path.basename(filename)
                    upload_file(request, sub_relative_root_dir, newfile, overwrite)
            except ValueError, e:
                logging.error("error adding %s\t%s", filename, e)
    finally:
        archive.close()

def upload_archive(request, directory, django_file, overwrite):
    # First we try to use "unzip" from the system, otherwise fall back to using
    # the slower zipfile module
    try:
        unzip_external(request, directory, django_file, overwrite)
    except:
        unzip_python(request, directory, django_file, overwrite)

def overwrite_file(request, relative_root_dir, django_file, upload_path):
    """overwrite with uploaded file"""
    upload_dir = os.path.dirname(absolute_real_path(upload_path))
    # Ensure that there is a directory into which we can dump the
    # uploaded file.
    if not os.path.exists(upload_dir):
        os.makedirs(upload_dir)
        
    # Get the file extensions of the uploaded filename and the
    # current translation project
    _upload_base, upload_ext = os.path.splitext(django_file.name)
    _local_base,  local_ext  = os.path.splitext(upload_path)
    # If the extension of the uploaded file matches the extension
    # used in this translation project, then we simply write the
    # file to the disc.
    if upload_ext == local_ext:
        outfile = open(absolute_real_path(upload_path), "wb")
        try:
            outfile.write(django_file.read())
        finally:
            outfile.close()
    else:
        # If the extension of the uploaded file does not match the
        # extension of the current translation project, we create
        # an empty file (with the right extension)...
        empty_store = factory.getobject(absolute_real_path(upload_path))
        # And save it...
        empty_store.save()
        scan_translation_project_files(request.translation_project)
        # Then we open this newly created file and merge the
        # uploaded file into it.
        store = Store.objects.get(file=upload_path)
        newstore = factory.getobject(django_file)
        #FIXME: maybe there is a faster way to do this?
        store.mergefile(newstore, request.user.username, allownewstrings=True, suggestions=False, notranslate=False, obsoletemissing=False)
    
def upload_file(request, relative_root_dir, django_file, overwrite):
    # for some reason factory checks explicitly for file existance and
    # if file is open, which makes it impossible to work with Django's
    # in memory uploads.
    #
    # setting _closed to False should work around this
    #FIXME: hackish, does this have any undesirable side effect?
    if getattr(django_file, '_closed', None) is None:
        django_file._closed = False
    # factory also checks for _mode
    if getattr(django_file, '_mode', None) is None:
        django_file._mode = 1
    # mode is an attribute not a property in Django 1.1
    if getattr(django_file, 'mode', None) is None:
        django_file.mode = 1

    local_filename = get_local_filename(request.translation_project, django_file.name)
    # The full filesystem path to 'local_filename'
    upload_path    = get_upload_path(request.translation_project, relative_root_dir, local_filename)

    file_exists = os.path.exists(absolute_real_path(upload_path))
    if file_exists and overwrite == 'overwrite' and not check_permission('overwrite', request):
        raise PermissionDenied(_("You do not have rights to overwrite files here."))
    if not file_exists and not check_permission('administrate', request):
        raise PermissionDenied(_("You do not have rights to upload new files here."))
    if overwrite == 'merge' and not check_permission('translate', request):
        raise PermissionDenied(_("You do not have rights to upload files here."))
    if overwrite == 'suggest' and not check_permission('suggest', request):
        raise PermissionDenied(_("You do not have rights to upload files here."))

    if not file_exists or overwrite == 'overwrite':
        overwrite_file(request, relative_root_dir, django_file, upload_path)
        return

    store = Store.objects.get(file=upload_path)
    newstore = factory.getobject(django_file)

    #FIXME: are we sure this is what we want to do? shouldn't we
    # diffrentiate between structure changing uploads and mere
    # pretranslate uploads?
    suggestions = overwrite == 'merge'
    notranslate = overwrite == 'suggest'
    allownewstrings = check_permission('overwrite', request) or check_permission('administrate', request) or check_permission('commit', request)
    obsoletemissing = allownewstrings and overwrite == 'merge'
    store.mergefile(newstore, request.user.username, suggestions=suggestions, notranslate=notranslate,
                    allownewstrings=allownewstrings, obsoletemissing=obsoletemissing)

class UpdateHandler(view_handler.Handler):
    actions = [('do_update', _('Update all from version control'))]

    class Form(forms.Form):
        pass

    def do_update(self, request, translation_project, directory):
        translation_project.update_project(request)
        return {}

    @classmethod
    def must_display(cls, request, *args, **kwargs):
        return check_permission('commit', request) and \
            versioncontrol.hasversioning(request.translation_project.abs_real_path)

class UploadHandler(view_handler.Handler):
    actions = [('do_upload', _('Upload'))]

    @classmethod
    def must_display(cls, request, *args, **kwargs):
        return check_permission('translate', request)

    def __init__(self, request, data=None, files=None):
        choices = [('merge', _("Merge the file with the current file and turn conflicts into suggestions")),
                   ('suggest', _("Add all new translations as suggestions"))]
        if check_permission('overwrite', request):
            choices.insert(0, ('overwrite',  _("Overwrite the current file if it exists")))

        class UploadForm(forms.Form):
            file = forms.FileField(required=True, label=_('File'))
            overwrite = forms.ChoiceField(required=True, widget=forms.RadioSelect,
                                          label='', choices=choices, initial='merge')
            
        self.Form = UploadForm
        super(UploadHandler, self).__init__(request, data, files)
        self.form.allow_overwrite = check_permission('overwrite', request)
        self.form.title = _("Upload File")

    def do_upload(self, request, translation_project, directory):
        if self.form.is_valid() and 'file' in request.FILES:
            django_file = self.form.cleaned_data['file']
            overwrite = self.form.cleaned_data['overwrite']
            scan_translation_project_files(translation_project)
            oldstats = translation_project.getquickstats()
            # The URL relative to the URL of the translation project. Thus, if
            # directory.pootle_path == /af/pootle/foo/bar, then
            # relative_root_dir == foo/bar.
            relative_root_dir = directory.pootle_path[len(translation_project.directory.pootle_path):]
            if django_file.name.endswith('.zip'):
                archive = True
                upload_archive(request, relative_root_dir, django_file, overwrite)
            else:
                archive = False
                upload_file(request, relative_root_dir, django_file, overwrite)
            scan_translation_project_files(translation_project)
            newstats = translation_project.getquickstats()

            # create a submission, doesn't fix stats but at least
            # shows up in last activity column
            s = Submission(creation_time=datetime.datetime.utcnow(),
                           translation_project=translation_project,
                           submitter=get_profile(request.user))
            s.save()
            
            post_file_upload.send(sender=translation_project, user=request.user, oldstats=oldstats,
                                  newstats=newstats, archive=archive)
        return {'upload': self}