Merge lp:~henninge/launchpad/bug-425645 into lp:launchpad
- bug-425645
- Merge into devel
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Gavin Panella | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | not available | ||||
Proposed branch: | lp:~henninge/launchpad/bug-425645 | ||||
Merge into: | lp:launchpad | ||||
Diff against target: | None lines | ||||
To merge this branch: | bzr merge lp:~henninge/launchpad/bug-425645 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gavin Panella (community) | Approve | ||
Review via email: mp+11430@code.launchpad.net |
Commit message
Description of the change
Henning Eggers (henninge) wrote : | # |
Gavin Panella (allenap) wrote : | # |
Hi Henning,
Because I don't really understand where this is going to fit in -
partly because my knowledge of rosetta is poor, and partly because
there's no page to look at - it was difficult to review. There are
several parts of the view that don't seem used or tested.
So, think about removing things that aren't needed, or add some simple
tests for them. I've also put some comments in the diff, but nothing
that's really going to set you back much.
Thanks!
Gavin.
> === modified file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
> @@ -65,6 +65,7 @@
> ISpecificationSet, SpecificationDe
> from lp.translations
> ITranslationGro
> +from lp.translations
> from canonical.
> from lp.services.
> from canonical.
> @@ -104,6 +105,7 @@
> from lp.registry.
> ISourcePackageN
> from lp.registry.
> +from lp.services.
> from lp.soyuz.
> from lp.soyuz.
> from lp.testing import run_with_login, time_counter
> @@ -485,6 +487,15 @@
> return getUtility(
> name, title, summary, url, owner)
>
> + def makeTranslator(
> + """Create a new, arbitrary `Translator`."""
> + language = getUtility(
> + if group is None:
> + group = self.makeTransl
> + if person is None:
> + person = self.makePerson()
> + return getUtility(
> +
> def makeMilestone(
> self, product=None, distribution=None, productseries=None, name=None):
> if product is None and distribution is None and productseries is None:
> @@ -755,7 +766,6 @@
> :param branch: The branch that should be the default stacked-on
> branch.
> """
> - from lp.testing import run_with_login
> # 'branch' might be private, so we remove the security proxy to get at
> # the methods.
> naked_branch = removeSecurityP
>
> === modified file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
> @@ -49,6 +49,8 @@
> from canonical.
> from canonical.
>
> +from canonical.launchpad import _
> +
>
> class CustomDropdownW
> def _div(self, cssClass, contents, **kw):
> @@ -134,6 +136,261 @@
> links = ('description', 'translate', 'upload', 'download')
>
>
> +class POFileBaseView(
Henning Eggers (henninge) wrote : | # |
Am 09.09.2009 18:33, Gavin Panella schrieb:
> Review: Needs Fixing code
> Hi Henning,
>
> Because I don't really understand where this is going to fit in -
> partly because my knowledge of rosetta is poor, and partly because
> there's no page to look at - it was difficult to review. There are
> several parts of the view that don't seem used or tested.
I am sorry you got caught in this. I forgot to mention the blueprint to
give you a sense of the larger picture that we are working on but I
don't know if that would have helped *that* much.
This is really just about having a view class in the first place. It
will receive further addition in the future as the page is developed and
templates use it. Thank you for bearing with us here ;-)
>
> So, think about removing things that aren't needed, or add some simple
> tests for them. I've also put some comments in the diff, but nothing
> that's really going to set you back much.
I think I was able to address or at least explain away all of your
concerns. The code is better now thanks to your review.
>
> Thanks!
>
> Gavin.
>
Thank you very much. Please find my comments below. I will paste an
incremental diff.
Henning
>
>> === modified file 'lib/lp/
[...]
>> === modified file 'lib/lp/
>> --- lib/lp/
>> +++ lib/lp/
>> @@ -49,6 +49,8 @@
>> from canonical.
>> from canonical.
>>
>> +from canonical.launchpad import _
>> +
>>
>> class CustomDropdownW
>> def _div(self, cssClass, contents, **kw):
>> @@ -134,6 +136,261 @@
>> links = ('description', 'translate', 'upload', 'download')
>>
>>
>> +class POFileBaseView(
>> + """A basic view for a POFile
>> +
>> + This view is different from POFileView as it is the base for a new
>> + generation of POFile views that use the new TranslatableMessage class
>> + to display messages. They will eventually replace POFileView and its
>> + decendants."""
>> +
>> + DEFAULT_SHOW = 'all'
>> + DEFAULT_SIZE = 10
>> +
>> + def initialize(self):
>> + super(POFileBas
>> +
>> + self._initializ
>> +
>> + self.batchnav = self._buildBatc
>> + # These two variables are stored for the sole purpose of being
>> + # output in hidden inputs that preserve the current navigation
>> + # when submitting forms.
>> + self.start = self.batchnav.start
>> + self.size = self.batchnav.
>
> Consider giving them more descriptive names, like batch_start and
> batch_size. Also, they're not used or tested, so consider ditching
> them.
Yeah, I ditched them although they will probably come back in a later
iteration ...
>
>> +
>> +
>> + @cachedproperty
>> + def contributors(self):
>> + return list(self.
>
> Is this premature optimisation? In any case, consider using an
> immutable type like a tuple or frozenset for a cache...
Henning Eggers (henninge) wrote : | # |
=== modified file 'lib/lp/
--- lib/lp/
+++ lib/lp/
@@ -151,16 +151,11 @@
- # These two variables are stored for the sole purpose of being
- # output in hidden inputs that preserve the current navigation
- # when submitting forms.
- self.start = self.batchnav.start
- self.size = self.batchnav.
@cachedpro
def contributors(self):
- return list(self.
+ return tuple(self.
@property
def user_can_
@@ -180,31 +175,21 @@
"""
if self.user_can_edit:
- statement = _("You have full access to this translation.")
- else:
- if self.user_
- statement = _("Your suggestions will be held for review by "
- "the managers of this translation.")
- else:
- # Check for logged in state
- if self.user is None:
- statement = _("You are not logged in. Please log in to "
- "work on translations.")
- else:
- if not self.has_
- statement = _("This translation is not open for "
- "changes.")
- else:
- if self.is_managed:
- statement = _("This template can be translated "
- "only by its managers.")
- else:
- statement = _("There is nobody to manage "
- "translation into this particular "
- "language. If you are interested "
- "in working on it, please contact "
- "the translation group.")
- return statement
+ return _("You have full access to this translation.")
+ if self.user_
+ return _("Your suggestions will be held for review by "
+ "the managers of this translation.")
+ # Check for logged in state
+ if self.user is None:
+ return _("You are not logged in. Please log in to "
+ "work on translations.")
+ if not self.has_
+ return _("This translation is not open for changes.")
+ if self.is_managed:
+ return _("This template can be translated only by its managers.")
+ return _("There is nobody to manage translation into this particular "
+ "language. If you are interested in working on it, please "
+ "contact the translation group.")
@property
def translation_
@@ -225,6 +210,8 @@
Gavin Panella (allenap) wrote : | # |
On Wed, 09 Sep 2009 18:03:08 -0000
Henning Eggers <email address hidden> wrote:
> Am 09.09.2009 18:33, Gavin Panella schrieb:
> > Review: Needs Fixing code
> > Hi Henning,
> >
> > Because I don't really understand where this is going to fit in -
> > partly because my knowledge of rosetta is poor, and partly because
> > there's no page to look at - it was difficult to review. There are
> > several parts of the view that don't seem used or tested.
>
> I am sorry you got caught in this. I forgot to mention the blueprint to
> give you a sense of the larger picture that we are working on but I
> don't know if that would have helped *that* much.
>
> This is really just about having a view class in the first place. It
> will receive further addition in the future as the page is developed and
> templates use it. Thank you for bearing with us here ;-)
No worries, I wasn't grumbling, just giving you fair warning that I
might have missed the point on several occassions during the review.
>
> >
> > So, think about removing things that aren't needed, or add some simple
> > tests for them. I've also put some comments in the diff, but nothing
> > that's really going to set you back much.
>
> I think I was able to address or at least explain away all of your
> concerns. The code is better now thanks to your review.
Excellent :)
>
> >
> > Thanks!
> >
> > Gavin.
> >
>
> Thank you very much. Please find my comments below. I will paste an
> incremental diff.
The diff all looks good.
review approve
merge approve
>
> Henning
>
>
>
> >
> >> === modified file 'lib/lp/
> [...]
> >> === modified file 'lib/lp/
> >> --- lib/lp/
> >> +++ lib/lp/
> >> @@ -49,6 +49,8 @@
> >> from canonical.
> >> from canonical.
> >>
> >> +from canonical.launchpad import _
> >> +
> >>
> >> class CustomDropdownW
> >> def _div(self, cssClass, contents, **kw):
> >> @@ -134,6 +136,261 @@
> >> links = ('description', 'translate', 'upload', 'download')
> >>
> >>
> >> +class POFileBaseView(
> >> + """A basic view for a POFile
> >> +
> >> + This view is different from POFileView as it is the base for a new
> >> + generation of POFile views that use the new TranslatableMessage class
> >> + to display messages. They will eventually replace POFileView and its
> >> + decendants."""
> >> +
> >> + DEFAULT_SHOW = 'all'
> >> + DEFAULT_SIZE = 10
> >> +
> >> + def initialize(self):
> >> + super(POFileBas
> >> +
> >> + self._initializ
> >> +
> >> + self.batchnav = self._buildBatc
> >> + # These two variables are stored for the sole purpose of being
> >> + # output in hidden inputs that preserve the current navigation
> >> + # when submitting forms.
> >> + self.start = self.batchnav.start
> >> + self.size = self.batchnav.
> >
> > Consider giving them mor...
Preview Diff
1 | === modified file 'lib/lp/testing/factory.py' |
2 | --- lib/lp/testing/factory.py 2009-09-04 12:17:11 +0000 |
3 | +++ lib/lp/testing/factory.py 2009-09-09 11:04:58 +0000 |
4 | @@ -65,6 +65,7 @@ |
5 | ISpecificationSet, SpecificationDefinitionStatus) |
6 | from lp.translations.interfaces.translationgroup import ( |
7 | ITranslationGroupSet) |
8 | +from lp.translations.interfaces.translator import ITranslatorSet |
9 | from canonical.launchpad.ftests._sqlobject import syncUpdate |
10 | from lp.services.mail.signedmessage import SignedMessage |
11 | from canonical.launchpad.webapp.dbpolicy import MasterDatabasePolicy |
12 | @@ -104,6 +105,7 @@ |
13 | from lp.registry.interfaces.sourcepackagename import ( |
14 | ISourcePackageNameSet) |
15 | from lp.registry.interfaces.ssh import ISSHKeySet, SSHKeyType |
16 | +from lp.services.worlddata.interfaces.language import ILanguageSet |
17 | from lp.soyuz.interfaces.component import IComponentSet |
18 | from lp.soyuz.interfaces.packageset import IPackagesetSet |
19 | from lp.testing import run_with_login, time_counter |
20 | @@ -485,6 +487,15 @@ |
21 | return getUtility(ITranslationGroupSet).new( |
22 | name, title, summary, url, owner) |
23 | |
24 | + def makeTranslator(self, language_code, group=None, person=None): |
25 | + """Create a new, arbitrary `Translator`.""" |
26 | + language = getUtility(ILanguageSet).getLanguageByCode(language_code) |
27 | + if group is None: |
28 | + group = self.makeTranslationGroup() |
29 | + if person is None: |
30 | + person = self.makePerson() |
31 | + return getUtility(ITranslatorSet).new(group, language, person) |
32 | + |
33 | def makeMilestone( |
34 | self, product=None, distribution=None, productseries=None, name=None): |
35 | if product is None and distribution is None and productseries is None: |
36 | @@ -755,7 +766,6 @@ |
37 | :param branch: The branch that should be the default stacked-on |
38 | branch. |
39 | """ |
40 | - from lp.testing import run_with_login |
41 | # 'branch' might be private, so we remove the security proxy to get at |
42 | # the methods. |
43 | naked_branch = removeSecurityProxy(branch) |
44 | |
45 | === modified file 'lib/lp/translations/browser/pofile.py' |
46 | --- lib/lp/translations/browser/pofile.py 2009-08-31 13:06:42 +0000 |
47 | +++ lib/lp/translations/browser/pofile.py 2009-09-09 11:04:58 +0000 |
48 | @@ -47,6 +47,8 @@ |
49 | from canonical.launchpad.webapp.batching import BatchNavigator |
50 | from canonical.launchpad.webapp.menu import structured |
51 | |
52 | +from canonical.launchpad import _ |
53 | + |
54 | |
55 | class CustomDropdownWidget(DropdownWidget): |
56 | def _div(self, cssClass, contents, **kw): |
57 | @@ -132,6 +134,261 @@ |
58 | links = ('description', 'translate', 'upload', 'download') |
59 | |
60 | |
61 | +class POFileBaseView(LaunchpadView): |
62 | + """A basic view for a POFile |
63 | + |
64 | + This view is different from POFileView as it is the base for a new |
65 | + generation of POFile views that use the new TranslatableMessage class |
66 | + to display messages. They will eventually replace POFileView and its |
67 | + decendants.""" |
68 | + |
69 | + DEFAULT_SHOW = 'all' |
70 | + DEFAULT_SIZE = 10 |
71 | + |
72 | + def initialize(self): |
73 | + super(POFileBaseView, self).initialize() |
74 | + |
75 | + self._initializeShowOption() |
76 | + |
77 | + self.batchnav = self._buildBatchNavigator() |
78 | + # These two variables are stored for the sole purpose of being |
79 | + # output in hidden inputs that preserve the current navigation |
80 | + # when submitting forms. |
81 | + self.start = self.batchnav.start |
82 | + self.size = self.batchnav.currentBatch().size |
83 | + |
84 | + |
85 | + @cachedproperty |
86 | + def contributors(self): |
87 | + return list(self.context.contributors) |
88 | + |
89 | + @property |
90 | + def user_can_edit(self): |
91 | + """Does the user have full edit rights for this translation?""" |
92 | + return self.context.canEditTranslations(self.user) |
93 | + |
94 | + @property |
95 | + def user_can_suggest(self): |
96 | + """Is the user allowed to make suggestions here?""" |
97 | + return self.context.canAddSuggestions(self.user) |
98 | + |
99 | + @property |
100 | + def permission_statement(self): |
101 | + """Construct the statement about permissions. |
102 | + |
103 | + Explain the permissions the current user has on this pofile. |
104 | + """ |
105 | + |
106 | + if self.user_can_edit: |
107 | + statement = _("You have full access to this translation.") |
108 | + else: |
109 | + if self.user_can_suggest: |
110 | + statement = _("Your suggestions will be held for review by " |
111 | + "the managers of this translation.") |
112 | + else: |
113 | + # Check for logged in state |
114 | + if self.user is None: |
115 | + statement = _("You are not logged in. Please log in to " |
116 | + "work on translations.") |
117 | + else: |
118 | + if not self.has_translationgroup: |
119 | + statement = _("This translation is not open for " |
120 | + "changes.") |
121 | + else: |
122 | + if self.is_managed: |
123 | + statement = _("This template can be translated " |
124 | + "only by its managers.") |
125 | + else: |
126 | + statement = _("There is nobody to manage " |
127 | + "translation into this particular " |
128 | + "language. If you are interested " |
129 | + "in working on it, please contact " |
130 | + "the translation group.") |
131 | + return statement |
132 | + |
133 | + @property |
134 | + def translation_groups_statement(self): |
135 | + """List translation groups and translation teams for this translation. |
136 | + |
137 | + Returns a HTML string that lists the translation groups and the |
138 | + relevant translators for this translation. |
139 | + """ |
140 | + if self.translation_group is not None: |
141 | + language = self.context.language |
142 | + groups = [] |
143 | + for group in self.context.potemplate.translationgroups: |
144 | + translator = group.query_translator(language) |
145 | + # XXX: henninge 2009-09-09 bug=426745: |
146 | + # The group and translator should be linkified. |
147 | + if translator is None: |
148 | + groups.append(_(u"%s translation group") % group.title) |
149 | + else: |
150 | + groups.append(_(u"%s assigned by %s") % ( |
151 | + translator.translator.displayname, group.title)) |
152 | + statement = (_(u"This translation is managed by ") + |
153 | + _(u" and ").join(groups))+"." |
154 | + else: |
155 | + statement = _(u"No translation group has been assigned.") |
156 | + return statement |
157 | + |
158 | + @property |
159 | + def has_plural_form_information(self): |
160 | + """Return whether we know the plural forms for this language.""" |
161 | + if self.context.potemplate.hasPluralMessage(): |
162 | + return self.context.language.pluralforms is not None |
163 | + # If there are no plural forms, we assume that we have the |
164 | + # plural form information for this language. |
165 | + return True |
166 | + |
167 | + @property |
168 | + def number_of_plural_forms(self): |
169 | + """The number of plural forms for the language or 1 if not known.""" |
170 | + if self.context.language.pluralforms is not None: |
171 | + return self.context.language.pluralforms |
172 | + return 1 |
173 | + |
174 | + @property |
175 | + def plural_expression(self): |
176 | + """The plural expression for this language or the empty string.""" |
177 | + if self.context.language.pluralexpression is not None: |
178 | + return self.context.language.pluralexpression |
179 | + return "" |
180 | + |
181 | + @cachedproperty |
182 | + def translation_group(self): |
183 | + """Is there a translation group for this translation? |
184 | + |
185 | + :return: TranslationGroup or None if not found. |
186 | + """ |
187 | + translation_groups = self.context.potemplate.translationgroups |
188 | + if translation_groups is not None and len(translation_groups) > 0: |
189 | + group = translation_groups[0] |
190 | + else: |
191 | + group = None |
192 | + return group |
193 | + |
194 | + def _get_translator_entry(self): |
195 | + """The translator entry or None if none is assigned.""" |
196 | + group = self.translation_group |
197 | + if group is not None: |
198 | + return group.query_translator(self.context.language) |
199 | + return None |
200 | + |
201 | + @cachedproperty |
202 | + def translator(self): |
203 | + """Who is assigned for translations to this language?""" |
204 | + translator_entry = self._get_translator_entry() |
205 | + if translator_entry is not None: |
206 | + return translator_entry.translator |
207 | + return None |
208 | + |
209 | + @cachedproperty |
210 | + def has_any_documentation(self): |
211 | + """Return whether there is any documentation for this POFile.""" |
212 | + if (self.translation_group is not None and |
213 | + self.translation_group.translation_guide_url is not None): |
214 | + return True |
215 | + translator_entry = self._get_translator_entry() |
216 | + if (translator_entry is not None and |
217 | + translator_entry.style_guide_url is not None): |
218 | + return True |
219 | + return False |
220 | + |
221 | + def _initializeShowOption(self): |
222 | + # Get any value given by the user |
223 | + self.show = self.request.form.get('show') |
224 | + self.search_text = self.request.form.get('search') |
225 | + if self.search_text is not None: |
226 | + self.show = 'all' |
227 | + |
228 | + # Functions that deliver the correct message counts for each |
229 | + # valid option value. |
230 | + count_functions = { |
231 | + 'all': self.context.messageCount, |
232 | + 'translated': self.context.translatedCount, |
233 | + 'untranslated': self.context.untranslatedCount, |
234 | + 'new_suggestions': self.context.unreviewedCount, |
235 | + 'changed_in_launchpad': self.context.updatesCount, |
236 | + } |
237 | + |
238 | + if self.show not in count_functions: |
239 | + self.show = self.DEFAULT_SHOW |
240 | + |
241 | + self.shown_count = count_functions[self.show]() |
242 | + |
243 | + def _buildBatchNavigator(self): |
244 | + """Construct a BatchNavigator of POTMsgSets and return it.""" |
245 | + |
246 | + # Changing the "show" option resets batching. |
247 | + old_show_option = self.request.form.get('old_show') |
248 | + show_option_changed = ( |
249 | + old_show_option is not None and old_show_option != self.show) |
250 | + if show_option_changed: |
251 | + force_start = True # start will be 0, by default |
252 | + else: |
253 | + force_start = False |
254 | + return POFileBatchNavigator(self._getSelectedPOTMsgSets(), |
255 | + self.request, size=self.DEFAULT_SIZE, |
256 | + transient_parameters=["old_show"], |
257 | + force_start=force_start) |
258 | + |
259 | + def _handleShowAll(self): |
260 | + """Get `POTMsgSet`s when filtering for "all" (but possibly searching). |
261 | + |
262 | + Normally returns all `POTMsgSet`s for this `POFile`, but also handles |
263 | + search requests which act as a separate form of filtering. |
264 | + """ |
265 | + if self.search_text is None: |
266 | + return self.context.potemplate.getPOTMsgSets() |
267 | + |
268 | + if len(self.search_text) <= 1: |
269 | + self.request.response.addWarningNotification( |
270 | + "Please try searching for a longer string.") |
271 | + return self.context.potemplate.getPOTMsgSets() |
272 | + |
273 | + return self.context.findPOTMsgSetsContaining(text=self.search_text) |
274 | + |
275 | + def _getSelectedPOTMsgSets(self): |
276 | + """Return a list of the POTMsgSets that will be rendered.""" |
277 | + # The set of message sets we get is based on the selection of kind |
278 | + # of strings we have in our form. |
279 | + get_functions = { |
280 | + 'all': self._handleShowAll, |
281 | + 'translated': self.context.getPOTMsgSetTranslated, |
282 | + 'untranslated': self.context.getPOTMsgSetUntranslated, |
283 | + 'new_suggestions': self.context.getPOTMsgSetWithNewSuggestions, |
284 | + 'changed_in_launchpad': |
285 | + self.context.getPOTMsgSetChangedInLaunchpad, |
286 | + } |
287 | + |
288 | + if self.show not in get_functions: |
289 | + raise UnexpectedFormData('show = "%s"' % self.show) |
290 | + |
291 | + # We cannot listify the results to avoid additional count queries, |
292 | + # because we could end up with a list of more than 32000 items with |
293 | + # an average list of 5000 items. |
294 | + # The batch system will slice the list of items so we will fetch only |
295 | + # the exact number of entries we need to render the page. |
296 | + return get_functions[self.show]() |
297 | + |
298 | + @property |
299 | + def messages(self): |
300 | + """The list of TranslatableMessages to show.""" |
301 | + last = None |
302 | + messages = [] |
303 | + for potmsgset in self.batchnav.currentBatch(): |
304 | + assert (last is None or |
305 | + potmsgset.getSequence( |
306 | + self.context.potemplate) >= last.getSequence( |
307 | + self.context.potemplate)), ( |
308 | + "POTMsgSets on page not in ascending sequence order") |
309 | + last = potmsgset |
310 | + |
311 | + messages.append( |
312 | + self.context.makeTranslatableMessage(potmsgset)) |
313 | + return messages |
314 | + |
315 | + |
316 | class POFileView(LaunchpadView): |
317 | """A basic view for a POFile""" |
318 | |
319 | |
320 | === added file 'lib/lp/translations/browser/tests/pofile-base-views.txt' |
321 | --- lib/lp/translations/browser/tests/pofile-base-views.txt 1970-01-01 00:00:00 +0000 |
322 | +++ lib/lp/translations/browser/tests/pofile-base-views.txt 2009-09-09 11:04:58 +0000 |
323 | @@ -0,0 +1,119 @@ |
324 | +POFileBaseView |
325 | +============== |
326 | + |
327 | +POFileBaseView provides different basic information about a POFile and a list |
328 | +of its content as TranslatableMessage objects. |
329 | + |
330 | + >>> from lp.translations.browser.pofile import POFileBaseView |
331 | + >>> from canonical.launchpad.webapp.servers import LaunchpadTestRequest |
332 | + |
333 | + >>> potemplate = factory.makePOTemplate() |
334 | + >>> pofile = factory.makePOFile('eo', potemplate) |
335 | + |
336 | + >>> view = POFileBaseView(pofile, LaunchpadTestRequest()) |
337 | + >>> view.initialize() |
338 | + |
339 | +The view provides a statement to be displayed to the user about the |
340 | +permissions the user has on the POFile. When not logged in, the user cannot |
341 | +work on translations. |
342 | + |
343 | + >>> print view.permission_statement |
344 | + You are not logged in. Please log in to work on translations. |
345 | + |
346 | +So we'd better log in. |
347 | + |
348 | + >>> login('foo.bar@canonical.com') |
349 | + >>> view = POFileBaseView(pofile, LaunchpadTestRequest()) |
350 | + >>> view.initialize() |
351 | + >>> print view.permission_statement |
352 | + You have full access to this translation. |
353 | + |
354 | +The view also provides more detailed information about what the user can do. |
355 | + |
356 | + >>> print view.user_can_edit |
357 | + True |
358 | + >>> print view.user_can_suggest |
359 | + True |
360 | + |
361 | +The view has information about the languages plural forms: |
362 | + |
363 | + >>> print view.has_plural_form_information |
364 | + True |
365 | + >>> print view.number_of_plural_forms |
366 | + 2 |
367 | + >>> print view.plural_expression |
368 | + n != 1 |
369 | + |
370 | +The view also know about the contributers to the translations in this POFile |
371 | +but currently there have not been any contributions yet. |
372 | + |
373 | + >>> print view.contributors |
374 | + [] |
375 | + |
376 | +So let's make a contribution. |
377 | + |
378 | + >>> contributor = factory.makePerson(displayname="Contri Butor") |
379 | + >>> potmsgset = factory.makePOTMsgSet(potemplate, sequence=1) |
380 | + >>> translation = factory.makeTranslationMessage(pofile, potmsgset, |
381 | + ... translator=contributor, reviewer=contributor, |
382 | + ... translations=['A translation made by a contributor.']) |
383 | + >>> view = POFileBaseView(pofile, LaunchpadTestRequest()) |
384 | + >>> view.initialize() |
385 | + >>> print view.contributors[0].displayname |
386 | + Contri Butor |
387 | + |
388 | +The view has a list of all translations. |
389 | + |
390 | + >>> print view.messages[0].getCurrentTranslation().msgstr0.translation |
391 | + A translation made by a contributor. |
392 | + |
393 | +The view can also tell us about the translation group but the pofile is not |
394 | +yet managed by a translation group. |
395 | + |
396 | + >>> print view.translation_group |
397 | + None |
398 | + |
399 | +The view makes a nice statement about this fact, too. |
400 | + |
401 | + >>> print view.translation_groups_statement |
402 | + No translation group has been assigned. |
403 | + |
404 | +So let's create one and let it manage the translations. |
405 | + |
406 | + >>> group = factory.makeTranslationGroup(title="Test Translators") |
407 | + >>> potemplate.product.translationgroup = group |
408 | + >>> view = POFileBaseView(pofile, LaunchpadTestRequest()) |
409 | + >>> view.initialize() |
410 | + >>> print view.translation_group.title |
411 | + Test Translators |
412 | + |
413 | +Who is assigned to do translations into this language? Nobody so far. |
414 | + |
415 | + >>> print view.translator |
416 | + None |
417 | + |
418 | +Let's change that. A single person can be a tanslator |
419 | + |
420 | + >>> translator = factory.makeTranslator('eo', group, contributor) |
421 | + >>> view = POFileBaseView(pofile, LaunchpadTestRequest()) |
422 | + >>> view.initialize() |
423 | + >>> print view.translator.displayname |
424 | + Contri Butor |
425 | + |
426 | +See what statement the view makes now. |
427 | + |
428 | + >>> print view.translation_groups_statement |
429 | + This translation is managed by Contri Butor assigned by Test Translators. |
430 | + |
431 | +Neither the group nor the translator have managed to setup some documentation. |
432 | + |
433 | + >>> view.has_any_documentation |
434 | + False |
435 | + |
436 | +But now the group finally got around to it. |
437 | + |
438 | + >>> group.translation_guide_url = "https://launchpad.net/" |
439 | + >>> view = POFileBaseView(pofile, LaunchpadTestRequest()) |
440 | + >>> view.initialize() |
441 | + >>> view.has_any_documentation |
442 | + True |
443 | |
444 | === added file 'lib/lp/translations/browser/tests/test_pofile_view.py' |
445 | --- lib/lp/translations/browser/tests/test_pofile_view.py 1970-01-01 00:00:00 +0000 |
446 | +++ lib/lp/translations/browser/tests/test_pofile_view.py 2009-09-09 11:04:58 +0000 |
447 | @@ -0,0 +1,123 @@ |
448 | +# Copyright 2009 Canonical Ltd. This software is licensed under the |
449 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
450 | + |
451 | +__metaclass__ = type |
452 | + |
453 | +from datetime import datetime, timedelta |
454 | +import pytz |
455 | +from unittest import TestLoader |
456 | + |
457 | +from canonical.launchpad.webapp.servers import LaunchpadTestRequest |
458 | +from canonical.testing import LaunchpadZopelessLayer |
459 | +from lp.testing import TestCaseWithFactory |
460 | +from lp.translations.browser.pofile import POFileBaseView |
461 | + |
462 | + |
463 | +class TestPOFileBaseView(TestCaseWithFactory): |
464 | + """Test POFileBaseView.""" |
465 | + |
466 | + layer = LaunchpadZopelessLayer |
467 | + |
468 | + def setUp(self): |
469 | + super(TestPOFileBaseView, self).setUp() |
470 | + self.potemplate = self.factory.makePOTemplate() |
471 | + self.pofile = self.factory.makePOFile('eo', self.potemplate) |
472 | + |
473 | + |
474 | + |
475 | +class TestPOFileBaseViewFiltering(TestCaseWithFactory): |
476 | + """Test POFileBaseView filtering functions.""" |
477 | + |
478 | + layer = LaunchpadZopelessLayer |
479 | + |
480 | + def gen_now(self): |
481 | + now = datetime.now(pytz.UTC) |
482 | + while True: |
483 | + yield now |
484 | + now += timedelta(milliseconds=1) |
485 | + |
486 | + def setUp(self): |
487 | + super(TestPOFileBaseViewFiltering, self).setUp() |
488 | + self.now = self.gen_now().next |
489 | + self.potemplate = self.factory.makePOTemplate() |
490 | + self.pofile = self.factory.makePOFile('eo', self.potemplate) |
491 | + |
492 | + # Create a number of POTMsgsets in different states. |
493 | + # An untranslated message. |
494 | + self.untranslated = self.factory.makePOTMsgSet( |
495 | + self.potemplate, sequence=1) |
496 | + # A translated message. |
497 | + self.translated = self.factory.makePOTMsgSet( |
498 | + self.potemplate, sequence=2) |
499 | + self.factory.makeTranslationMessage(self.pofile, self.translated) |
500 | + # A translated message with a new suggestion. |
501 | + self.new_suggestion = self.factory.makePOTMsgSet( |
502 | + self.potemplate, sequence=3) |
503 | + self.factory.makeTranslationMessage( |
504 | + self.pofile, self.new_suggestion, |
505 | + date_updated=self.now()) |
506 | + self.factory.makeTranslationMessage( |
507 | + self.pofile, self.new_suggestion, suggestion=True, |
508 | + date_updated=self.now()) |
509 | + # An imported that was changed in Launchpad. |
510 | + self.changed = self.factory.makePOTMsgSet( |
511 | + self.potemplate, sequence=4) |
512 | + self.factory.makeTranslationMessage( |
513 | + self.pofile, self.changed, is_imported=True, |
514 | + date_updated=self.now()) |
515 | + self.factory.makeTranslationMessage( |
516 | + self.pofile, self.changed, |
517 | + date_updated=self.now()) |
518 | + |
519 | + def _assertEqualPOTMsgSets(self, expected, messages): |
520 | + self.assertEqual(expected, [tm.potmsgset for tm in messages]) |
521 | + |
522 | + def test_show_all_messages(self): |
523 | + view = POFileBaseView(self.pofile, LaunchpadTestRequest()) |
524 | + view.initialize() |
525 | + self._assertEqualPOTMsgSets( |
526 | + [self.untranslated, self.translated, |
527 | + self.new_suggestion, self.changed], |
528 | + view.messages) |
529 | + |
530 | + def test_show_translated(self): |
531 | + form = {'show': 'translated'} |
532 | + view = POFileBaseView(self.pofile, LaunchpadTestRequest(form=form)) |
533 | + view.initialize() |
534 | + self._assertEqualPOTMsgSets( |
535 | + [self.translated, self.new_suggestion, self.changed], |
536 | + view.messages) |
537 | + |
538 | + def test_show_untranslated(self): |
539 | + form = {'show': 'untranslated'} |
540 | + view = POFileBaseView(self.pofile, LaunchpadTestRequest(form=form)) |
541 | + view.initialize() |
542 | + self._assertEqualPOTMsgSets([self.untranslated], view.messages) |
543 | + |
544 | + def test_show_new_suggestions(self): |
545 | + form = {'show': 'new_suggestions'} |
546 | + view = POFileBaseView(self.pofile, LaunchpadTestRequest(form=form)) |
547 | + view.initialize() |
548 | + self._assertEqualPOTMsgSets([self.new_suggestion], view.messages) |
549 | + |
550 | + def test_show_changed_in_launchpad(self): |
551 | + form = {'show': 'changed_in_launchpad'} |
552 | + view = POFileBaseView(self.pofile, LaunchpadTestRequest(form=form)) |
553 | + view.initialize() |
554 | + self._assertEqualPOTMsgSets( |
555 | + [self.changed], view.messages) |
556 | + |
557 | + def test_show_invalid_filter(self): |
558 | + # Invalid filter strings default to showing all messages. |
559 | + form = {'show': 'foo_bar'} |
560 | + view = POFileBaseView(self.pofile, LaunchpadTestRequest(form=form)) |
561 | + view.initialize() |
562 | + self._assertEqualPOTMsgSets( |
563 | + [self.untranslated, self.translated, |
564 | + self.new_suggestion, self.changed], |
565 | + view.messages) |
566 | + |
567 | + |
568 | +def test_suite(): |
569 | + return TestLoader().loadTestsFromName(__name__) |
570 | + |
571 | |
572 | === modified file 'lib/lp/translations/interfaces/pofile.py' |
573 | --- lib/lp/translations/interfaces/pofile.py 2009-08-07 17:12:44 +0000 |
574 | +++ lib/lp/translations/interfaces/pofile.py 2009-09-09 11:04:58 +0000 |
575 | @@ -189,6 +189,12 @@ |
576 | `date_created` with newest first. |
577 | """ |
578 | |
579 | + def makeTranslatableMessage(potmsgset): |
580 | + """Factory method for an `ITranslatableMessage` object. |
581 | + |
582 | + :param potmsgset: The `IPOTMsgSet` to combine this pofile with. |
583 | + """ |
584 | + |
585 | def export(ignore_obsolete=False, export_utf8=False): |
586 | """Export this PO file as string. |
587 | |
588 | |
589 | === modified file 'lib/lp/translations/model/pofile.py' |
590 | --- lib/lp/translations/model/pofile.py 2009-08-18 11:26:49 +0000 |
591 | +++ lib/lp/translations/model/pofile.py 2009-09-09 11:04:58 +0000 |
592 | @@ -61,6 +61,7 @@ |
593 | from lp.translations.interfaces.translationsperson import ( |
594 | ITranslationsPerson) |
595 | from lp.translations.interfaces.translations import TranslationConstants |
596 | +from lp.translations.model.translatablemessage import TranslatableMessage |
597 | from lp.translations.utilities.translation_common_format import ( |
598 | TranslationMessageData) |
599 | from canonical.launchpad.webapp.publisher import canonical_url |
600 | @@ -410,6 +411,10 @@ |
601 | """See `IPOFile`.""" |
602 | return self.language.getFullEnglishName(self.variant) |
603 | |
604 | + def makeTranslatableMessage(self, potmsgset): |
605 | + """See `IPOFile`.""" |
606 | + return TranslatableMessage(potmsgset, self) |
607 | + |
608 | |
609 | class POFile(SQLBase, POFileMixIn): |
610 | implements(IPOFile) |
= Overview =
This branch is part of the work on the redesign of the +translate page. It adds a new view for a POFile that makes use of the new TranslatableMessage model class.
The view provides a number of basic information about the POFile as described in the doctest. It also provides a list of TranslatableMessage objects that represent the content of the file.
The view is meant to replace the existing POFileView and POFileTranslate View. At the moment it carries a lot of duplicate code from those views but that will go away in the future.
== Implementation notes ==
Part of the implementation is a new method for the POFile model class that creates a TranslatableMessage instance. LauchpadObjectF actory also reveived a new method to easily create a Translator entry as this was needed in the doc test.
The view itself is a collection of methods from existing views, namely POFileView, POFileTranslateView and BaseTranslationView with some adaptions, minor and major. One major adaption are the permission_ statement and translation_ groups_ statement which produce (posibly HTML) strings to be inserted directly in the template. These methods take the meat out of pofile- translate- access. pt which I found quite confusing.
I had to file bug 426745 as a follow-up because I did not want to include the generation of markup for the translation group and translator using a tal formatter - mainly because don't know yet how to do that. But it should not be too hard to do.
== Test command ==
bin/test -vvt pofile-base-views -t pofile_view
== Demo/QA ==
The view is not yet connected to any page so it cannot be demoed or QA'ed.
= Launchpad lint =
Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.
Linting changed files: testing/ factory. py translations/ browser/ pofile. py translations/ browser/ tests/pofile- base-views. txt translations/ browser/ tests/test_ pofile_ view.py translations/ interfaces/ pofile. py translations/ model/pofile. py
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/