← Back to team overview

launchpad-reviewers team mailing list archive

Re: [Merge] ~ilasc/launchpad:oci-recipe-push-rules-edit into launchpad:master

 

Added some inline comments in the code. Really good set of tests! Thank you!

Diff comments:

> diff --git a/lib/lp/oci/browser/ocirecipe.py b/lib/lp/oci/browser/ocirecipe.py
> index 356ac1c..104b06c 100644
> --- a/lib/lp/oci/browser/ocirecipe.py
> +++ b/lib/lp/oci/browser/ocirecipe.py
> @@ -138,12 +162,18 @@ class OCIRecipeContextMenu(ContextMenu):
>  
>      facet = 'overview'
>  
> -    links = ('request_builds',)
> +    links = ('request_builds', 'edit_push_rules')
>  
>      @enabled_with_permission('launchpad.Edit')
>      def request_builds(self):
>          return Link('+request-builds', 'Request builds', icon='add')
>  
> +    @enabled_with_permission('launchpad.Edit')
> +    def edit_push_rules(self):
> +        return Link('+edit-push-rules',
> +                    'Edit push rules',
> +                    icon='edit')

I think a more common style pattern would be:

```
return Link(
    '+edit-push-rules', 'Edit push rules', icon='edit')
```

> +
>  
>  class OCIProjectRecipesView(LaunchpadView):
>      """Default view for the list of OCI recipes of an OCI project."""
> @@ -243,6 +273,615 @@ def new_builds_notification_text(builds, already_pending=None):
>          return builds_text
>  
>  
> +class OCIRecipeEditCredentialsView(LaunchpadFormView):
> +    """View for +ocirecipe-edit-credentials.pt."""
> +
> +    @cachedproperty
> +    def oci_registry_credentials(self):
> +        return list(getUtility(
> +            IOCIRegistryCredentialsSet).findByOwner(self.context.owner))
> +
> +    schema = Interface
> +
> +    def _getFieldName(self, name, credentials_id):
> +        """Get the combined field name for an `OCIRegistryCredentials` ID.
> +
> +        In order to be able to render a table, we encode the credentials ID
> +        in the form field name.
> +        """
> +        return "%s.%d" % (name, credentials_id)
> +
> +    def getEditFieldsRow(self, credentials=None):
> +        id = getattr(credentials, 'id', None)
> +        owner = Choice(
> +            title=u'Owner',
> +            vocabulary=(
> +                'AllUserTeamsParticipationPlusSelfSimpleDisplay'),
> +            default=credentials.owner.name,
> +            __name__=self._getFieldName('owner', id))
> +
> +        username = TextLine(
> +            title=u'Username',
> +            __name__=self._getFieldName('username', id),
> +            default=credentials.username,
> +            required=False, readonly=False)
> +
> +        password = Password(
> +            title=u'Password',
> +            __name__=self._getFieldName('password', id),
> +            default=None,
> +            required=False, readonly=False)
> +
> +        confirm_password = Password(
> +            title=u'Confirm password',
> +            __name__=self._getFieldName('confirm_password', id),
> +            default=None,
> +            required=False, readonly=False)
> +
> +        url = TextLine(
> +            title=u'Registry URL',
> +            __name__=self._getFieldName('url', id),
> +            default=credentials.url,
> +            required=True, readonly=False)
> +
> +        delete = Bool(
> +            title=u'Delete',
> +            __name__=self._getFieldName('delete', id),
> +            default=False,
> +            required=True, readonly=False)
> +
> +        return owner, username, password, confirm_password, url, delete
> +
> +    def getAddFieldsRow(self):
> +        add_url = TextLine(
> +            title=u'Registry URL',
> +            __name__=u'add_url',
> +            required=False, readonly=False)
> +        add_username = TextLine(
> +            title=u'Username',
> +            __name__=u'add_username',
> +            required=False, readonly=False)
> +        add_password = Password(
> +            title=u'Password',
> +            __name__=u'add_password',
> +            required=False, readonly=False)
> +        add_confirm_password = Password(
> +            title=u'Confirm password',
> +            __name__=u'add_confirm_password',
> +            required=False, readonly=False)
> +
> +        return add_url, add_username, add_password, add_confirm_password
> +
> +    def _parseFieldName(self, field_name):
> +        """Parse a combined field name as described in `_getFieldName`.
> +
> +        :raises UnexpectedFormData: if the field name cannot be parsed or
> +            the `OCIRegistryCredentials` cannot be found.
> +        """
> +        field_bits = field_name.split(".")
> +        if len(field_bits) != 2:
> +            raise UnexpectedFormData(
> +                "Cannot parse field name: %s" % field_name)
> +        field_type = field_bits[0]
> +        try:
> +            credentials_id = int(field_bits[1])
> +        except ValueError:
> +            raise UnexpectedFormData(
> +                "Cannot parse field name: %s" % field_name)
> +        return field_type, credentials_id
> +
> +    def setUpFields(self):
> +        """See `LaunchpadFormView`."""
> +        LaunchpadFormView.setUpFields(self)
> +
> +        for elem in self.oci_registry_credentials:
> +            fields = self.getEditFieldsRow(elem)
> +            self.form_fields += FormFields(*fields)
> +
> +        add_fields = self.getAddFieldsRow()
> +        self.form_fields += FormFields(*add_fields)
> +
> +    @property
> +    def label(self):
> +        return 'Edit OCI registry credentials'
> +
> +    @property
> +    def cancel_url(self):
> +        return canonical_url(self.context)
> +
> +    def getCredentialsWidgets(self, credentials):
> +        widgets_by_name = {widget.name: widget for widget in self.widgets}
> +        owner_field_name = (
> +                "field." + self._getFieldName("owner", credentials.id))
> +        username_field_name = (
> +                "field." + self._getFieldName("username", credentials.id))
> +        password_field_name = (
> +                "field." + self._getFieldName("password", credentials.id))
> +        confirm_password_field_name = (
> +                "field." + self._getFieldName("confirm_password",
> +                                              credentials.id))
> +        url_field_name = "field." + self._getFieldName("url", credentials.id)
> +        delete_field_name = (
> +                "field." + self._getFieldName("delete", credentials.id))
> +        return {
> +            "owner": widgets_by_name[owner_field_name],
> +            "username": widgets_by_name[username_field_name],
> +            "password": widgets_by_name[password_field_name],
> +            "confirm_password": widgets_by_name[confirm_password_field_name],
> +            "url": widgets_by_name[url_field_name],
> +            "delete": widgets_by_name[delete_field_name]
> +        }
> +
> +    def parseData(self, data):
> +        """Rearrange form data to make it easier to process."""
> +        parsed_data = {}
> +        add_url = data["add_url"]
> +        add_username = data["add_username"]
> +        add_password = data["add_password"]
> +        add_confirm_password = data["add_confirm_password"]
> +        if add_url or add_username or add_password or add_confirm_password:
> +            parsed_data.setdefault(None, {
> +                "username": add_username,
> +                "password": add_password,
> +                "confirm_password": add_confirm_password,
> +                "url": add_url,
> +                "action": "add",
> +            })
> +        for field_name in (
> +                name for name in data if name.split(".")[0] == "owner"):
> +            _, credentials_id = self._parseFieldName(field_name)
> +            owner_field_name = self._getFieldName(
> +                "owner", credentials_id)
> +            username_field_name = self._getFieldName(
> +                "username", credentials_id)
> +            password_field_name = self._getFieldName(
> +                "password", credentials_id)
> +            confirm_password_field_name = self._getFieldName(
> +                "confirm_password", credentials_id)
> +            url_field_name = self._getFieldName("url", credentials_id)
> +            delete_field_name = self._getFieldName("delete", credentials_id)
> +            if data.get(delete_field_name):
> +                action = "delete"
> +            else:
> +                action = "change"
> +            parsed_data.setdefault(credentials_id, {
> +                "username": data.get(username_field_name),
> +                "password": data.get(password_field_name),
> +                "confirm_password": data.get(confirm_password_field_name),
> +                "url": data.get(url_field_name),
> +                "owner": data.get(owner_field_name),
> +                "action": action,
> +            })
> +
> +        return parsed_data
> +
> +    def changeCredentials(self, parsed_credentials, credentials):
> +        username = parsed_credentials["username"]
> +        password = parsed_credentials["password"]
> +        confirm_password = parsed_credentials["confirm_password"]
> +        owner = parsed_credentials["owner"]
> +        if password or confirm_password:
> +            if password != confirm_password:
> +                self.setFieldError(
> +                    self._getFieldName(
> +                        "confirm_password", credentials.id),
> +                    "Passwords do not match.")
> +            else:
> +                credentials.setCredentials(
> +                    {"username": username,
> +                     "password": password})
> +                credentials.url = parsed_credentials["url"]
> +        elif username != credentials.username:
> +            removeSecurityProxy(credentials).username = username
> +            credentials.url = parsed_credentials["url"]
> +        elif parsed_credentials["url"] != credentials.url:
> +            credentials.url = parsed_credentials["url"]
> +        if owner != credentials.owner:
> +            credentials.owner = owner
> +
> +    def deleteCredentials(self, credentials):
> +        push_rule_set = getUtility(IOCIPushRuleSet)
> +        if not push_rule_set.findByRegistryCredentials(
> +                credentials).is_empty():
> +            self.setFieldError(
> +                self._getFieldName(
> +                    "delete", credentials.id),
> +                "These credentials cannot be deleted as there are "
> +                "push rules defined that still use them.")
> +        else:
> +            credentials.destroySelf()
> +
> +    def addCredentials(self, parsed_add_credentials):
> +        url = parsed_add_credentials["url"]
> +        password = parsed_add_credentials["password"]
> +        confirm_password = parsed_add_credentials["confirm_password"]
> +        username = parsed_add_credentials["username"]
> +        if url:
> +            if password or confirm_password:
> +                if not password == confirm_password:
> +                    self.setFieldError(
> +                        "add_password",
> +                        "Please make sure the new "
> +                        "password matches the "
> +                        "confirm password field.")
> +                    return
> +
> +                credentials = {
> +                    'username': username,
> +                    'password': password}
> +                try:
> +                    getUtility(IOCIRegistryCredentialsSet).new(
> +                        owner=self.context.owner,
> +                        url=url,
> +                        credentials=credentials)
> +                except OCIRegistryCredentialsAlreadyExist:
> +                    self.setFieldError(
> +                        "add_url",
> +                        "Credentials already exist "
> +                        "with the same URL and "
> +                        "username.")
> +            else:
> +                credentials = {'username': username}
> +                try:

This try/except block is a duplication of the block above.

Maybe just:

```
if not url:
    return
if password or confirm password:
    if not password == confirm_password:
        self.setFieldError(...)
        return
    credentials = {username, password}
else:
    credentials = {username}

try:
    getUtility(...).new(...)
except:
    self.setFieldError(...)
```

> +                    getUtility(IOCIRegistryCredentialsSet).new(
> +                        owner=self.context.owner,
> +                        url=url,
> +                        credentials=credentials)
> +                except OCIRegistryCredentialsAlreadyExist:
> +                    self.setFieldError(
> +                        "add_url",
> +                        "Credentials already exist "
> +                        "with the same URL and username.")
> +        else:
> +            self.setFieldError(
> +                "add_url",
> +                "Registry URL cannot be empty.")
> +
> +    def updateCredentialsFromData(self, parsed_data):
> +        credentials_map = {
> +            credentials.id: credentials
> +            for credentials in self.oci_registry_credentials}
> +
> +        for credentials_id, parsed_credentials in parsed_data.items():
> +            credentials = credentials_map.get(credentials_id)
> +            action = parsed_credentials["action"]
> +
> +            if action == "change":
> +                self.changeCredentials(parsed_credentials, credentials)
> +            elif action == "delete":
> +                self.deleteCredentials(credentials)
> +            elif action == "add":
> +                parsed_add_credentials = parsed_data[credentials]
> +                self.addCredentials(parsed_add_credentials)
> +            else:
> +                raise AssertionError("unknown action: %s" % action)
> +
> +    @action("Save")
> +    def save(self, action, data):
> +        parsed_data = self.parseData(data)
> +        self.updateCredentialsFromData(parsed_data)
> +
> +        if not self.errors:
> +            self.request.response.addNotification("Saved credentials")
> +            self.next_url = canonical_url(self.context)
> +
> +
> +class OCIRecipeEditPushRulesView(LaunchpadFormView):
> +    """View for +ocirecipe-edit-push-rules.pt."""
> +
> +    class schema(Interface):
> +        """Schema for editing push rules."""
> +
> +    @cachedproperty
> +    def push_rules(self):
> +        return list(
> +            getUtility(IOCIPushRuleSet).findByRecipe(self.context))
> +
> +    @property
> +    def has_push_rules(self):
> +        return len(self.push_rules) > 0
> +
> +    def _getFieldName(self, name, rule_id):
> +        """Get the combined field name for an `OCIPushRule` ID.
> +
> +        In order to be able to render a table, we encode the rule ID
> +        in the form field name.
> +        """
> +        return "%s.%d" % (name, rule_id)
> +
> +    def _parseFieldName(self, field_name):
> +        """Parse a combined field name as described in `_getFieldName`.
> +
> +        :raises UnexpectedFormData: if the field name cannot be parsed or
> +            the `OCIPushRule` cannot be found.
> +        """
> +        field_bits = field_name.split(".")
> +        if len(field_bits) != 2:
> +            raise UnexpectedFormData(
> +                "Cannot parse field name: %s" % field_name)
> +        field_type = field_bits[0]
> +        try:
> +            rule_id = int(field_bits[1])
> +        except ValueError:
> +            raise UnexpectedFormData(
> +                "Cannot parse field name: %s" % field_name)
> +        return field_type, rule_id
> +
> +    def setUpWidgets(self):
> +        LaunchpadFormView.setUpWidgets(self)
> +
> +    def setUpFields(self):
> +        """See `LaunchpadFormView`."""
> +        LaunchpadFormView.setUpFields(self)
> +        image_fields = []
> +        checkbox_fields = []
> +        username_fields = []
> +        password_fields = []
> +        url_fields = []
> +        delete_fields = []
> +        existing_credentials = []
> +        for elem in list(self.context.push_rules):
> +            image_fields.append(
> +                TextLine(
> +                    title=u'Image name',
> +                    __name__=self._getFieldName('image_name', elem.id),
> +                    default=elem.image_name,
> +                    required=True, readonly=False))
> +            delete_fields.append(
> +                Bool(
> +                    title=u'Delete',
> +                    __name__=self._getFieldName('delete', elem.id),
> +                    default=False,
> +                    required=True, readonly=False))
> +        url_fields.append(
> +            TextLine(
> +                title=u'Registry URL',
> +                __name__=u'add_url',
> +                required=False, readonly=False))
> +        image_fields.append(
> +            TextLine(
> +                title=u'Image name',
> +                __name__=u'add_image_name',
> +                required=False, readonly=False))
> +        username_fields.append(
> +            TextLine(
> +                title=u'Username',
> +                __name__=u'add_username',
> +                required=False, readonly=False))
> +        password_fields.append(
> +            Password(
> +                title=u'Password',
> +                __name__=u'add_password',
> +                required=False, readonly=False))
> +        password_fields.append(
> +            Password(
> +                title=u'Confirm password',
> +                __name__=u'add_confirm_password',
> +                required=False, readonly=False))
> +        existing_credentials.append(
> +            Choice(
> +                vocabulary='OCIRegistryCredentials',
> +                title='Choose credentials',
> +                required=False,
> +                __name__=u'existing_credentials'))
> +        checkbox_fields.append(
> +            Bool(
> +                    title=u'Add new credentials',
> +                    __name__=u'add_new_credentials',
> +                    default=False,
> +                    readonly=False))
> +        checkbox_fields.append(
> +            Bool(
> +                    title=u'Use existing credentials',
> +                    __name__=u'use_existing_credentials',
> +                    default=False,
> +                    readonly=False))
> +        self.form_fields += FormFields(*image_fields)
> +        self.form_fields += FormFields(*delete_fields)
> +        self.form_fields += FormFields(*url_fields)
> +        self.form_fields += FormFields(*checkbox_fields)
> +        self.form_fields += FormFields(*username_fields)
> +        self.form_fields += FormFields(*password_fields)
> +        self.form_fields += FormFields(*existing_credentials)
> +
> +    custom_widget_use_existing_credentials = CustomWidgetFactory(
> +        CheckBoxWidget)
> +    custom_widget_existing_credentials = CustomWidgetFactory(
> +        LaunchpadRadioWidget,
> +        widget_class='field subordinate')
> +
> +    custom_widget_add_new_credentials = CustomWidgetFactory(
> +        CheckBoxWidget)
> +    custom_widget_add_url = CustomWidgetFactory(
> +        TextWidget,
> +        widget_class='field subordinate')
> +    custom_widget_add_username = CustomWidgetFactory(
> +        TextWidget,
> +        widget_class='field subordinate')
> +    custom_widget_add_password = CustomWidgetFactory(
> +        PasswordWidget,
> +        widget_class='field subordinate')
> +    custom_widget_add_confirm_password = CustomWidgetFactory(
> +        PasswordWidget,
> +        widget_class='field subordinate')
> +
> +    @property
> +    def label(self):
> +        return 'Edit OCI push rules for %s' % self.context.name
> +
> +    page_title = 'Edit OCI push rules'
> +
> +    @property
> +    def cancel_url(self):
> +        return canonical_url(self.context)
> +
> +    def getRulesWidgets(self, rule):
> +        widgets_by_name = {widget.name: widget for widget in self.widgets}
> +        image_field_name = (
> +                "field." + self._getFieldName("image_name", rule.id))
> +        delete_field_name = (
> +                "field." + self._getFieldName("delete", rule.id))
> +        return {
> +            "image_name": widgets_by_name[image_field_name],
> +            "delete": widgets_by_name[delete_field_name],
> +        }
> +
> +    def parseData(self, data):
> +        """Rearrange form data to make it easier to process."""
> +        parsed_data = {}
> +        add_image_name = data["add_image_name"]
> +        add_url = data["add_url"]
> +        add_username = data["add_username"]
> +        add_password = data["add_password"]
> +        add_confirm_password = data["add_confirm_password"]
> +        add_existing_credentials = data["existing_credentials"]
> +
> +        # parse data from the Add new rule section of the form
> +        if (add_url or add_username or add_password or
> +                add_confirm_password or add_image_name or
> +                add_existing_credentials):
> +            parsed_data.setdefault(None, {
> +                "image_name": add_image_name,
> +                "url": add_url,
> +                "username": add_username,
> +                "password": add_password,
> +                "confirm_password": add_confirm_password,
> +                "existing_credentials": data["existing_credentials"],
> +                "add_new_credentials": data["add_new_credentials"],
> +                "use_existing_credentials": data["use_existing_credentials"],
> +                "action": "add",
> +            })
> +
> +        for field_name in sorted(
> +                name for name in data if name.split(".")[0] == "image_name"):
> +            _, rule_id = self._parseFieldName(field_name)
> +            image_field_name = self._getFieldName("image_name", rule_id)
> +            delete_field_name = self._getFieldName("delete", rule_id)
> +            if data.get(delete_field_name):
> +                action = "delete"
> +            else:
> +                action = "change"
> +            parsed_data.setdefault(rule_id, {
> +                "image_name": data.get(image_field_name),
> +                "action": action,
> +            })
> +
> +        return parsed_data
> +
> +    def updatePushRulesFromData(self, parsed_data):
> +        rules_map = {
> +            rule.id: rule
> +            for rule in self.context.push_rules}
> +        for rule_id, parsed_rules in parsed_data.items():
> +            rule = rules_map.get(rule_id)
> +            action = parsed_rules["action"]
> +
> +            if action == "change":
> +                image_name = parsed_rules["image_name"]
> +                if not image_name:
> +                    self.setFieldError(
> +                        self._getFieldName(
> +                            "image_name", rule_id),
> +                        "Image name must be set.")
> +                else:
> +                    removeSecurityProxy(rule).image_name = image_name
> +            elif action == "delete":
> +                removeSecurityProxy(rule).destroySelf()
> +            elif action == "add":
> +                add_data = parsed_data[None]

Can we refactor this to another method? Please, check the comments for coding style on `def addCredentials` above to avoid too many levels of indentation and repeated try/except blocks.

> +                image_name = add_data["image_name"]
> +                url = add_data["url"]
> +                password = add_data["password"]
> +                confirm_password = add_data["confirm_password"]
> +                username = add_data["username"]
> +                existing_credentials = add_data["existing_credentials"]
> +                checked_use_existing_credentials = \
> +                    add_data["use_existing_credentials"]
> +                checked_add_new_credentials = add_data["add_new_credentials"]
> +
> +                if image_name:
> +                    if not (checked_add_new_credentials
> +                            or checked_use_existing_credentials):
> +                        self.setFieldError("existing_credentials",
> +                                           "You must check either use existing"
> +                                           " or introduce new credentials for "
> +                                           "this rule.")
> +                        return
> +                    if checked_add_new_credentials \

Prefer putting this type of line breaking with parenthesis rather than backslash. One extra empty space after the backslash here could cause SyntaxError, for example. And it could be tricky to find.

```
if (checked_add_new_credentials
        and checked_use_existing_credentials):
    # the code
```

> +                            and checked_use_existing_credentials:
> +                        self.setFieldError("existing_credentials",
> +                                           "You can either use existing "
> +                                           "credentials or introduce new ones "
> +                                           "for this rule but not both.")
> +                        return
> +                    if checked_use_existing_credentials:
> +                        if existing_credentials:
> +                            try:
> +                                getUtility(IOCIPushRuleSet).new(
> +                                    self.context,
> +                                    existing_credentials,
> +                                    image_name)
> +                            except OCIPushRuleAlreadyExists:
> +                                self.setFieldError("add_image_name",
> +                                                   "A push rule already exists"
> +                                                   " with the same URL, image "
> +                                                   "name and credentials.")
> +                                return
> +                    if checked_add_new_credentials:
> +                        if url:
> +                            if password == confirm_password:
> +                                credentials = {
> +                                    'username': username,
> +                                    'password': password}
> +                                try:
> +                                    creds = getUtility(
> +                                        IOCIRegistryCredentialsSet
> +                                    ).getOrCreate(
> +                                        owner=self.context.owner,
> +                                        url=url,
> +                                        credentials=credentials)
> +                                except OCIRegistryCredentialsAlreadyExist:
> +                                    self.setFieldError(
> +                                        "add_url",
> +                                        "Credentials already exist with"
> +                                        " the same URL and username.")
> +                                    return
> +                                try:
> +                                    getUtility(IOCIPushRuleSet).new(
> +                                        self.context, creds, image_name)
> +                                except OCIPushRuleAlreadyExists:
> +                                    self.setFieldError(
> +                                        "add_image_name",
> +                                        "A push rule already exists with the "
> +                                        "same URL, image name, and "
> +                                        "credentials.")
> +                                    return
> +                            else:
> +                                self.setFieldError(
> +                                    "password",
> +                                    "Please make sure the new password matches"
> +                                    " the confirm password field.")
> +                        else:
> +                            self.setFieldError(
> +                                "add_url",
> +                                "Registry URL must be set.")
> +                else:
> +                    self.setFieldError(
> +                        "add_image_name",
> +                        "Image name must be set.")
> +            else:
> +                raise AssertionError("unknown action: %s" % action)
> +
> +    @action("Save")
> +    def save(self, action, data):
> +        parsed_data = self.parseData(data)
> +        self.updatePushRulesFromData(parsed_data)
> +
> +        if not self.errors:
> +            self.request.response.addNotification("Saved push rules")
> +            self.next_url = canonical_url(self.context)
> +
> +
>  class OCIRecipeRequestBuildsView(LaunchpadFormView):
>      """A view for requesting builds of an OCI recipe."""
>  
> diff --git a/lib/lp/oci/templates/ocirecipe-edit-credentials.pt b/lib/lp/oci/templates/ocirecipe-edit-credentials.pt
> new file mode 100644
> index 0000000..89483da
> --- /dev/null
> +++ b/lib/lp/oci/templates/ocirecipe-edit-credentials.pt

I think the other templates are being indented with 2 spaces, instead of 4. I'm not sure about the policy for this, but I usually follow the 2 spaces.

> @@ -0,0 +1,59 @@
> +<html
> +  xmlns="http://www.w3.org/1999/xhtml";
> +  xmlns:tal="http://xml.zope.org/namespaces/tal";
> +  xmlns:metal="http://xml.zope.org/namespaces/metal";
> +  xmlns:i18n="http://xml.zope.org/namespaces/i18n";
> +  metal:use-macro="view/macro:page/main_only"
> +  i18n:domain="launchpad">
> +<body>
> +
> +<div metal:fill-slot="main">
> +    <div metal:use-macro="context/@@launchpad_form/form">
> +    <metal:formbody fill-slot="widgets">
> +
> +    <table class="form">
> +        <tr tal:repeat="credentials view/oci_registry_credentials">
> +            <tal:credentials_widgets
> +              define="credentials_widgets python:view.getCredentialsWidgets(credentials);
> +                      parity python:'even' if repeat['credentials'].even() else 'odd'">
> +                <td tal:define="widget nocall:credentials_widgets/url">
> +                    <metal:widget use-macro="context/@@launchpad_form/widget_div" />
> +                </td>
> +                <td tal:define="widget nocall:credentials_widgets/owner">
> +                    <metal:widget use-macro="context/@@launchpad_form/widget_div" />
> +                </td>
> +                <td tal:define="widget nocall:credentials_widgets/username">
> +                    <metal:widget use-macro="context/@@launchpad_form/widget_div" />
> +                </td>
> +                <td tal:define="widget nocall:credentials_widgets/password">
> +                    <metal:widget use-macro="context/@@launchpad_form/widget_div" />
> +                </td>
> +                <td tal:define="widget nocall:credentials_widgets/confirm_password">
> +                    <metal:widget use-macro="context/@@launchpad_form/widget_div" />
> +                </td>
> +                <td tal:define="widget nocall:credentials_widgets/delete">
> +                    <metal:widget use-macro="context/@@launchpad_form/widget_div" />
> +                </td>
> +            </tal:credentials_widgets>
> +        </tr>
> +        <tr>
> +            <td tal:define="widget nocall:view/widgets/add_url">
> +                <metal:widget use-macro="context/@@launchpad_form/widget_div" />
> +            </td>
> +            <td tal:define="widget nocall:view/widgets/add_username">
> +                <metal:widget use-macro="context/@@launchpad_form/widget_div" />
> +            </td>
> +            <td tal:define="widget nocall:view/widgets/add_password">
> +                <metal:widget use-macro="context/@@launchpad_form/widget_div" />
> +            </td>
> +            <td tal:define="widget nocall:view/widgets/add_confirm_password">
> +                <metal:widget use-macro="context/@@launchpad_form/widget_div" />
> +            </td>
> +        </tr>
> +    </table>
> +
> +    </metal:formbody>
> +    </div>
> +</div>
> +</body>
> +</html>


-- 
https://code.launchpad.net/~ilasc/launchpad/+git/launchpad/+merge/386371
Your team Launchpad code reviewers is requested to review the proposed merge of ~ilasc/launchpad:oci-recipe-push-rules-edit into launchpad:master.


References