~launchpad-pqm/launchpad/devel

10295.1.2 by Jeroen Vermeulen
Review changes.
1
# Copyright 2009-2010 Canonical Ltd.  This software is licensed under the
8687.15.18 by Karl Fogel
Add the copyright header block to files under lib/canonical/.
2
# GNU Affero General Public License version 3 (see the file LICENSE).
2570.1.2 by Carlos Perello Marin
Implemented initial support for the translation import queue and removed the old attach script
3
10295.1.2 by Jeroen Vermeulen
Review changes.
4
"""Browser views for `ITranslationImportQueue`."""
2570.1.2 by Carlos Perello Marin
Implemented initial support for the translation import queue and removed the old attach script
5
6
__metaclass__ = type
7
8
__all__ = [
11091.7.6 by Jeroen Vermeulen
Forgotten change: escape strings.
9
    'escape_js_string',
2570.1.37 by Carlos Perelló Marín
More review comments applied
10
    'TranslationImportQueueEntryNavigation',
11
    'TranslationImportQueueEntryView',
2570.1.7 by Carlos Perello Marin
Lots of fixes + new code + tests updates
12
    'TranslationImportQueueNavigation',
2570.1.14 by Carlos Perelló Marín
Implemented the queue views and integragted it into the navigation.
13
    'TranslationImportQueueView',
2570.1.2 by Carlos Perello Marin
Implemented initial support for the translation import queue and removed the old attach script
14
    ]
15
3691.118.12 by Carlos Perello Marin
Added missing imports
16
import os
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
17
6520.2.1 by Jeroen Vermeulen
Handle bad values for two URL parameters better.
18
from zope.app.form.interfaces import ConversionError
2570.1.13 by Carlos Perelló Marín
Implemented the translation import queue web
19
from zope.component import getUtility
20
from zope.interface import implements
4268.3.23 by Carlos Perello Marin
Ported old import queue page to use the new infrastructure
21
from zope.schema.interfaces import IContextSourceBinder
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
22
from zope.schema.vocabulary import (
23
    SimpleTerm,
24
    SimpleVocabulary,
25
    )
2570.1.13 by Carlos Perelló Marín
Implemented the translation import queue web
26
11929.9.1 by Tim Penhey
Move launchpadform into lp.app.browser.
27
from lp.app.browser.launchpadform import (
28
    action,
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
29
    LaunchpadFormView,
30
    )
11626.3.2 by Curtis Hovey
Move tales gto lp.app.
31
from lp.app.browser.tales import DateTimeFormatterAPI
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
32
from lp.app.errors import (
33
    NotFoundError,
34
    UnexpectedFormData,
35
    )
12442.2.9 by j.c.sackett
Ran import reformatter per review.
36
from lp.app.validators.name import valid_name
7675.110.3 by Curtis Hovey
Ran the migration script to move registry code to lp.registry.
37
from lp.registry.interfaces.distroseries import IDistroSeries
10295.1.2 by Jeroen Vermeulen
Review changes.
38
from lp.registry.interfaces.sourcepackage import ISourcePackageFactory
14606.3.1 by William Grant
Merge canonical.database into lp.services.database.
39
from lp.services.database.constants import UTC_NOW
40
from lp.services.webapp import (
41
    canonical_url,
42
    GetitemNavigation,
43
    )
10737.1.2 by Henning Eggers
Fixed the bug.
44
from lp.services.worlddata.interfaces.language import ILanguageSet
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
45
from lp.translations.browser.hastranslationimports import (
46
    HasTranslationImportsView,
47
    )
11818.3.1 by Jeroen Vermeulen
Split out lp.translations.enums and lp.translations.interfaces.hastranslationimports.
48
from lp.translations.enums import RosettaImportStatus
8751.1.1 by Danilo Šegan
Store migration changes so far.
49
from lp.translations.interfaces.pofile import IPOFileSet
50
from lp.translations.interfaces.potemplate import IPOTemplateSet
12177.10.1 by Jeroen Vermeulen
Cleaned up import-queue browser code.
51
from lp.translations.interfaces.translationimporter import (
52
    ITranslationImporter,
53
    )
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
54
from lp.translations.interfaces.translationimportqueue import (
55
    IEditTranslationImportQueueEntry,
56
    ITranslationImportQueue,
57
    ITranslationImportQueueEntry,
58
    SpecialTranslationImportTargetFilter,
59
    TranslationFileType,
60
    )
61
from lp.translations.utilities.template import (
62
    make_domain,
63
    make_name,
64
    )
2570.1.7 by Carlos Perello Marin
Lots of fixes + new code + tests updates
65
11091.7.6 by Jeroen Vermeulen
Forgotten change: escape strings.
66
11091.7.7 by Jeroen Vermeulen
Test string escaping.
67
def replace(string, replacement):
68
    """In `string,` replace `replacement`[0] with `replacement`[1]."""
69
    return string.replace(*replacement)
70
71
11091.7.6 by Jeroen Vermeulen
Forgotten change: escape strings.
72
def escape_js_string(string):
73
    """Escape `string` for use as a string in a JS <script> tag."""
11091.7.7 by Jeroen Vermeulen
Test string escaping.
74
    replacements = [
75
        ('\\', '\\\\'),
76
        ('"', '\\"'),
77
        ("'", "\\'"),
78
        ('\n', '\\n'),
79
        ]
80
    return reduce(replace, replacements, string)
11091.7.6 by Jeroen Vermeulen
Forgotten change: escape strings.
81
82
2570.1.37 by Carlos Perelló Marín
More review comments applied
83
class TranslationImportQueueEntryNavigation(GetitemNavigation):
84
85
    usedfor = ITranslationImportQueueEntry
86
87
3691.289.2 by Carlos Perello Marin
Fixed code to prevent IPOFile.path duplicates inside the same sourcepackagename/distrorelease or productseries. Migrated TranslationImportQueueEntryView from GeneralForm to LaunchpadFormView and improved its portlet to have links instead of just text
88
class TranslationImportQueueEntryView(LaunchpadFormView):
89
    """The view part of admin interface for the translation import queue."""
7272.4.2 by Henning Eggers
Improved validation for import form.
90
    label = "Review import queue entry"
3691.289.2 by Carlos Perello Marin
Fixed code to prevent IPOFile.path duplicates inside the same sourcepackagename/distrorelease or productseries. Migrated TranslationImportQueueEntryView from GeneralForm to LaunchpadFormView and improved its portlet to have links instead of just text
91
    schema = IEditTranslationImportQueueEntry
2570.1.17 by Carlos Perelló Marín
Admin interface nearly done
92
10295.1.2 by Jeroen Vermeulen
Review changes.
93
    max_series_to_display = 3
94
13980.3.1 by Curtis Hovey
Moved translationimportqueueentry_index page_title into view.
95
    page_title = 'Translation import queue entry'
96
3200.1.30 by Carlos Perello Marin
Added autofill forms for translationimportqueue
97
    @property
98
    def initial_values(self):
99
        """Initialize some values on the form, when it's possible."""
100
        field_values = {}
3691.289.2 by Carlos Perello Marin
Fixed code to prevent IPOFile.path duplicates inside the same sourcepackagename/distrorelease or productseries. Migrated TranslationImportQueueEntryView from GeneralForm to LaunchpadFormView and improved its portlet to have links instead of just text
101
        if self.request.method == 'POST':
13194.2.1 by Gavin Panella
Change all uses of 'initialise' to 'initialize'.
102
            # We got a form post, we don't need to do any initialization.
3691.289.2 by Carlos Perello Marin
Fixed code to prevent IPOFile.path duplicates inside the same sourcepackagename/distrorelease or productseries. Migrated TranslationImportQueueEntryView from GeneralForm to LaunchpadFormView and improved its portlet to have links instead of just text
103
            return field_values
3200.1.33 by Carlos Perello Marin
Added some other autofill options and pagetests
104
        # Fill the know values.
7272.4.1 by Henning Eggers
Added more validation and explicitness to TranslationnImportEntryView.
105
        field_values['path'] = self.context.path
12177.10.1 by Jeroen Vermeulen
Cleaned up import-queue browser code.
106
107
        importer = getUtility(ITranslationImporter)
108
        if importer.isTemplateName(self.context.path):
109
            file_type = TranslationFileType.POT
110
        elif importer.isTranslationName(self.context.path):
7272.4.1 by Henning Eggers
Added more validation and explicitness to TranslationnImportEntryView.
111
            file_type = TranslationFileType.PO
112
        else:
113
            file_type = TranslationFileType.UNSPEC
114
        field_values['file_type'] = file_type
115
3200.1.30 by Carlos Perello Marin
Added autofill forms for translationimportqueue
116
        if self.context.sourcepackagename is not None:
117
            field_values['sourcepackagename'] = self.context.sourcepackagename
7849.12.2 by Henning Eggers
Factored a check out into aproperty is_targeted_to_ubuntu.
118
        if self.context.is_targeted_to_ubuntu:
7849.12.1 by Henning Eggers
Added launguagepack checkbox to TranslationImportEntryView.
119
            if self.context.potemplate is None:
120
                # Default for Ubuntu templates is to
121
                # include them in languagepacks.
122
                field_values['languagepack'] = True
123
            else:
124
                field_values['languagepack'] = (
125
                    self.context.potemplate.languagepack)
7849.12.2 by Henning Eggers
Factored a check out into aproperty is_targeted_to_ubuntu.
126
        if (file_type in (TranslationFileType.POT,
10737.1.2 by Henning Eggers
Fixed the bug.
127
                          TranslationFileType.UNSPEC)):
10737.1.4 by Henning Eggers
Fixed test.
128
            potemplate = self.context.potemplate
129
            if potemplate is None:
10737.1.2 by Henning Eggers
Fixed the bug.
130
                domain = make_domain(self.context.path)
131
                field_values['name'] = make_name(domain)
132
                field_values['translation_domain'] = domain
133
            else:
10737.1.4 by Henning Eggers
Fixed test.
134
                field_values['name'] = potemplate.name
10737.1.2 by Henning Eggers
Fixed the bug.
135
                field_values['translation_domain'] = (
10737.1.4 by Henning Eggers
Fixed test.
136
                    potemplate.translation_domain)
7272.4.1 by Henning Eggers
Added more validation and explicitness to TranslationnImportEntryView.
137
        if file_type in (TranslationFileType.PO, TranslationFileType.UNSPEC):
7272.4.2 by Henning Eggers
Improved validation for import form.
138
            field_values['potemplate'] = self.context.potemplate
7272.4.1 by Henning Eggers
Added more validation and explicitness to TranslationnImportEntryView.
139
            if self.context.pofile is not None:
140
                field_values['language'] = self.context.pofile.language
141
            else:
142
                # The entries that are translations usually have the language
143
                # code
144
                # as its filename. We try to get it to use as a suggestion.
145
                language_set = getUtility(ILanguageSet)
146
                filename = os.path.basename(self.context.path)
147
                guessed_language, file_ext = filename.split(u'.', 1)
11122.3.2 by Danilo Šegan
Remove variant from ILanguageSet and callsites.
148
                language = language_set.getLanguageByCode(guessed_language)
7272.4.1 by Henning Eggers
Added more validation and explicitness to TranslationnImportEntryView.
149
                if language is not None:
150
                    field_values['language'] = language
151
                    # Need to warn the user that we guessed the language
152
                    # information.
153
                    self.request.response.addWarningNotification(
154
                        "Review the language selection as we guessed it and"
155
                        " could not be accurate.")
3200.1.33 by Carlos Perello Marin
Added some other autofill options and pagetests
156
3200.1.30 by Carlos Perello Marin
Added autofill forms for translationimportqueue
157
        return field_values
158
3691.289.2 by Carlos Perello Marin
Fixed code to prevent IPOFile.path duplicates inside the same sourcepackagename/distrorelease or productseries. Migrated TranslationImportQueueEntryView from GeneralForm to LaunchpadFormView and improved its portlet to have links instead of just text
159
    @property
9295.1.1 by Danilo Šegan
Migrate translationimportqueue-entry.pt to 3.0 and fix up next_url handling.
160
    def cancel_url(self):
161
        """See `LaunchpadFormView`."""
9295.1.6 by Danilo Šegan
Test fixes.
162
        referrer = self.referrer_url
163
        if referrer is None:
164
            translationimportqueue_set = getUtility(ITranslationImportQueue)
165
            return canonical_url(translationimportqueue_set)
166
        else:
167
            return referrer
9295.1.1 by Danilo Šegan
Migrate translationimportqueue-entry.pt to 3.0 and fix up next_url handling.
168
169
    @property
170
    def referrer_url(self):
9295.1.6 by Danilo Šegan
Test fixes.
171
        referrer = self.request.getHeader('referer')
172
        if referrer != canonical_url(self.context):
173
            return referrer
174
        else:
175
            return None
9295.1.1 by Danilo Šegan
Migrate translationimportqueue-entry.pt to 3.0 and fix up next_url handling.
176
177
    @property
10295.1.2 by Jeroen Vermeulen
Review changes.
178
    def import_target(self):
179
        """The entry's `ProductSeries` or `SourcePackage`."""
180
        productseries = self.context.productseries
181
        distroseries = self.context.distroseries
182
        sourcepackagename = self.context.sourcepackagename
183
        if distroseries is None:
184
            return productseries
185
        else:
186
            factory = getUtility(ISourcePackageFactory)
187
            return factory.new(sourcepackagename, distroseries)
188
189
    @property
190
    def productseries_templates_link(self):
191
        """Return link to `ProductSeries`' templates.
192
193
        Use this only if the entry is attached to a `ProductSeries`.
194
        """
195
        assert self.context.productseries is not None, (
196
            "Entry is not attached to a ProductSeries.")
197
198
        template_count = self.context.productseries.potemplate_count
199
        if template_count == 0:
200
            return "no templates"
201
        else:
202
            link = "%s/+templates" % canonical_url(
203
                self.context.productseries, rootsite='translations')
204
            if template_count == 1:
205
                word = "template"
206
            else:
207
                word = "templates"
208
            return '<a href="%s">%d %s</a>' % (link, template_count, word)
209
210
    def _composeProductSeriesLink(self, productseries):
211
        """Produce HTML to link to `productseries`."""
212
        return '<a href="%s">%s</a>' % (
213
            canonical_url(productseries, rootsite='translations'),
214
            productseries.name)
215
216
    @property
217
    def product_translatable_series(self):
218
        """Summarize whether `Product` has translatable series.
219
220
        Use this only if the entry is attached to a `ProductSeries`.
221
        """
222
        assert self.context.productseries is not None, (
223
            "Entry is not attached to a ProductSeries.")
224
225
        product = self.context.productseries.product
226
        translatable_series = list(product.translatable_series)
227
        if len(translatable_series) == 0:
228
            return "Project has no translatable series."
229
        else:
11626.3.10 by Curtis Hovey
Hush lints epic complaints about the changes files.
230
            max_series_to_display = self.max_series_to_display
10295.1.2 by Jeroen Vermeulen
Review changes.
231
            links = [
232
                self._composeProductSeriesLink(series)
11626.3.10 by Curtis Hovey
Hush lints epic complaints about the changes files.
233
                for series in translatable_series[:max_series_to_display]]
10295.1.2 by Jeroen Vermeulen
Review changes.
234
            links_text = ', '.join(links)
11626.3.10 by Curtis Hovey
Hush lints epic complaints about the changes files.
235
            if len(translatable_series) > max_series_to_display:
10295.1.2 by Jeroen Vermeulen
Review changes.
236
                tail = ", ..."
237
            else:
238
                tail = "."
239
            return "Project has translatable series: " + links_text + tail
240
241
    @property
242
    def status_change_date(self):
243
        """Show date of last status change.
244
245
        Says nothing at all if the entry's status has not changed since
246
        upload.
247
        """
248
        change_date = self.context.date_status_changed
249
        if change_date == self.context.dateimported:
250
            return ""
251
        else:
252
            formatter = DateTimeFormatterAPI(change_date)
253
            return "Last changed %s." % formatter.displaydate()
254
255
    @property
3691.289.2 by Carlos Perello Marin
Fixed code to prevent IPOFile.path duplicates inside the same sourcepackagename/distrorelease or productseries. Migrated TranslationImportQueueEntryView from GeneralForm to LaunchpadFormView and improved its portlet to have links instead of just text
256
    def next_url(self):
9295.1.1 by Danilo Šegan
Migrate translationimportqueue-entry.pt to 3.0 and fix up next_url handling.
257
        """See `LaunchpadFormView`."""
258
        # The referer header we want is only available before the view's
259
        # form submits to itself. This field is a hidden input in the form.
260
        referrer = self.request.form.get('next_url')
261
262
        if (referrer is not None
263
            and referrer.startswith(self.request.getApplicationURL())):
264
            return referrer
265
        else:
266
            translationimportqueue_set = getUtility(ITranslationImportQueue)
267
            return canonical_url(translationimportqueue_set)
3691.289.2 by Carlos Perello Marin
Fixed code to prevent IPOFile.path duplicates inside the same sourcepackagename/distrorelease or productseries. Migrated TranslationImportQueueEntryView from GeneralForm to LaunchpadFormView and improved its portlet to have links instead of just text
268
2570.1.17 by Carlos Perelló Marín
Admin interface nearly done
269
    def initialize(self):
3691.289.2 by Carlos Perello Marin
Fixed code to prevent IPOFile.path duplicates inside the same sourcepackagename/distrorelease or productseries. Migrated TranslationImportQueueEntryView from GeneralForm to LaunchpadFormView and improved its portlet to have links instead of just text
270
        """Remove some fields based on the entry handled."""
7272.4.1 by Henning Eggers
Added more validation and explicitness to TranslationnImportEntryView.
271
        self.field_names = ['file_type', 'path', 'sourcepackagename',
11091.7.2 by Jeroen Vermeulen
Make template name and domain settable through the templates dropdown.
272
                            'potemplate', 'potemplate_name',
7849.12.1 by Henning Eggers
Added launguagepack checkbox to TranslationImportEntryView.
273
                            'name', 'translation_domain', 'languagepack',
11122.3.4 by Danilo Šegan
Get rid of the remaining variant usage.
274
                            'language']
2570.1.19 by Carlos Perelló Marín
Added a page to request new potemplate additions for a product series so we have that integrated into launchpad
275
3691.289.9 by Carlos Perello Marin
Applied review comments + test fixes
276
        if self.context.productseries is not None:
3691.289.2 by Carlos Perello Marin
Fixed code to prevent IPOFile.path duplicates inside the same sourcepackagename/distrorelease or productseries. Migrated TranslationImportQueueEntryView from GeneralForm to LaunchpadFormView and improved its portlet to have links instead of just text
277
            # We are handling an entry for a productseries, this field is not
278
            # useful here.
279
            self.field_names.remove('sourcepackagename')
2570.1.17 by Carlos Perelló Marín
Admin interface nearly done
280
7849.12.2 by Henning Eggers
Factored a check out into aproperty is_targeted_to_ubuntu.
281
        if not self.context.is_targeted_to_ubuntu:
7849.12.1 by Henning Eggers
Added launguagepack checkbox to TranslationImportEntryView.
282
            # Only show languagepack for Ubuntu packages.
283
            self.field_names.remove('languagepack')
284
13194.2.1 by Gavin Panella
Change all uses of 'initialise' to 'initialize'.
285
        # Execute default initialization.
3691.289.2 by Carlos Perello Marin
Fixed code to prevent IPOFile.path duplicates inside the same sourcepackagename/distrorelease or productseries. Migrated TranslationImportQueueEntryView from GeneralForm to LaunchpadFormView and improved its portlet to have links instead of just text
286
        LaunchpadFormView.initialize(self)
287
7272.4.6 by Henning Eggers
Fixed template name validation to work with None. Improved page test readability. Startet doctest.
288
# XXX: HenningEggers 2008-11-21 bug=300608: This code is too generic to be in
7272.4.7 by Henning Eggers
Completed doctest.
289
#      the view and should be factored out.
7272.4.1 by Henning Eggers
Added more validation and explicitness to TranslationnImportEntryView.
290
    def _checkProductOrPackage(self, obj):
291
        """Check if the given object is linked to the same productseries
292
        or sourcepackage as the context.
293
294
        :param obj: The object to check, must have productseries,
295
            distroseries and sourcepackagename attributes.
296
        :return: true if object and context match.
297
        """
7272.4.2 by Henning Eggers
Improved validation for import form.
298
        try:
299
            if self.context.productseries != None:
300
                return obj.productseries == self.context.productseries
301
            if self.context.distroseries != None:
302
                return (
303
                    obj.distroseries == self.context.distroseries and
304
                    obj.sourcepackagename == self.context.sourcepackagename)
305
        except AttributeError:
306
            pass  # return False
7272.4.1 by Henning Eggers
Added more validation and explicitness to TranslationnImportEntryView.
307
        return False
308
309
    def _getPOTemplateSubset(self, sourcepackagename):
310
        potemplate_set = getUtility(IPOTemplateSet)
311
        if self.context.productseries is None:
312
            if (sourcepackagename is not None and
313
                self.context.sourcepackagename is not None and
314
                sourcepackagename.id != self.context.sourcepackagename.id):
315
                # Got another sourcepackagename from the form, we will use it.
316
                potemplate_subset = potemplate_set.getSubset(
317
                    distroseries=self.context.distroseries,
318
                    sourcepackagename=sourcepackagename)
319
            else:
320
                potemplate_subset = potemplate_set.getSubset(
321
                    distroseries=self.context.distroseries,
322
                    sourcepackagename=self.context.sourcepackagename)
323
        else:
324
            potemplate_subset = potemplate_set.getSubset(
325
                productseries=self.context.productseries)
326
        return potemplate_subset
327
12177.10.1 by Jeroen Vermeulen
Cleaned up import-queue browser code.
328
    def _findObjectionToFilePath(self, file_type, path):
329
        """Return textual objection, if any, to setting this file path."""
330
        importer = getUtility(ITranslationImporter)
331
        if file_type == TranslationFileType.POT:
332
            if not importer.isTemplateName(path):
333
                return "This filename is not appropriate for a template."
334
        else:
335
            if not importer.isTranslationName(path):
12177.10.3 by Jeroen Vermeulen
Indentation.
336
                return "This filename is not appropriate for a translation."
12177.10.1 by Jeroen Vermeulen
Cleaned up import-queue browser code.
337
338
        if path == self.context.path:
339
            # No change, so no objections.
340
            return None
341
342
        # The Rosetta Expert decided to change the path of the file.
343
        # Before accepting such change, we should check first whether
344
        # there is already another entry with that path in the same
345
        # context (sourcepackagename/distroseries or productseries).
346
        # A duplicate name will confuse the auto-approval
347
        # process.
348
        if file_type == TranslationFileType.POT:
349
            potemplate_set = getUtility(IPOTemplateSet)
350
            existing_file = potemplate_set.getPOTemplateByPathAndOrigin(
351
                path, self.context.productseries, self.context.distroseries,
352
                self.context.sourcepackagename)
353
            already_exists = existing_file is not None
354
        else:
355
            pofile_set = getUtility(IPOFileSet)
356
            existing_files = pofile_set.getPOFilesByPathAndOrigin(
357
                path, self.context.productseries,
358
                self.context.distroseries,
359
                self.context.sourcepackagename)
360
            already_exists = not existing_files.is_empty()
361
362
        if already_exists:
363
            # We already have an IPOFile in this path, let's notify
364
            # the user about that so they choose another path.
365
            return "There is already a file in the given path."
366
367
        return None
368
7272.4.5 by Henning Eggers
Refactoring.
369
    def _validatePath(self, file_type, path):
12177.10.1 by Jeroen Vermeulen
Cleaned up import-queue browser code.
370
        """Should the entry's path be updated?"""
371
        if path is None or path.strip() == "":
372
            self.setFieldError('path', "The file name is missing.")
373
            return False
374
375
        objection = self._findObjectionToFilePath(file_type, path)
376
        if objection is None:
377
            return True
7272.4.2 by Henning Eggers
Improved validation for import form.
378
        else:
12177.10.1 by Jeroen Vermeulen
Cleaned up import-queue browser code.
379
            self.setFieldError('path', objection)
380
            return False
7272.4.5 by Henning Eggers
Refactoring.
381
382
    def _validatePOT(self, data):
383
        name = data.get('name')
384
        translation_domain = data.get('translation_domain')
385
        if name is None:
386
            self.setFieldError('name', 'Please specify a name for '
387
                               'the template.')
7272.4.6 by Henning Eggers
Fixed template name validation to work with None. Improved page test readability. Startet doctest.
388
        elif not valid_name(name):
7272.4.5 by Henning Eggers
Refactoring.
389
            self.setFieldError('name', 'Please specify a valid name for '
390
                               'the template. Names must be all lower '
391
                               'case and start with a letter or number.')
392
        if translation_domain is None:
393
            self.setFieldError('translation_domain', 'Please specify a '
394
                               'translation domain for the template.')
395
396
    def _validatePO(self, data):
397
        potemplate_name = data.get('potemplate_name')
398
        man_potemplate = None
399
        if potemplate_name == None:
400
            potemplate = data.get('potemplate')
401
            if not self._checkProductOrPackage(potemplate):
402
                self.setFieldError(
403
                    'potemplate', 'Please choose a template.')
404
        else:
405
            sourcepackagename = data.get('sourcepackagename')
406
            potemplate_subset = (
407
                self._getPOTemplateSubset(sourcepackagename))
408
            try:
409
                man_potemplate = potemplate_subset[potemplate_name]
410
            except NotFoundError:
411
                self.setFieldError('potemplate_name',
412
                    'Please enter a valid template name '
413
                    'or choose from the list above.')
414
        return man_potemplate
415
416
    def validate(self, data):
417
        """Extra validations for the given fields."""
418
        # Without a file type we cannot do anything
419
        file_type = data.get('file_type')
420
        if file_type not in (TranslationFileType.PO,
421
                             TranslationFileType.POT):
422
            self.setFieldError('file_type', 'Please specify the file type')
423
            return
424
425
        self.path_changed = self._validatePath(file_type, data.get('path'))
426
427
        self.man_potemplate = None
7272.4.2 by Henning Eggers
Improved validation for import form.
428
        if file_type == TranslationFileType.POT:
7272.4.5 by Henning Eggers
Refactoring.
429
            self._validatePOT(data)
7272.4.1 by Henning Eggers
Added more validation and explicitness to TranslationnImportEntryView.
430
        if file_type == TranslationFileType.PO:
7272.4.5 by Henning Eggers
Refactoring.
431
            self.man_potemplate = self._validatePO(data)
432
433
    def _changeActionPOT(self, data):
434
        """Process form for PO template files.
435
436
        PO template specific processing. Creates a new potemplate entry
437
        in the db if none exists with the given name. Updates the queue
438
        entry's path if it was changed.
439
440
        :param data: The form data.
441
        :returns: The potemplate instance."""
442
        path = data.get('path')
443
        name = data.get('name')
444
        sourcepackagename = data.get('sourcepackagename')
445
        translation_domain = data.get('translation_domain')
7849.12.1 by Henning Eggers
Added launguagepack checkbox to TranslationImportEntryView.
446
        languagepack = data.get('languagepack')
7272.4.5 by Henning Eggers
Refactoring.
447
448
        if self.path_changed:
449
            self.context.path = path
450
        # We are importing an IPOTemplate file.
451
452
        # Create a new potemplate if this template name
453
        # does not yet appear in this subset.
454
        potemplate_subset = self._getPOTemplateSubset(sourcepackagename)
455
        try:
456
            potemplate = potemplate_subset[name]
457
        except NotFoundError:
458
            potemplate = potemplate_subset.new(
459
                name,
460
                translation_domain,
461
                self.context.path,
462
                self.context.importer)
463
464
        if (self.context.sourcepackagename is not None and
465
            potemplate.sourcepackagename is not None and
11626.3.10 by Curtis Hovey
Hush lints epic complaints about the changes files.
466
            self.context.sourcepackagename != potemplate.sourcepackagename):
7272.4.5 by Henning Eggers
Refactoring.
467
            # We got the template from a different package than the one
468
            # selected by the user where the import should done, so we
469
            # note it here.
470
            potemplate.from_sourcepackagename = (
471
                self.context.sourcepackagename)
7849.12.1 by Henning Eggers
Added launguagepack checkbox to TranslationImportEntryView.
472
7849.12.2 by Henning Eggers
Factored a check out into aproperty is_targeted_to_ubuntu.
473
        if self.context.is_targeted_to_ubuntu:
7849.12.1 by Henning Eggers
Added launguagepack checkbox to TranslationImportEntryView.
474
            potemplate.languagepack = languagepack
475
7272.4.5 by Henning Eggers
Refactoring.
476
        return potemplate
477
478
    def _changeActionPO(self, data):
479
        """Process form for PO data files.
480
481
        PO file specific processing. Creates a new pofile entry in the db
482
        if no matching one exists. Updates the queue entry's path if it was
483
        changed.
484
485
        :param data: The form data.
486
        :returns: The potemplate instance."""
487
488
        path = data.get('path')
489
        language = data.get('language')
490
491
        # Use manual potemplate, if given.
492
        # man_potemplate is set in validate().
493
        if self.man_potemplate != None:
494
            potemplate = self.man_potemplate
495
        else:
496
            potemplate = data.get('potemplate')
497
11122.3.4 by Danilo Šegan
Get rid of the remaining variant usage.
498
        pofile = potemplate.getPOFileByLang(language.code)
7272.4.5 by Henning Eggers
Refactoring.
499
        if pofile is None:
500
            # We don't have such IPOFile, we need to create it.
501
            pofile = potemplate.newPOFile(
11122.3.4 by Danilo Šegan
Get rid of the remaining variant usage.
502
                language.code, self.context.importer)
7272.4.5 by Henning Eggers
Refactoring.
503
        self.context.pofile = pofile
504
        if (self.context.sourcepackagename is not None and
505
            potemplate.sourcepackagename is not None and
506
            self.context.sourcepackagename.id !=
507
            pofile.potemplate.sourcepackagename.id):
508
            # We got the template from a different package than the one
509
            # selected by the user where the import should done, so we
510
            # note it here.
511
            pofile.from_sourcepackagename = self.context.sourcepackagename
512
513
        if self.path_changed:
514
            self.context.path = path
515
            # We got a path to store as the new one for the POFile.
516
            pofile.setPathIfUnique(path)
7675.916.98 by Henning Eggers
Merged db-stable at r10026 (recife roll-back) but without accepting the changes.
517
        elif self.context.by_maintainer:
518
            # This entry was uploaded by the maintainer, which means that the
519
            # path we got is exactly the right one. If it's different from
520
            # what pofile has, that would mean that either the entry changed
7272.4.5 by Henning Eggers
Refactoring.
521
            # its path since previous upload or that we had to guess it
522
            # and now that we got the right path, we should fix it.
523
            pofile.setPathIfUnique(self.context.path)
524
        else:
525
            # Leave path unchanged.
526
            pass
527
        return potemplate
7272.4.1 by Henning Eggers
Added more validation and explicitness to TranslationnImportEntryView.
528
529
    @action("Approve")
3691.289.2 by Carlos Perello Marin
Fixed code to prevent IPOFile.path duplicates inside the same sourcepackagename/distrorelease or productseries. Migrated TranslationImportQueueEntryView from GeneralForm to LaunchpadFormView and improved its portlet to have links instead of just text
530
    def change_action(self, action, data):
2570.1.16 by Carlos Perelló Marín
First steps to implemente the admin UI to associate the entries on the queue with final IPOTemplate/IPOFile objects
531
        """Process the form we got from the submission."""
7272.4.7 by Henning Eggers
Completed doctest.
532
        self._change_action(data)
533
534
    def _change_action(self, data):
535
        """Private function to be called by the doctest."""
7272.4.2 by Henning Eggers
Improved validation for import form.
536
        file_type = data.get('file_type')
2570.1.17 by Carlos Perelló Marín
Admin interface nearly done
537
7272.4.2 by Henning Eggers
Improved validation for import form.
538
        if file_type == TranslationFileType.PO:
7272.4.5 by Henning Eggers
Refactoring.
539
            potemplate = self._changeActionPO(data)
7272.4.2 by Henning Eggers
Improved validation for import form.
540
        if file_type == TranslationFileType.POT:
7272.4.5 by Henning Eggers
Refactoring.
541
            potemplate = self._changeActionPOT(data)
7272.4.2 by Henning Eggers
Improved validation for import form.
542
543
        # Store the associated IPOTemplate.
544
        self.context.potemplate = potemplate
545
9893.3.1 by Henning Eggers
Merged API work.
546
        self.context.setStatus(RosettaImportStatus.APPROVED, self.user)
3691.289.2 by Carlos Perello Marin
Fixed code to prevent IPOFile.path duplicates inside the same sourcepackagename/distrorelease or productseries. Migrated TranslationImportQueueEntryView from GeneralForm to LaunchpadFormView and improved its portlet to have links instead of just text
547
        self.context.date_status_changed = UTC_NOW
2570.1.13 by Carlos Perelló Marín
Implemented the translation import queue web
548
11091.7.2 by Jeroen Vermeulen
Make template name and domain settable through the templates dropdown.
549
    @property
550
    def js_domain_mapping(self):
551
        """Return JS code mapping templates' names to translation domains."""
552
        target = self.import_target
553
        if target is None:
554
            contents = ""
555
        else:
556
            contents = ", \n".join([
11091.7.6 by Jeroen Vermeulen
Forgotten change: escape strings.
557
                "'%s': '%s'" % (
558
                    escape_js_string(template.name),
559
                    escape_js_string(template.translation_domain))
11626.3.10 by Curtis Hovey
Hush lints epic complaints about the changes files.
560
                for template in target.getCurrentTranslationTemplates()])
11091.7.2 by Jeroen Vermeulen
Make template name and domain settable through the templates dropdown.
561
        return "var template_domains = {%s};" % contents
562
2570.1.14 by Carlos Perelló Marín
Implemented the queue views and integragted it into the navigation.
563
2570.1.37 by Carlos Perelló Marín
More review comments applied
564
class TranslationImportQueueNavigation(GetitemNavigation):
3691.8.25 by Carlos Perello Marin
Reused the context menu for Rosetta across all its suburls
565
    usedfor = ITranslationImportQueue
566
567
4268.3.23 by Carlos Perello Marin
Ported old import queue page to use the new infrastructure
568
class TranslationImportQueueView(HasTranslationImportsView):
9287.5.2 by Jeroen Vermeulen
Accepted that labels don't wrap for now on licensing page, fixed up some pagetests, cleaned up import queue view hierarchy.
569
    """The global Translation Import Queue."""
9493.1.1 by Henning Eggers
Fix labels and page titles on shared translation pages.
570
571
    label = "Translation import queue"
3200.1.25 by Carlos Perello Marin
Implemented filtering options on the import queue form and improved our guessing algorithm
572
2570.1.14 by Carlos Perelló Marín
Implemented the queue views and integragted it into the navigation.
573
    def initialize(self):
574
        """Useful initialization for this view class."""
9287.5.2 by Jeroen Vermeulen
Accepted that labels don't wrap for now on licensing page, fixed up some pagetests, cleaned up import queue view hierarchy.
575
        super(TranslationImportQueueView, self).initialize()
6520.2.4 by Jeroen Vermeulen
Don't bother trying to tolerate bad values, just raise UFD.
576
        target_filter = self.widgets['filter_target']
577
        if target_filter.hasInput() and not target_filter.hasValidInput():
578
            raise UnexpectedFormData("Unknown target.")
4268.3.23 by Carlos Perello Marin
Ported old import queue page to use the new infrastructure
579
580
    @property
581
    def entries(self):
582
        """Return the entries in the queue for this context."""
4268.3.27 by Carlos Perello Marin
Applied review comments
583
        target, file_extension, import_status = (
584
            self.getEntriesFilteringOptions())
5682.2.6 by Jeroen Vermeulen
Review changes.
585
        if file_extension is None:
586
            extensions = None
587
        else:
5682.2.1 by Jeroen Vermeulen
Moved recognition of file suffixes into translationformat module.
588
            extensions = [file_extension]
4268.3.23 by Carlos Perello Marin
Ported old import queue page to use the new infrastructure
589
590
        return self.context.getAllEntries(
4268.3.27 by Carlos Perello Marin
Applied review comments
591
                target=target, import_status=import_status,
5682.2.1 by Jeroen Vermeulen
Moved recognition of file suffixes into translationformat module.
592
                file_extensions=extensions)
4268.3.23 by Carlos Perello Marin
Ported old import queue page to use the new infrastructure
593
594
    def createFilterTargetField(self):
595
        """Create a field with a vocabulary to filter by target.
596
597
        :return: A form.Fields instance containing the target field.
598
        """
4268.3.27 by Carlos Perello Marin
Applied review comments
599
        return self.createFilterFieldHelper(
6520.2.2 by Jeroen Vermeulen
Removed some of the complexity I introduced earlier that turns out not to help.
600
            name='filter_target',
601
            source=TranslationImportTargetVocabularyFactory(self),
4268.3.27 by Carlos Perello Marin
Applied review comments
602
            title='Choose which target to show')
4268.3.23 by Carlos Perello Marin
Ported old import queue page to use the new infrastructure
603
6520.2.1 by Jeroen Vermeulen
Handle bad values for two URL parameters better.
604
4268.3.23 by Carlos Perello Marin
Ported old import queue page to use the new infrastructure
605
class TranslationImportTargetVocabularyFactory:
606
    """Factory for a vocabulary containing a list of targets."""
607
608
    implements(IContextSourceBinder)
609
5967.6.9 by Jeroen Vermeulen
Review changes.
610
    def __init__(self, view):
5967.6.11 by Jeroen Vermeulen
Grammar fix.
611
        """Create a `TranslationImportTargetVocabularyFactory`.
5967.6.1 by Jeroen Vermeulen
Add asterisks to targets dropdown in import queue UI for targets that have entries with matching state.
612
5967.6.9 by Jeroen Vermeulen
Review changes.
613
        :param view: The view that called this factory.  We access its
614
            filter_status widget later to see which status it filters for.
615
        """
616
        self.view = view
5967.6.1 by Jeroen Vermeulen
Add asterisks to targets dropdown in import queue UI for targets that have entries with matching state.
617
6520.2.2 by Jeroen Vermeulen
Removed some of the complexity I introduced earlier that turns out not to help.
618
    def __call__(self, context):
5967.6.1 by Jeroen Vermeulen
Add asterisks to targets dropdown in import queue UI for targets that have entries with matching state.
619
        import_queue = getUtility(ITranslationImportQueue)
5967.6.3 by Jeroen Vermeulen
Renamed getPillarObjectsWithImports to getRequestTargets.
620
        targets = import_queue.getRequestTargets()
5967.6.9 by Jeroen Vermeulen
Review changes.
621
        filtered_targets = set()
5967.6.1 by Jeroen Vermeulen
Add asterisks to targets dropdown in import queue UI for targets that have entries with matching state.
622
5967.6.9 by Jeroen Vermeulen
Review changes.
623
        # Read filter_status, in order to mark targets that have requests with
624
        # that status pending.  This works because we set up the filter_status
625
        # widget before the filter_target one, which uses this vocabulary
626
        # factory.
627
        status_widget = self.view.widgets['filter_status']
628
        if status_widget.hasInput():
6520.2.1 by Jeroen Vermeulen
Handle bad values for two URL parameters better.
629
            try:
630
                status_filter = status_widget.getInputValue()
631
            except ConversionError:
632
                raise UnexpectedFormData("Invalid status parameter.")
5967.6.9 by Jeroen Vermeulen
Review changes.
633
            if status_filter != 'all':
634
                try:
635
                    status = RosettaImportStatus.items[status_filter]
636
                    filtered_targets = set(
637
                        import_queue.getRequestTargets(status))
638
                except LookupError:
639
                    # Unknown status.  Ignore.
640
                    pass
4268.3.23 by Carlos Perello Marin
Ported old import queue page to use the new infrastructure
641
6520.2.4 by Jeroen Vermeulen
Don't bother trying to tolerate bad values, just raise UFD.
642
        terms = [SimpleTerm('all', 'all', 'All targets')]
7960.3.1 by Jeroen Vermeulen
Added translation import queue filters for "any project" and "any distribution."
643
644
        for item in SpecialTranslationImportTargetFilter.items:
645
            term_name = '[%s]' % item.name
646
            terms.append(SimpleTerm(term_name, term_name, item.title))
647
4268.3.23 by Carlos Perello Marin
Ported old import queue page to use the new infrastructure
648
        for target in targets:
4268.3.31 by Carlos Perello Marin
Fixed filtering of distroseries
649
            if IDistroSeries.providedBy(target):
650
                # Distroseries are not pillar names, we need to note
651
                # distribution.name/distroseries.name
652
                term_name = '%s/%s' % (target.distribution.name, target.name)
653
            else:
654
                term_name = target.name
5967.6.1 by Jeroen Vermeulen
Add asterisks to targets dropdown in import queue UI for targets that have entries with matching state.
655
656
            displayname = target.displayname
5967.6.9 by Jeroen Vermeulen
Review changes.
657
            if target in filtered_targets:
5967.6.1 by Jeroen Vermeulen
Add asterisks to targets dropdown in import queue UI for targets that have entries with matching state.
658
                displayname += '*'
659
660
            terms.append(SimpleTerm(term_name, term_name, displayname))
6520.2.4 by Jeroen Vermeulen
Don't bother trying to tolerate bad values, just raise UFD.
661
        return SimpleVocabulary(terms)