61
from canonical.launchpad.webapp.launchpadform import ReturnToReferrerMixin
67
62
from canonical.launchpad.webapp.menu import structured
68
from lp.app.browser.launchpadform import ReturnToReferrerMixin
69
63
from lp.app.browser.tales import DateTimeFormatterAPI
70
from lp.app.enums import (
71
service_uses_launchpad,
64
from canonical.lazr.utils import smartquote
65
from lp.app.enums import service_uses_launchpad
74
66
from lp.app.errors import NotFoundError
75
from lp.app.validators.name import valid_name
76
67
from lp.registry.browser.productseries import ProductSeriesFacets
77
68
from lp.registry.browser.sourcepackage import SourcePackageFacets
78
69
from lp.registry.interfaces.productseries import IProductSeries
79
from lp.registry.interfaces.role import IPersonRoles
80
70
from lp.registry.interfaces.sourcepackage import ISourcePackage
81
from lp.registry.model.packaging import Packaging
82
from lp.registry.model.product import Product
83
from lp.registry.model.productseries import ProductSeries
84
from lp.registry.model.sourcepackagename import SourcePackageName
85
71
from lp.services.worlddata.interfaces.language import ILanguageSet
86
72
from lp.translations.browser.poexportrequest import BaseExportView
87
73
from lp.translations.browser.translations import TranslationsMixin
322
307
if (by_source_count > self.SHOW_RELATED_TEMPLATES):
323
308
other = by_source_count - self.SHOW_RELATED_TEMPLATES
324
309
if (self.context.distroseries):
325
sourcepackage = self.context.distroseries.getSourcePackage(
326
self.context.sourcepackagename)
328
sourcepackage, rootsite="translations",
329
view_name='+translations')
310
# XXX adiroiban 2009-12-21 bug=499058: A canonical_url for
311
# SourcePackageName is needed to avoid hardcoding this URL.
312
url = (canonical_url(
313
self.context.distroseries, rootsite="translations") +
314
"/+source/" + self.context.sourcepackagename.name +
331
317
url = canonical_url(
332
318
self.context.productseries,
555
541
"""View class that lets you edit a POTemplate object."""
557
543
schema = IPOTemplate
544
field_names = ['translation_domain', 'description', 'priority',
545
'path', 'owner', 'iscurrent']
558
546
label = 'Edit translation template details'
559
547
page_title = 'Edit details'
560
548
PRIORITY_MIN_VALUE = 0
561
549
PRIORITY_MAX_VALUE = 100000
564
def field_names(self):
566
'name', 'translation_domain', 'description', 'priority',
568
if self.context.distroseries:
574
field_names.append('owner')
578
def _return_url(self):
579
# We override the ReturnToReferrerMixin _return_url because it might
580
# change when any of the productseries, distroseries,
581
# sourcepackagename or name attributes change, and the basic version
582
# only supports watching changes to a single attribute.
584
# The referer header is a hidden input in the form.
585
referrer = self.request.form.get('_return_url')
586
returnChanged = False
588
# "referer" is misspelled in the HTTP specification.
589
referrer = self.request.getHeader('referer')
590
# If we were looking at the actual template, we want a new
592
if referrer is not None and '/+pots/' in referrer:
595
if (referrer is not None
596
and not returnChanged
597
and referrer.startswith(self.request.getApplicationURL())
598
and referrer != self.request.getHeader('location')):
601
return canonical_url(self.context)
603
551
@action(_('Change'), name='change')
604
552
def change_action(self, action, data):
605
553
context = self.context
620
568
naked_context = removeSecurityProxy(context)
621
569
naked_context.date_last_updated = datetime.datetime.now(pytz.UTC)
623
def _validateTargetAndGetTemplates(self, data):
624
"""Return a POTemplateSubset corresponding to the chosen target."""
625
sourcepackagename = data.get('sourcepackagename',
626
self.context.sourcepackagename)
627
return getUtility(IPOTemplateSet).getSubset(
628
distroseries=self.context.distroseries,
629
sourcepackagename=sourcepackagename,
630
productseries=self.context.productseries)
632
571
def validate(self, data):
633
name = data.get('name', None)
634
if name is None or not valid_name(name):
637
'Template name can only start with lowercase letters a-z '
638
'or digits 0-9, and other than those characters, can only '
639
'contain "-", "+" and "." characters.')
641
distroseries = data.get('distroseries', self.context.distroseries)
642
sourcepackagename = data.get(
643
'sourcepackagename', self.context.sourcepackagename)
644
productseries = data.get('productseries', None)
645
sourcepackage_changed = (
646
distroseries is not None and
647
(distroseries != self.context.distroseries or
648
sourcepackagename != self.context.sourcepackagename))
649
productseries_changed = (productseries is not None and
650
productseries != self.context.productseries)
651
similar_templates = self._validateTargetAndGetTemplates(data)
652
if similar_templates is not None:
654
name, similar_templates, sourcepackage_changed,
655
productseries_changed)
657
data.get('translation_domain'), similar_templates,
658
sourcepackage_changed, productseries_changed)
660
572
priority = data.get('priority')
661
573
if priority is None:
668
580
'The priority value must be between %s and %s.' % (
669
581
self.PRIORITY_MIN_VALUE, self.PRIORITY_MAX_VALUE))
585
def _return_attribute_name(self):
586
"""See 'ReturnToReferrerMixin'."""
590
class POTemplateAdminView(POTemplateEditView):
591
"""View class that lets you admin a POTemplate object."""
593
'name', 'translation_domain', 'description', 'header', 'iscurrent',
594
'owner', 'productseries', 'distroseries', 'sourcepackagename',
595
'from_sourcepackagename', 'sourcepackageversion', 'binarypackagename',
596
'languagepack', 'path', 'source_file_format', 'priority',
598
label = 'Administer translation template'
599
page_title = "Administer"
671
601
def validateName(self, name, similar_templates,
672
602
sourcepackage_changed, productseries_changed):
687
617
def validateDomain(self, domain, similar_templates,
688
618
sourcepackage_changed, productseries_changed):
689
clashes = similar_templates.getPOTemplatesByTranslationDomain(domain)
690
if not clashes.is_empty():
619
other_template = similar_templates.getPOTemplateByTranslationDomain(
621
if other_template is not None:
691
622
if sourcepackage_changed:
692
623
self.setFieldError(
693
624
'sourcepackagename',
701
632
self.setFieldError(
702
633
'translation_domain', "Domain is already in use.")
705
def _return_attribute_name(self):
706
"""See 'ReturnToReferrerMixin'."""
710
class POTemplateAdminView(POTemplateEditView):
711
"""View class that lets you admin a POTemplate object."""
713
'name', 'translation_domain', 'description', 'header', 'iscurrent',
714
'owner', 'productseries', 'distroseries', 'sourcepackagename',
715
'from_sourcepackagename', 'sourcepackageversion', 'binarypackagename',
716
'languagepack', 'path', 'source_file_format', 'priority',
718
label = 'Administer translation template'
719
page_title = "Administer"
721
def _validateTargetAndGetTemplates(self, data):
722
"""Return a POTemplateSubset corresponding to the chosen target."""
635
def validate(self, data):
636
super(POTemplateAdminView, self).validate(data)
723
637
distroseries = data.get('distroseries')
724
638
sourcepackagename = data.get('sourcepackagename')
725
639
productseries = data.get('productseries')
736
650
if message is not None:
737
651
self.addError(message)
739
return getUtility(IPOTemplateSet).getSubset(
654
# Validate name and domain; they should be unique within the
655
# template's translation target (productseries or source
656
# package). Validate against the target selected by the form,
657
# not the template's existing target; the form may change the
658
# template's target as well.
659
similar_templates = getUtility(IPOTemplateSet).getSubset(
740
660
distroseries=distroseries, sourcepackagename=sourcepackagename,
741
661
productseries=productseries)
662
sourcepackage_changed = (
663
distroseries != self.context.distroseries or
664
sourcepackagename != self.context.sourcepackagename)
665
productseries_changed = productseries != self.context.productseries
668
data.get('name'), similar_templates,
669
sourcepackage_changed, productseries_changed)
671
data.get('translation_domain'), similar_templates,
672
sourcepackage_changed, productseries_changed)
744
675
class POTemplateExportView(BaseExportView):
923
854
def initialize(self, series, is_distroseries=True):
924
self._template_name_cache = {}
925
self._packaging_cache = {}
926
855
self.is_distroseries = is_distroseries
927
856
if is_distroseries:
928
857
self.distroseries = series
930
859
self.productseries = series
931
user = IPersonRoles(self.user, None)
932
self.can_admin = (user is not None and
933
(user.in_admin or user.in_rosetta_experts))
860
self.can_admin = check_permission(
861
'launchpad.TranslationsAdmin', series)
934
862
self.can_edit = (
936
check_permission('launchpad.TranslationsAdmin', series))
863
self.can_admin or check_permission('launchpad.Edit', series))
938
865
self.user_is_logged_in = (self.user is not None)
941
# If this is not a distroseries, then the query is much simpler.
942
if not self.is_distroseries:
943
potemplateset = getUtility(IPOTemplateSet)
944
# The "shape" of the data returned by POTemplateSubset isn't quite
945
# right so we have to run it through zip first.
946
return zip(potemplateset.getSubset(
947
productseries=self.productseries,
948
distroseries=self.distroseries,
949
ordered_by_names=True))
951
# Otherwise we have to do more work, primarily for the "sharing"
953
OtherTemplate = ClassAlias(POTemplate)
954
join = (self.context.getTemplatesCollection()
955
.joinOuter(Packaging, And(
956
Packaging.distroseries == self.context.id,
957
Packaging.sourcepackagename ==
958
POTemplate.sourcepackagenameID))
959
.joinOuter(ProductSeries,
960
ProductSeries.id == Packaging.productseriesID)
961
.joinOuter(Product, And(
962
Product.id == ProductSeries.productID,
964
Product.translations_usage == ServiceUsage.LAUNCHPAD,
965
Product.translations_usage == ServiceUsage.EXTERNAL)))
966
.joinOuter(OtherTemplate, And(
967
OtherTemplate.productseriesID == ProductSeries.id,
968
OtherTemplate.name == POTemplate.name))
969
.joinInner(SourcePackageName,
970
SourcePackageName.id == POTemplate.sourcepackagenameID))
972
return join.select(POTemplate, Packaging, ProductSeries, Product,
973
OtherTemplate, SourcePackageName)
867
def iter_templates(self):
868
potemplateset = getUtility(IPOTemplateSet)
869
return potemplateset.getSubset(
870
productseries=self.productseries,
871
distroseries=self.distroseries,
872
ordered_by_names=True)
975
874
def rowCSSClass(self, template):
976
875
if template.iscurrent:
997
896
text += ' (inactive)'
1000
def _renderSharing(self, template, packaging, productseries, upstream,
1001
other_template, sourcepackagename):
1002
"""Render a link to `template`.
1004
:param template: The target `POTemplate`.
1005
:return: HTML for the "sharing" status of `template`.
1007
# Testing is easier if we are willing to extract the sourcepackagename
1008
# from the template.
1009
if sourcepackagename is None:
1010
sourcepackagename = template.sourcepackagename
1011
# Build the edit link.
1012
escaped_source = cgi.escape(sourcepackagename.name)
1013
source_url = '+source/%s' % escaped_source
1014
details_url = source_url + '/+sharing-details'
1015
edit_link = '<a class="sprite edit" href="%s"></a>' % details_url
1017
# If all the conditions are met for sharing...
1018
if packaging and upstream and other_template is not None:
1019
escaped_series = cgi.escape(productseries.name)
1020
escaped_template = cgi.escape(template.name)
1021
pot_url = ('/%s/%s/+pots/%s' %
1022
(escaped_source, escaped_series, escaped_template))
1023
return (edit_link + '<a href="%s">%s/%s</a>'
1024
% (pot_url, escaped_source, escaped_series))
1026
# Otherwise just say that the template isn't shared and give them
1027
# a link to change the sharing.
1028
return edit_link + 'not shared'
1030
899
def _renderLastUpdateDate(self, template):
1031
900
"""Render a template's "last updated" column."""
1032
901
formatter = DateTimeFormatterAPI(template.date_last_updated)
1122
990
('lastupdate_column', "Updated"),
1123
991
('actions_column', actions_header),
1126
if self.is_distroseries:
1127
columns[3:3] = [('sharing', "Shared with")]
1129
993
return '\n'.join([
1130
994
self._renderField(css, text, tag='th')
1131
995
for (css, text) in columns])
1133
def renderTemplateRow(self, template, packaging=None, productseries=None,
1134
upstream=None, other_template=None, sourcepackagename=None):
997
def renderTemplateRow(self, template):
1135
998
"""Render HTML for an entire template row."""
1136
999
if not self.can_edit and not template.iscurrent:
1148
1011
('actions_column', self._renderActionsColumn(template, base_url)),
1151
if self.is_distroseries:
1153
'sharing', self._renderSharing(template, packaging,
1154
productseries, upstream, other_template,
1158
1014
tds = [self._renderField(*field) for field in fields]
1160
1016
css = self.rowCSSClass(template)