~launchpad-pqm/launchpad/devel

8687.15.18 by Karl Fogel
Add the copyright header block to files under lib/canonical/.
1
# Copyright 2009 Canonical Ltd.  This software is licensed under the
2
# GNU Affero General Public License version 3 (see the file LICENSE).
6934.4.1 by Jeroen Vermeulen
New function, and driver script, for admins to delete selected translation messages.
3
4
"""Remove specific translation messages from the database."""
5
6
__metaclass__ = type
7
__all__ = [
13636.1.19 by Henning Eggers
Expose and use process_options.
8
    'process_options',
6987.2.2 by Jeroen Vermeulen
Moved Launchpadscript into c.l.scripts, extended OptionParser to handle custom option types.
9
    'RemoveTranslations',
6934.4.1 by Jeroen Vermeulen
New function, and driver script, for admins to delete selected translation messages.
10
    'remove_translations',
11
    ]
12
6987.2.2 by Jeroen Vermeulen
Moved Launchpadscript into c.l.scripts, extended OptionParser to handle custom option types.
13
import logging
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
14
from optparse import (
15
    Option,
16
    OptionValueError,
17
    )
6987.2.2 by Jeroen Vermeulen
Moved Launchpadscript into c.l.scripts, extended OptionParser to handle custom option types.
18
6987.2.1 by Jeroen Vermeulen
Made options processing testable, and tested. Fixed bug with --id.
19
from zope.component import getUtility
20
14606.3.1 by William Grant
Merge canonical.database into lp.services.database.
21
from lp.services.database.postgresql import drop_tables
22
from lp.services.database.sqlbase import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
23
    cursor,
24
    sqlvalues,
25
    )
26
from lp.services.scripts.base import (
27
    LaunchpadScript,
28
    LaunchpadScriptFailure,
29
    )
8751.1.8 by Danilo Šegan
Fix most lint warnings.
30
from lp.translations.interfaces.translationmessage import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
31
    RosettaTranslationOrigin,
32
    )
6987.2.2 by Jeroen Vermeulen
Moved Launchpadscript into c.l.scripts, extended OptionParser to handle custom option types.
33
34
13636.1.14 by Henning Eggers
Rename to processing.
35
def process_bool_option(value):
36
    """Validation and conversion for Boolean argument."""
6987.2.2 by Jeroen Vermeulen
Moved Launchpadscript into c.l.scripts, extended OptionParser to handle custom option types.
37
    value = value.lower()
38
    bool_representations = {
39
        'true': True,
40
        '1': True,
41
        'false': False,
42
        '0': False,
43
        }
44
45
    if value not in bool_representations:
46
        raise OptionValueError("Invalid boolean value: %s" % value)
47
48
    return bool_representations[value]
6987.2.1 by Jeroen Vermeulen
Made options processing testable, and tested. Fixed bug with --id.
49
50
51
def get_id(identifier, lookup_function=None):
52
    """Look up id of object identified by a string.
53
6987.2.2 by Jeroen Vermeulen
Moved Launchpadscript into c.l.scripts, extended OptionParser to handle custom option types.
54
    Raises `OptionValueError` if the option's value appears invalid.
55
6987.2.1 by Jeroen Vermeulen
Made options processing testable, and tested. Fixed bug with --id.
56
    :param identifier: String identifying an object.  If entirely
57
        numeric, taken as id.  Otherwise, passed to lookup_function.
58
    :param lookup_function: Callback that will take `identifier` as
59
        its argument and return a numeric object id.  If no object
6987.2.2 by Jeroen Vermeulen
Moved Launchpadscript into c.l.scripts, extended OptionParser to handle custom option types.
60
        has the given identifier, may raise a `LookUpError` or return
6987.2.1 by Jeroen Vermeulen
Made options processing testable, and tested. Fixed bug with --id.
61
        None.
62
    :return: Numeric object id, or None if no identifier is given.
63
    """
64
    if identifier is None or identifier == '':
65
        return None
6987.2.2 by Jeroen Vermeulen
Moved Launchpadscript into c.l.scripts, extended OptionParser to handle custom option types.
66
    elif isinstance(identifier, basestring) and identifier == '':
67
        return None
68
    elif isinstance(identifier, int):
6987.2.1 by Jeroen Vermeulen
Made options processing testable, and tested. Fixed bug with --id.
69
        return identifier
6987.2.2 by Jeroen Vermeulen
Moved Launchpadscript into c.l.scripts, extended OptionParser to handle custom option types.
70
    elif identifier.isdigit():
6987.2.1 by Jeroen Vermeulen
Made options processing testable, and tested. Fixed bug with --id.
71
        return int(identifier)
6987.2.2 by Jeroen Vermeulen
Moved Launchpadscript into c.l.scripts, extended OptionParser to handle custom option types.
72
    elif lookup_function is None:
73
        raise OptionValueError("Expected numeric id, got '%s'." % identifier)
6987.2.1 by Jeroen Vermeulen
Made options processing testable, and tested. Fixed bug with --id.
74
    else:
6987.2.2 by Jeroen Vermeulen
Moved Launchpadscript into c.l.scripts, extended OptionParser to handle custom option types.
75
        try:
76
            result = lookup_function(identifier)
77
        except LookupError:
78
            raise OptionValueError("'%s' not found." % identifier)
79
6987.2.1 by Jeroen Vermeulen
Made options processing testable, and tested. Fixed bug with --id.
80
    if result is None:
6987.2.2 by Jeroen Vermeulen
Moved Launchpadscript into c.l.scripts, extended OptionParser to handle custom option types.
81
        raise OptionValueError("'%s' not found." % identifier)
6987.2.1 by Jeroen Vermeulen
Made options processing testable, and tested. Fixed bug with --id.
82
    return result
83
84
6987.2.2 by Jeroen Vermeulen
Moved Launchpadscript into c.l.scripts, extended OptionParser to handle custom option types.
85
def get_person_id(name):
86
    """`get_id` helper.  Look up person by name."""
11666.3.3 by Curtis Hovey
Fixed cyclic imports.
87
    # XXX sinzui 2010-10-04 bug=654537: Account and EmailAddress cause cyclic
88
    # imports because they are not in the lp tree.
89
    from lp.registry.interfaces.person import IPersonSet
6987.2.2 by Jeroen Vermeulen
Moved Launchpadscript into c.l.scripts, extended OptionParser to handle custom option types.
90
    person = getUtility(IPersonSet).getByName(name)
91
    if person is None:
92
        return None
93
    return person.id
94
95
96
def get_origin(name):
97
    """`get_id` helper.  Look up `RosettaTranslationOrigin` by name."""
98
    try:
99
        return getattr(RosettaTranslationOrigin, name).value
100
    except AttributeError:
101
        return None
102
103
13636.1.14 by Henning Eggers
Rename to processing.
104
def process_origin_option(value):
105
    """Validation and conversion for `RosettaTranslationsOrigin`."""
6987.2.2 by Jeroen Vermeulen
Moved Launchpadscript into c.l.scripts, extended OptionParser to handle custom option types.
106
    return get_id(value, get_origin)
107
108
13636.1.14 by Henning Eggers
Rename to processing.
109
def process_person_option(value):
110
    """Validation and conversion for `Person`."""
6987.2.2 by Jeroen Vermeulen
Moved Launchpadscript into c.l.scripts, extended OptionParser to handle custom option types.
111
    return get_id(value, get_person_id)
112
13636.1.19 by Henning Eggers
Expose and use process_options.
113
13636.1.15 by Henning Eggers
List special options.
114
# Options that need special processing.
115
OPTIONS_TO_PROCESS = {
116
    'submitter': process_person_option,
117
    'reviewer': process_person_option,
118
    'origin': process_origin_option,
119
    'is_current_ubuntu': process_bool_option,
120
    'is_current_upstream': process_bool_option,
121
    }
122
13636.1.19 by Henning Eggers
Expose and use process_options.
123
124
def process_options(options):
125
    """Process options that need special processing."""
126
    for option_name, process_func in OPTIONS_TO_PROCESS.items():
127
        option_value = getattr(options, option_name)
13636.1.21 by Henning Eggers
Really fix script. And test.
128
        if option_value is not None:
129
            setattr(options, option_name, process_func(option_value))
13636.1.19 by Henning Eggers
Expose and use process_options.
130
131
6987.2.2 by Jeroen Vermeulen
Moved Launchpadscript into c.l.scripts, extended OptionParser to handle custom option types.
132
def is_nonempty_list(list_option):
133
    """Is list_option a non-empty a nonempty list of option values?"""
134
    return list_option is not None and len(list_option) > 0
135
136
137
def is_nonempty_string(string_option):
138
    """Is string_option a non-empty option value?"""
139
    return string_option is not None and string_option != ''
6987.2.1 by Jeroen Vermeulen
Made options processing testable, and tested. Fixed bug with --id.
140
141
6934.4.1 by Jeroen Vermeulen
New function, and driver script, for admins to delete selected translation messages.
142
def compose_language_match(language_code):
143
    """Compose SQL condition for matching a language in the deletion query.
144
11122.3.4 by Danilo Šegan
Get rid of the remaining variant usage.
145
    :param: Language code to match.
6934.4.1 by Jeroen Vermeulen
New function, and driver script, for admins to delete selected translation messages.
146
    :return: SQL condition in string form.
147
    """
11122.3.17 by Danilo Segan
Apply review comments from jtv as well.
148
    return 'Language.code = %s' % sqlvalues(language_code)
6934.4.1 by Jeroen Vermeulen
New function, and driver script, for admins to delete selected translation messages.
149
150
151
def add_bool_match(conditions, expression, match_value):
152
    """Add match for tri-state Boolean to SQL conditions.
153
154
    :param conditions: Set of SQL condition clauses to add to.
155
    :param expression: Variable or other SQL expression to match on.
156
    :param match_value: If given, the Boolean value to match.  If left
157
        as None, no condition is added.
158
    """
159
    if match_value is None:
160
        return
161
162
    if match_value:
163
        match = expression
164
    else:
165
        match = 'NOT (%s)' % expression
166
    conditions.add(match)
167
168
6987.2.2 by Jeroen Vermeulen
Moved Launchpadscript into c.l.scripts, extended OptionParser to handle custom option types.
169
class RemoveTranslations(LaunchpadScript):
170
    """Remove specific `TranslationMessage`s from the database.
171
172
    The script accepts a wide range of options to specify exactly which
173
    messages need deleting.  It will refuse to run if the options are so
174
    non-specific that the command is more likely to be a mistake than a
175
    valid use case.  In borderline cases, it may be persuaded to run
176
    using a "force" option.
177
    """
178
179
    description = "Delete matching translation messages from the database."
180
    loglevel = logging.INFO
181
182
    my_options = [
13636.1.13 by Henning Eggers
Removed ExtendedOption.
183
        Option(
184
            '-s', '--submitter', dest='submitter',
6987.2.2 by Jeroen Vermeulen
Moved Launchpadscript into c.l.scripts, extended OptionParser to handle custom option types.
185
            help="Submitter match: delete only messages with this "
186
                "submitter."),
13636.1.13 by Henning Eggers
Removed ExtendedOption.
187
        Option(
188
            '-r', '--reviewer', dest='reviewer',
6987.2.2 by Jeroen Vermeulen
Moved Launchpadscript into c.l.scripts, extended OptionParser to handle custom option types.
189
            help="Reviewer match: delete only messages with this reviewer."),
13636.1.13 by Henning Eggers
Removed ExtendedOption.
190
        Option(
7131.5.1 by Jeroen Vermeulen
Unmask imported messages that were obscured by ones we're deleting. Support deletion based on license answer.
191
            '-x', '--reject-license', action='store_true',
192
            dest='reject_license',
7160.3.2 by Jeroen Vermeulen
Review changes.
193
            help="Match submitters who rejected the license agreement."),
13636.1.13 by Henning Eggers
Removed ExtendedOption.
194
        Option(
6987.2.2 by Jeroen Vermeulen
Moved Launchpadscript into c.l.scripts, extended OptionParser to handle custom option types.
195
            '-i', '--id', action='append', dest='ids', type='int',
196
            help="ID of message to delete.  May be specified multiple "
197
                "times."),
13636.1.13 by Henning Eggers
Removed ExtendedOption.
198
        Option(
6987.2.2 by Jeroen Vermeulen
Moved Launchpadscript into c.l.scripts, extended OptionParser to handle custom option types.
199
            '-p', '--potemplate', dest='potemplate', type='int',
200
            help="Template id match.  Delete only messages in this "
201
                "template."),
13636.1.13 by Henning Eggers
Removed ExtendedOption.
202
        Option(
6987.2.2 by Jeroen Vermeulen
Moved Launchpadscript into c.l.scripts, extended OptionParser to handle custom option types.
203
            '-l', '--language', dest='language',
204
            help="Language match.  Deletes (default) or spares (with -L) "
205
                 "messages in this language."),
13636.1.13 by Henning Eggers
Removed ExtendedOption.
206
        Option(
7131.5.1 by Jeroen Vermeulen
Unmask imported messages that were obscured by ones we're deleting. Support deletion based on license answer.
207
            '-L', '--not-language', action='store_true', dest='not_language',
6987.2.2 by Jeroen Vermeulen
Moved Launchpadscript into c.l.scripts, extended OptionParser to handle custom option types.
208
            help="Invert language match: spare messages in given language."),
13636.1.13 by Henning Eggers
Removed ExtendedOption.
209
        Option(
7675.916.98 by Henning Eggers
Merged db-stable at r10026 (recife roll-back) but without accepting the changes.
210
            '-C', '--is-current-ubuntu', dest='is_current_ubuntu',
211
            help="Match on is_current_ubuntu value (True or False)."),
13636.1.13 by Henning Eggers
Removed ExtendedOption.
212
        Option(
7675.916.98 by Henning Eggers
Merged db-stable at r10026 (recife roll-back) but without accepting the changes.
213
            '-I', '--is-current-upstream', dest='is_current_upstream',
214
            help="Match on is_current_upstream value (True or False)."),
13636.1.13 by Henning Eggers
Removed ExtendedOption.
215
        Option(
6987.2.2 by Jeroen Vermeulen
Moved Launchpadscript into c.l.scripts, extended OptionParser to handle custom option types.
216
            '-m', '--msgid', dest='msgid',
217
            help="Match on (singular) msgid text."),
13636.1.13 by Henning Eggers
Removed ExtendedOption.
218
        Option(
219
            '-o', '--origin', dest='origin',
6987.2.2 by Jeroen Vermeulen
Moved Launchpadscript into c.l.scripts, extended OptionParser to handle custom option types.
220
            help="Origin match: delete only messages with this origin code."),
13636.1.13 by Henning Eggers
Removed ExtendedOption.
221
        Option(
7131.5.1 by Jeroen Vermeulen
Unmask imported messages that were obscured by ones we're deleting. Support deletion based on license answer.
222
            '-f', '--force', action='store_true', dest='force',
6987.2.2 by Jeroen Vermeulen
Moved Launchpadscript into c.l.scripts, extended OptionParser to handle custom option types.
223
            help="Override safety check on moderately unsafe action."),
13636.1.13 by Henning Eggers
Removed ExtendedOption.
224
        Option(
7131.5.2 by Jeroen Vermeulen
Finally added that --dry-run option. Removed some test noise.
225
            '-d', '--dry-run', action='store_true', dest='dry_run',
11666.3.7 by Curtis Hovey
Hushed lint.
226
            help="Go through the motions, but don't really delete."),
6987.2.2 by Jeroen Vermeulen
Moved Launchpadscript into c.l.scripts, extended OptionParser to handle custom option types.
227
        ]
228
229
    def add_my_options(self):
230
        """See `LaunchpadScript`."""
231
        self.parser.add_options(self.my_options)
232
233
    def _check_constraints_safety(self):
234
        """Are these options to the deletion script sufficiently safe?
7519.2.61 by Danilo Šegan
Fix remove-translations-by script and related tests.
235
6987.2.2 by Jeroen Vermeulen
Moved Launchpadscript into c.l.scripts, extended OptionParser to handle custom option types.
236
        :return: Boolean approval and output message.  All disapprovals come
237
            with an explanation; some approvals come with an informational
238
            message.
239
        """
240
        if is_nonempty_list(self.options.ids):
241
            return (True, None)
242
        if is_nonempty_string(self.options.submitter):
243
            return (True, None)
244
        if is_nonempty_string(self.options.reviewer):
245
            return (True, None)
246
247
        forced = self.options.force
7131.5.1 by Jeroen Vermeulen
Unmask imported messages that were obscured by ones we're deleting. Support deletion based on license answer.
248
6987.2.2 by Jeroen Vermeulen
Moved Launchpadscript into c.l.scripts, extended OptionParser to handle custom option types.
249
        if is_nonempty_string(self.options.potemplate) and forced:
250
            return (
251
                True,
252
                "Safety override in effect.  Deleting translations for "
253
                "template %s." % self.options.potemplate)
254
7131.5.1 by Jeroen Vermeulen
Unmask imported messages that were obscured by ones we're deleting. Support deletion based on license answer.
255
        if self.options.reject_license:
7675.916.98 by Henning Eggers
Merged db-stable at r10026 (recife roll-back) but without accepting the changes.
256
            if self.options.is_current_upstream == False:
257
                # "Remove non-is_current_upstream messages submitted by users
7131.5.1 by Jeroen Vermeulen
Unmask imported messages that were obscured by ones we're deleting. Support deletion based on license answer.
258
                # who rejected the license."
259
                return (True, None)
260
261
            rosettaweb_key = RosettaTranslationOrigin.ROSETTAWEB.value
262
            if self.options.origin == rosettaweb_key:
263
                # "Remove messages submitted directly in Launchpad by
264
                # users who rejected the license."
265
                return (True, None)
266
267
            if forced:
268
                return (
269
                    True,
270
                    "Safety override in effect.  Removing translations "
271
                    "by users who rejected the license, regardless of "
272
                    "origin.")
273
274
            return (
275
                False,
276
                "To delete the translations by users who "
277
                "rejected the translations license, specify at least "
278
                "--origin=ROSETTAWEB or --is-imported=False.")
279
6987.2.2 by Jeroen Vermeulen
Moved Launchpadscript into c.l.scripts, extended OptionParser to handle custom option types.
280
        return (
281
            False,
282
            "Refusing unsafe deletion.  Use matching options to constrain "
283
            "deletion to a safe subset.")
284
285
    def main(self):
286
        """See `LaunchpadScript`."""
13636.1.21 by Henning Eggers
Really fix script. And test.
287
        process_options(self.options)
6987.2.2 by Jeroen Vermeulen
Moved Launchpadscript into c.l.scripts, extended OptionParser to handle custom option types.
288
        (result, message) = self._check_constraints_safety()
289
        if not result:
290
            raise LaunchpadScriptFailure(message)
291
        if message is not None:
292
            self.logger.warn(message)
293
7131.5.2 by Jeroen Vermeulen
Finally added that --dry-run option. Removed some test noise.
294
        if self.options.dry_run:
295
            self.logger.info("Dry run only.  Not really deleting.")
296
6987.2.2 by Jeroen Vermeulen
Moved Launchpadscript into c.l.scripts, extended OptionParser to handle custom option types.
297
        remove_translations(logger=self.logger,
7131.5.1 by Jeroen Vermeulen
Unmask imported messages that were obscured by ones we're deleting. Support deletion based on license answer.
298
            submitter=self.options.submitter,
299
            reject_license=self.options.reject_license,
7160.3.2 by Jeroen Vermeulen
Review changes.
300
            reviewer=self.options.reviewer,
301
            ids=self.options.ids,
7131.5.1 by Jeroen Vermeulen
Unmask imported messages that were obscured by ones we're deleting. Support deletion based on license answer.
302
            potemplate=self.options.potemplate,
6987.2.2 by Jeroen Vermeulen
Moved Launchpadscript into c.l.scripts, extended OptionParser to handle custom option types.
303
            language_code=self.options.language,
304
            not_language=self.options.not_language,
7675.916.98 by Henning Eggers
Merged db-stable at r10026 (recife roll-back) but without accepting the changes.
305
            is_current_ubuntu=self.options.is_current_ubuntu,
306
            is_current_upstream=self.options.is_current_upstream,
7160.3.2 by Jeroen Vermeulen
Review changes.
307
            msgid_singular=self.options.msgid,
308
            origin=self.options.origin)
7131.5.2 by Jeroen Vermeulen
Finally added that --dry-run option. Removed some test noise.
309
7160.3.2 by Jeroen Vermeulen
Review changes.
310
        if self.options.dry_run:
311
            if self.txn is not None:
312
                self.txn.abort()
313
        else:
7131.5.2 by Jeroen Vermeulen
Finally added that --dry-run option. Removed some test noise.
314
            self.txn.commit()
6987.2.2 by Jeroen Vermeulen
Moved Launchpadscript into c.l.scripts, extended OptionParser to handle custom option types.
315
316
12305.1.3 by Abel Deuring
fixed test failures in remove-translations-by.txt
317
def warn_about_deleting_current_messages(cur, from_text, where_text, logger):
12305.1.2 by Abel Deuring
implemented reviewer's comments
318
    # Deleting currently used translations is a bit harmful. Log
319
    # them so that we have a clue which messages might have to be
320
    # translated again. Note that this script tries to find
321
    # another translation that becomes current -- but only in one
322
    # situation: If we delete a shared translation which is current
323
    # in Ubuntu, a shared translation which is current in upstream
324
    # becomes the current Ubuntu translation. In other cases (deleting
325
    # a diverged translation, deleting a shared translation which
326
    # is current upstream) we do not attempt to find another current
327
    # message.
328
    if logger is not None and logger.getEffectiveLevel() <= logging.WARN:
329
        query = """
12305.1.3 by Abel Deuring
fixed test failures in remove-translations-by.txt
330
            SELECT
331
                TranslationMessage.id, TranslationMessage.is_current_upstream,
332
                TranslationMessage.is_current_ubuntu
333
            FROM %s
334
            WHERE %s AND (
335
                TranslationMessage.is_current_upstream OR
336
                TranslationMessage.is_current_ubuntu)
337
            """ % (from_text, where_text)
12305.1.2 by Abel Deuring
implemented reviewer's comments
338
        cur.execute(query)
339
        rows = cur.fetchall()
340
        if cur.rowcount > 0:
341
            logger.warn(
342
                'Deleting messages currently in use:')
343
            for (id, is_current_upstream, is_current_ubuntu) in rows:
344
                current = []
345
                if is_current_upstream:
346
                    current.append('upstream')
347
                if is_current_ubuntu:
348
                    current.append('Ubuntu')
349
                logger.warn(
350
                    'Message %i is a current translation in %s'
351
                    % (id, ' and '.join(current)))
352
13636.1.6 by Henning Eggers
Removed lint.
353
11666.3.7 by Curtis Hovey
Hushed lint.
354
def remove_translations(logger=None, submitter=None, reviewer=None,
7131.5.1 by Jeroen Vermeulen
Unmask imported messages that were obscured by ones we're deleting. Support deletion based on license answer.
355
                        reject_license=False, ids=None, potemplate=None,
356
                        language_code=None, not_language=False,
7675.916.98 by Henning Eggers
Merged db-stable at r10026 (recife roll-back) but without accepting the changes.
357
                        is_current_ubuntu=None, is_current_upstream=None,
6987.2.1 by Jeroen Vermeulen
Made options processing testable, and tested. Fixed bug with --id.
358
                        msgid_singular=None, origin=None):
6934.4.1 by Jeroen Vermeulen
New function, and driver script, for admins to delete selected translation messages.
359
    """Remove specified translation messages.
360
361
    :param logger: Optional logger to write output to.
362
    :param submitter: Delete only messages submitted by this person.
363
    :param reviewer: Delete only messages reviewed by this person.
7131.5.1 by Jeroen Vermeulen
Unmask imported messages that were obscured by ones we're deleting. Support deletion based on license answer.
364
    :param reject_license: Delete only messages submitted by persons who
365
        have rejected the licensing agreement.
6934.4.1 by Jeroen Vermeulen
New function, and driver script, for admins to delete selected translation messages.
366
    :param ids: Delete only messages with these `TranslationMessage` ids.
367
    :param potemplate: Delete only messages in this template.
6987.2.1 by Jeroen Vermeulen
Made options processing testable, and tested. Fixed bug with --id.
368
    :param language_code: Language code.  Depending on `not_language`,
6934.4.1 by Jeroen Vermeulen
New function, and driver script, for admins to delete selected translation messages.
369
        either delete messages in this language or spare messages in this
370
        language that would otherwise be deleted.
6987.2.1 by Jeroen Vermeulen
Made options processing testable, and tested. Fixed bug with --id.
371
    :param not_language: Whether to spare (True) or delete (False)
6934.4.1 by Jeroen Vermeulen
New function, and driver script, for admins to delete selected translation messages.
372
        messages in this language.
7675.916.98 by Henning Eggers
Merged db-stable at r10026 (recife roll-back) but without accepting the changes.
373
    :param is_current_ubuntu: Delete only messages with this is_current_ubuntu
374
        value.
375
    :param is_current_upstream: Delete only messages with this
376
        is_current_upstream value.
6934.4.1 by Jeroen Vermeulen
New function, and driver script, for admins to delete selected translation messages.
377
    :param msgid_singular: Delete only messages with this singular msgid.
378
    :param origin: Delete only messages with this `TranslationOrigin` code.
379
380
    :return: Number of messages deleted.
381
    """
382
    joins = set()
383
    conditions = set()
384
    if submitter is not None:
385
        conditions.add(
386
            'TranslationMessage.submitter = %s' % sqlvalues(submitter))
387
    if reviewer is not None:
388
        conditions.add(
389
            'TranslationMessage.reviewer = %s' % sqlvalues(reviewer))
7131.5.1 by Jeroen Vermeulen
Unmask imported messages that were obscured by ones we're deleting. Support deletion based on license answer.
390
    if reject_license:
391
        joins.add('TranslationRelicensingAgreement')
392
        conditions.add(
393
            'TranslationMessage.submitter = '
394
            'TranslationRelicensingAgreement.person')
395
        conditions.add(
396
            'NOT TranslationRelicensingAgreement.allow_relicensing')
6934.4.1 by Jeroen Vermeulen
New function, and driver script, for admins to delete selected translation messages.
397
    if ids is not None:
398
        conditions.add('TranslationMessage.id IN %s' % sqlvalues(ids))
399
    if potemplate is not None:
7519.2.61 by Danilo Šegan
Fix remove-translations-by script and related tests.
400
        joins.add('TranslationTemplateItem')
401
        conditions.add(
402
            'TranslationTemplateItem.potmsgset '
403
            ' = TranslationMessage.potmsgset')
404
        conditions.add(
405
            'TranslationTemplateItem.potemplate = %s' % sqlvalues(potemplate))
6934.4.1 by Jeroen Vermeulen
New function, and driver script, for admins to delete selected translation messages.
406
407
    if language_code is not None:
408
        joins.add('Language')
7519.2.61 by Danilo Šegan
Fix remove-translations-by script and related tests.
409
        conditions.add('Language.id = TranslationMessage.language')
6934.4.1 by Jeroen Vermeulen
New function, and driver script, for admins to delete selected translation messages.
410
        language_match = compose_language_match(language_code)
6987.2.1 by Jeroen Vermeulen
Made options processing testable, and tested. Fixed bug with --id.
411
        if not_language:
6934.4.1 by Jeroen Vermeulen
New function, and driver script, for admins to delete selected translation messages.
412
            conditions.add('NOT (%s)' % language_match)
413
        else:
414
            conditions.add(language_match)
415
7675.916.98 by Henning Eggers
Merged db-stable at r10026 (recife roll-back) but without accepting the changes.
416
    add_bool_match(
417
        conditions, 'TranslationMessage.is_current_ubuntu', is_current_ubuntu)
418
    add_bool_match(
419
        conditions, 'TranslationMessage.is_current_upstream',
420
        is_current_upstream)
6934.4.1 by Jeroen Vermeulen
New function, and driver script, for admins to delete selected translation messages.
421
422
    if msgid_singular is not None:
423
        joins.add('POTMsgSet')
424
        conditions.add('POTMsgSet.id = TranslationMessage.potmsgset')
425
        joins.add('POMsgID')
426
        conditions.add('POMsgID.id = POTMsgSet.msgid_singular')
427
        conditions.add('POMsgID.msgid = %s' % sqlvalues(msgid_singular))
428
429
    if origin is not None:
430
        conditions.add('TranslationMessage.origin = %s' % sqlvalues(origin))
431
432
    assert len(conditions) > 0, "That would delete ALL translations, maniac!"
433
7131.5.1 by Jeroen Vermeulen
Unmask imported messages that were obscured by ones we're deleting. Support deletion based on license answer.
434
    cur = cursor()
435
    drop_tables(cur, 'temp_doomed_message')
436
437
    joins.add('TranslationMessage')
438
    from_text = ', '.join(joins)
439
    where_text = ' AND\n    '.join(conditions)
440
12305.1.3 by Abel Deuring
fixed test failures in remove-translations-by.txt
441
    warn_about_deleting_current_messages(cur, from_text, where_text, logger)
12305.1.1 by Abel Deuring
Log warnings in the remove_translations script if current translations are removed. Removed the theoretically impossible case of finding a diverged translation being current in upstream which is linked to a POTemplate having a diverged translation being current in Ubuntu.
442
7160.3.2 by Jeroen Vermeulen
Review changes.
443
    # Keep track of messages we're going to delete.
7131.5.1 by Jeroen Vermeulen
Unmask imported messages that were obscured by ones we're deleting. Support deletion based on license answer.
444
    # Don't bother indexing this.  We'd more likely end up optimizing
445
    # away the operator's "oh-shit-ctrl-c" time than helping anyone.
446
    query = """
447
        CREATE TEMP TABLE temp_doomed_message AS
448
        SELECT TranslationMessage.id, NULL::integer AS imported_message
449
        FROM %s
450
        WHERE %s
451
        """ % (from_text, where_text)
452
    cur.execute(query)
453
12305.1.1 by Abel Deuring
Log warnings in the remove_translations script if current translations are removed. Removed the theoretically impossible case of finding a diverged translation being current in upstream which is linked to a POTemplate having a diverged translation being current in Ubuntu.
454
    # Note which shared messages are masked by the messages we're
455
    # going to delete.  We'll be making those the current ones.
7131.5.1 by Jeroen Vermeulen
Unmask imported messages that were obscured by ones we're deleting. Support deletion based on license answer.
456
    query = """
12305.1.2 by Abel Deuring
implemented reviewer's comments
457
         UPDATE temp_doomed_message
7131.5.1 by Jeroen Vermeulen
Unmask imported messages that were obscured by ones we're deleting. Support deletion based on license answer.
458
        SET imported_message = Imported.id
459
        FROM TranslationMessage Doomed, TranslationMessage Imported
460
        WHERE
461
            Doomed.id = temp_doomed_message.id AND
462
            -- Is alternative for the message we're about to delete.
463
            Imported.potmsgset = Doomed.potmsgset AND
7519.2.61 by Danilo Šegan
Fix remove-translations-by script and related tests.
464
            Imported.language = Doomed.language AND
12305.1.1 by Abel Deuring
Log warnings in the remove_translations script if current translations are removed. Removed the theoretically impossible case of finding a diverged translation being current in upstream which is linked to a POTemplate having a diverged translation being current in Ubuntu.
465
            Imported.potemplate IS NULL AND
466
            Doomed.potemplate IS NULL AND
7675.916.98 by Henning Eggers
Merged db-stable at r10026 (recife roll-back) but without accepting the changes.
467
            -- Is used upstream.
468
            Imported.is_current_upstream IS TRUE AND
7160.3.2 by Jeroen Vermeulen
Review changes.
469
            -- Was masked by the message we're about to delete.
7675.916.98 by Henning Eggers
Merged db-stable at r10026 (recife roll-back) but without accepting the changes.
470
            Doomed.is_current_ubuntu IS TRUE AND
7131.5.1 by Jeroen Vermeulen
Unmask imported messages that were obscured by ones we're deleting. Support deletion based on license answer.
471
            Imported.id <> Doomed.id
472
            """
473
    cur.execute(query)
474
7160.3.2 by Jeroen Vermeulen
Review changes.
475
    if logger is not None and logger.getEffectiveLevel() <= logging.DEBUG:
476
        # Dump sample of doomed messages for debugging purposes.
477
        cur.execute("""
478
            SELECT *
479
            FROM temp_doomed_message
480
            ORDER BY id
481
            LIMIT 20
482
            """)
483
        rows = cur.fetchall()
484
        if cur.rowcount > 0:
485
            logger.debug("Sample of messages to be deleted follows.")
486
            logger.debug("%10s %10s" % ("[message]", "[unmasks]"))
487
            for (doomed, unmasked) in rows:
488
                if unmasked is None:
489
                    unmasked = '--'
490
                logger.debug("%10s %10s" % (doomed, unmasked))
491
7131.5.1 by Jeroen Vermeulen
Unmask imported messages that were obscured by ones we're deleting. Support deletion based on license answer.
492
    cur.execute("""
6934.4.1 by Jeroen Vermeulen
New function, and driver script, for admins to delete selected translation messages.
493
        DELETE FROM TranslationMessage
7131.5.1 by Jeroen Vermeulen
Unmask imported messages that were obscured by ones we're deleting. Support deletion based on license answer.
494
        USING temp_doomed_message
495
        WHERE TranslationMessage.id = temp_doomed_message.id
496
        """)
497
6934.4.1 by Jeroen Vermeulen
New function, and driver script, for admins to delete selected translation messages.
498
    rows_deleted = cur.rowcount
499
    if logger is not None:
500
        if rows_deleted > 0:
501
            logger.info("Deleting %d message(s)." % rows_deleted)
502
        else:
503
            logger.warn("No rows match; not deleting anything.")
504
7131.5.1 by Jeroen Vermeulen
Unmask imported messages that were obscured by ones we're deleting. Support deletion based on license answer.
505
    cur.execute("""
506
        UPDATE TranslationMessage
7675.916.98 by Henning Eggers
Merged db-stable at r10026 (recife roll-back) but without accepting the changes.
507
        SET is_current_ubuntu = TRUE
7131.5.1 by Jeroen Vermeulen
Unmask imported messages that were obscured by ones we're deleting. Support deletion based on license answer.
508
        FROM temp_doomed_message
509
        WHERE TranslationMessage.id = temp_doomed_message.imported_message
510
        """)
511
512
    if cur.rowcount > 0 and logger is not None:
513
        logger.debug("Unmasking %d imported message(s)." % cur.rowcount)
514
515
    drop_tables(cur, 'temp_doomed_message')
516
6934.4.1 by Jeroen Vermeulen
New function, and driver script, for admins to delete selected translation messages.
517
    return rows_deleted