~launchpad-pqm/launchpad/devel

« back to all changes in this revision

Viewing changes to lib/lp/translations/browser/potemplate.py

  • Committer: Julian Edwards
  • Date: 2011-07-28 20:46:18 UTC
  • mfrom: (13553 devel)
  • mto: This revision was merged to the branch mainline in revision 13555.
  • Revision ID: julian.edwards@canonical.com-20110728204618-tivj2wx2oa9s32bx
merge trunk

Show diffs side-by-side

added added

removed removed

Lines of Context:
30
30
import operator
31
31
import os.path
32
32
 
33
 
from lazr.restful.utils import smartquote
34
33
import pytz
35
 
from storm.expr import (
36
 
    And,
37
 
    Or,
38
 
    )
39
 
from storm.info import ClassAlias
40
34
from zope.component import getUtility
41
35
from zope.interface import implements
42
36
from zope.publisher.browser import FileUpload
64
58
    ICanonicalUrlData,
65
59
    ILaunchBag,
66
60
    )
 
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,
72
 
    ServiceUsage,
73
 
    )
 
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
101
87
from lp.translations.interfaces.translationimportqueue import (
102
88
    ITranslationImportQueue,
103
89
    )
104
 
from lp.translations.model.potemplate import POTemplate
105
90
 
106
91
 
107
92
class POTemplateNavigation(Navigation):
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)
327
 
                url = canonical_url(
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 +
 
315
                    "/+translations")
330
316
            else:
331
317
                url = canonical_url(
332
318
                    self.context.productseries,
555
541
    """View class that lets you edit a POTemplate object."""
556
542
 
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
562
550
 
563
 
    @property
564
 
    def field_names(self):
565
 
        field_names = [
566
 
            'name', 'translation_domain', 'description', 'priority',
567
 
            'path', 'iscurrent']
568
 
        if self.context.distroseries:
569
 
            field_names.extend([
570
 
                'sourcepackagename',
571
 
                'languagepack',
572
 
                ])
573
 
        else:
574
 
            field_names.append('owner')
575
 
        return field_names
576
 
 
577
 
    @property
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.
583
 
 
584
 
        # The referer header is a hidden input in the form.
585
 
        referrer = self.request.form.get('_return_url')
586
 
        returnChanged = False
587
 
        if referrer is None:
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
591
 
            # URL constructed.
592
 
            if referrer is not None and '/+pots/' in referrer:
593
 
                returnChanged = True
594
 
 
595
 
        if (referrer is not None
596
 
            and not returnChanged
597
 
            and referrer.startswith(self.request.getApplicationURL())
598
 
            and referrer != self.request.getHeader('location')):
599
 
            return referrer
600
 
        else:
601
 
            return canonical_url(self.context)
602
 
 
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)
622
570
 
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)
631
 
 
632
571
    def validate(self, data):
633
 
        name = data.get('name', None)
634
 
        if name is None or not valid_name(name):
635
 
            self.setFieldError(
636
 
                '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.')
640
 
 
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:
653
 
            self.validateName(
654
 
                name, similar_templates, sourcepackage_changed,
655
 
                productseries_changed)
656
 
            self.validateDomain(
657
 
                data.get('translation_domain'), similar_templates,
658
 
                sourcepackage_changed, productseries_changed)
659
 
 
660
572
        priority = data.get('priority')
661
573
        if priority is None:
662
574
            return
667
579
                'priority',
668
580
                'The priority value must be between %s and %s.' % (
669
581
                self.PRIORITY_MIN_VALUE, self.PRIORITY_MAX_VALUE))
 
582
            return
 
583
 
 
584
    @property
 
585
    def _return_attribute_name(self):
 
586
        """See 'ReturnToReferrerMixin'."""
 
587
        return "name"
 
588
 
 
589
 
 
590
class POTemplateAdminView(POTemplateEditView):
 
591
    """View class that lets you admin a POTemplate object."""
 
592
    field_names = [
 
593
        'name', 'translation_domain', 'description', 'header', 'iscurrent',
 
594
        'owner', 'productseries', 'distroseries', 'sourcepackagename',
 
595
        'from_sourcepackagename', 'sourcepackageversion', 'binarypackagename',
 
596
        'languagepack', 'path', 'source_file_format', 'priority',
 
597
        'date_last_updated']
 
598
    label = 'Administer translation template'
 
599
    page_title = "Administer"
670
600
 
671
601
    def validateName(self, name, similar_templates,
672
602
                     sourcepackage_changed, productseries_changed):
686
616
 
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(
 
620
            domain)
 
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.")
703
634
 
704
 
    @property
705
 
    def _return_attribute_name(self):
706
 
        """See 'ReturnToReferrerMixin'."""
707
 
        return "name"
708
 
 
709
 
 
710
 
class POTemplateAdminView(POTemplateEditView):
711
 
    """View class that lets you admin a POTemplate object."""
712
 
    field_names = [
713
 
        'name', 'translation_domain', 'description', 'header', 'iscurrent',
714
 
        'owner', 'productseries', 'distroseries', 'sourcepackagename',
715
 
        'from_sourcepackagename', 'sourcepackageversion', 'binarypackagename',
716
 
        'languagepack', 'path', 'source_file_format', 'priority',
717
 
        'date_last_updated']
718
 
    label = 'Administer translation template'
719
 
    page_title = "Administer"
720
 
 
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')
735
649
 
736
650
        if message is not None:
737
651
            self.addError(message)
738
 
            return None
739
 
        return getUtility(IPOTemplateSet).getSubset(
 
652
            return
 
653
 
 
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
 
666
 
 
667
        self.validateName(
 
668
            data.get('name'), similar_templates,
 
669
            sourcepackage_changed, productseries_changed)
 
670
        self.validateDomain(
 
671
            data.get('translation_domain'), similar_templates,
 
672
            sourcepackage_changed, productseries_changed)
742
673
 
743
674
 
744
675
class POTemplateExportView(BaseExportView):
894
825
           potemplate.iscurrent):
895
826
            # This template is available for translation.
896
827
            return potemplate
897
 
        elif check_permission('launchpad.TranslationsAdmin', potemplate):
 
828
        elif check_permission('launchpad.Edit', potemplate):
898
829
            # User has Edit privileges for this template and can access it.
899
830
            return potemplate
900
831
        else:
921
852
    can_admin = None
922
853
 
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
929
858
        else:
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 = (
935
 
            self.can_admin or
936
 
            check_permission('launchpad.TranslationsAdmin', series))
 
863
            self.can_admin or check_permission('launchpad.Edit', series))
937
864
 
938
865
        self.user_is_logged_in = (self.user is not None)
939
866
 
940
 
    def iter_data(self):
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))
950
 
 
951
 
        # Otherwise we have to do more work, primarily for the "sharing"
952
 
        # column.
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,
963
 
                Or(
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))
971
 
 
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)
974
873
 
975
874
    def rowCSSClass(self, template):
976
875
        if template.iscurrent:
997
896
            text += ' (inactive)'
998
897
        return text
999
898
 
1000
 
    def _renderSharing(self, template, packaging, productseries, upstream,
1001
 
            other_template, sourcepackagename):
1002
 
        """Render a link to `template`.
1003
 
 
1004
 
        :param template: The target `POTemplate`.
1005
 
        :return: HTML for the "sharing" status of `template`.
1006
 
        """
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
1016
 
 
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))
1025
 
        else:
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'
1029
 
 
1030
899
    def _renderLastUpdateDate(self, template):
1031
900
        """Render a template's "last updated" column."""
1032
901
        formatter = DateTimeFormatterAPI(template.date_last_updated)
1113
982
            actions_header = "Actions"
1114
983
        else:
1115
984
            actions_header = None
1116
 
 
1117
985
        columns = [
1118
986
            ('priority_column', "Priority"),
1119
987
            ('sourcepackage_column', sourcepackage_header),
1122
990
            ('lastupdate_column', "Updated"),
1123
991
            ('actions_column', actions_header),
1124
992
            ]
1125
 
 
1126
 
        if self.is_distroseries:
1127
 
            columns[3:3] = [('sharing', "Shared with")]
1128
 
 
1129
993
        return '\n'.join([
1130
994
            self._renderField(css, text, tag='th')
1131
995
            for (css, text) in columns])
1132
996
 
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:
1137
1000
            return ""
1148
1011
            ('actions_column', self._renderActionsColumn(template, base_url)),
1149
1012
        ]
1150
1013
 
1151
 
        if self.is_distroseries:
1152
 
            fields[3:3] = [(
1153
 
                'sharing', self._renderSharing(template, packaging,
1154
 
                    productseries, upstream, other_template,
1155
 
                    sourcepackagename)
1156
 
                )]
1157
 
 
1158
1014
        tds = [self._renderField(*field) for field in fields]
1159
1015
 
1160
1016
        css = self.rowCSSClass(template)