Merge lp:~henninge/launchpad/bug-425645 into lp:launchpad

Proposed by Henning Eggers
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
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Review via email: mp+11430@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Henning Eggers (henninge) wrote :

= 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 POFileTranslateView. 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. LauchpadObjectFactory 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:
  lib/lp/testing/factory.py
  lib/lp/translations/browser/pofile.py
  lib/lp/translations/browser/tests/pofile-base-views.txt
  lib/lp/translations/browser/tests/test_pofile_view.py
  lib/lp/translations/interfaces/pofile.py
  lib/lp/translations/model/pofile.py

Revision history for this message
Gavin Panella (allenap) wrote :
Download full text (26.2 KiB)

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/testing/factory.py'
> --- lib/lp/testing/factory.py 2009-09-04 12:17:11 +0000
> +++ lib/lp/testing/factory.py 2009-09-09 15:11:35 +0000
> @@ -65,6 +65,7 @@
> ISpecificationSet, SpecificationDefinitionStatus)
> from lp.translations.interfaces.translationgroup import (
> ITranslationGroupSet)
> +from lp.translations.interfaces.translator import ITranslatorSet
> from canonical.launchpad.ftests._sqlobject import syncUpdate
> from lp.services.mail.signedmessage import SignedMessage
> from canonical.launchpad.webapp.dbpolicy import MasterDatabasePolicy
> @@ -104,6 +105,7 @@
> from lp.registry.interfaces.sourcepackagename import (
> ISourcePackageNameSet)
> from lp.registry.interfaces.ssh import ISSHKeySet, SSHKeyType
> +from lp.services.worlddata.interfaces.language import ILanguageSet
> from lp.soyuz.interfaces.component import IComponentSet
> from lp.soyuz.interfaces.packageset import IPackagesetSet
> from lp.testing import run_with_login, time_counter
> @@ -485,6 +487,15 @@
> return getUtility(ITranslationGroupSet).new(
> name, title, summary, url, owner)
>
> + def makeTranslator(self, language_code, group=None, person=None):
> + """Create a new, arbitrary `Translator`."""
> + language = getUtility(ILanguageSet).getLanguageByCode(language_code)
> + if group is None:
> + group = self.makeTranslationGroup()
> + if person is None:
> + person = self.makePerson()
> + return getUtility(ITranslatorSet).new(group, language, person)
> +
> 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 = removeSecurityProxy(branch)
>
> === modified file 'lib/lp/translations/browser/pofile.py'
> --- lib/lp/translations/browser/pofile.py 2009-09-02 07:13:11 +0000
> +++ lib/lp/translations/browser/pofile.py 2009-09-09 15:11:35 +0000
> @@ -49,6 +49,8 @@
> from canonical.launchpad.webapp.batching import BatchNavigator
> from canonical.launchpad.webapp.menu import structured
>
> +from canonical.launchpad import _
> +
>
> class CustomDropdownWidget(DropdownWidget):
> def _div(self, cssClass, contents, **kw):
> @@ -134,6 +136,261 @@
> links = ('description', 'translate', 'upload', 'download')
>
>
> +class POFileBaseView(Lau...

review: Needs Fixing (code)
Revision history for this message
Henning Eggers (henninge) wrote :
Download full text (13.0 KiB)

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/testing/factory.py'
[...]
>> === modified file 'lib/lp/translations/browser/pofile.py'
>> --- lib/lp/translations/browser/pofile.py 2009-09-02 07:13:11 +0000
>> +++ lib/lp/translations/browser/pofile.py 2009-09-09 15:11:35 +0000
>> @@ -49,6 +49,8 @@
>> from canonical.launchpad.webapp.batching import BatchNavigator
>> from canonical.launchpad.webapp.menu import structured
>>
>> +from canonical.launchpad import _
>> +
>>
>> class CustomDropdownWidget(DropdownWidget):
>> def _div(self, cssClass, contents, **kw):
>> @@ -134,6 +136,261 @@
>> links = ('description', 'translate', 'upload', 'download')
>>
>>
>> +class POFileBaseView(LaunchpadView):
>> + """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(POFileBaseView, self).initialize()
>> +
>> + self._initializeShowOption()
>> +
>> + self.batchnav = self._buildBatchNavigator()
>> + # 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.currentBatch().size
>
> 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.context.contributors)
>
> Is this premature optimisation? In any case, consider using an
> immutable type like a tuple or frozenset for a cache...

Revision history for this message
Henning Eggers (henninge) wrote :
Download full text (8.2 KiB)

=== modified file 'lib/lp/translations/browser/pofile.py'
--- lib/lp/translations/browser/pofile.py 2009-09-09 11:04:58 +0000
+++ lib/lp/translations/browser/pofile.py 2009-09-09 17:16:12 +0000
@@ -151,16 +151,11 @@
         self._initializeShowOption()

         self.batchnav = self._buildBatchNavigator()
- # 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.currentBatch().size

     @cachedproperty
     def contributors(self):
- return list(self.context.contributors)
+ return tuple(self.context.contributors)

     @property
     def user_can_edit(self):
@@ -180,31 +175,21 @@
         """

         if self.user_can_edit:
- statement = _("You have full access to this translation.")
- else:
- if self.user_can_suggest:
- 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_translationgroup:
- 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_can_suggest:
+ 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_translationgroup:
+ 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_groups_statement(self):
@@ -225,6 +210,8 @@
                 else:
                     groups.appen...

Read more...

Revision history for this message
Gavin Panella (allenap) wrote :
Download full text (14.5 KiB)

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/testing/factory.py'
> [...]
> >> === modified file 'lib/lp/translations/browser/pofile.py'
> >> --- lib/lp/translations/browser/pofile.py 2009-09-02 07:13:11 +0000
> >> +++ lib/lp/translations/browser/pofile.py 2009-09-09 15:11:35 +0000
> >> @@ -49,6 +49,8 @@
> >> from canonical.launchpad.webapp.batching import BatchNavigator
> >> from canonical.launchpad.webapp.menu import structured
> >>
> >> +from canonical.launchpad import _
> >> +
> >>
> >> class CustomDropdownWidget(DropdownWidget):
> >> def _div(self, cssClass, contents, **kw):
> >> @@ -134,6 +136,261 @@
> >> links = ('description', 'translate', 'upload', 'download')
> >>
> >>
> >> +class POFileBaseView(LaunchpadView):
> >> + """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(POFileBaseView, self).initialize()
> >> +
> >> + self._initializeShowOption()
> >> +
> >> + self.batchnav = self._buildBatchNavigator()
> >> + # 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.currentBatch().size
> >
> > Consider giving them mor...

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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)