swagger-api / swagger-codegen

swagger-codegen contains a template-driven engine to generate documentation, API clients and server stubs in different languages by parsing your OpenAPI / Swagger definition.
http://swagger.io
Apache License 2.0
17k stars 6.03k forks source link

Python/Flask server 'import re' missing in models #4752

Closed webron closed 7 years ago

webron commented 7 years ago

From @jaccoh on February 8, 2017 8:13

Swagger File

---
swagger: "2.0"
info:
  description: "This API allows you to do CRUD operations to the authoratitive DNS\
    \ environement. Please use it with care and common sense. Use it at your own risk.\
    \ Please see: https://confluence.solvinity.net/display/PE/Authoritiative+DNS+Rest-API\
    \ for more information."
  version: "3.0"
  title: "AuthDNS API"
  contact:
    name: "Jacco Hoeve"
    email: "jacco.hoeve@solvinity.com"
  license:
    name: "proprietary"
basePath: "/api/v3"
schemes:
  - http
  - https
x-types: #These anchors are reused throughout the yaml file.
    zone: &zone
      name: "zone"
      in: "path"
      description: "Name of zone. Allowed characters: a-z (lowercase!), 0-9, '-' and '.' (dot not at the beginning and not more than one in a row). Zone must also end with a dot."
      required: true
      type: "string"
      pattern: "^(([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])\\.)*([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])\\.$"
    zone_all: &zone_all
      name: "zone"
      in: "path"
      description: "Name of zone. Allowed characters: a-z (lowercase!), 0-9, '-' and '.' (dot not at the beginning and not more than one in a row). Zone must also end with a dot. You may also specify 'all'"
      required: true
      type: "string"
      pattern: "^(([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])\\.)*([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])\\.$|^all$"
    view: &view
      name: "view"
      in: "path"
      description: "Name of view"
      required: true
      type: "string"
    record: &record
      record:
        type: "string"
        description: "Record/Host name. Allowed characters: a-z (lowercase!), 0-9, '-', '_' (underscore only as first character), and '.' (dot not at the beginning and not more than one in a row)."
        pattern: "^[_]?(([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])\\.)*([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])$"
    value: &value
      value:
        type: "string"
        description: "Value of record or current value or record (when updating)"
    type: &type
      type:
        type: "string"
        enum:
          - "A"
          - "AAAA"
          - "CNAME"
          - "MX"
          - "TXT"
          - "SRV"
          - "PTR"
          - "NS"
          - "SOA"
          - "TLSA"
        description: "DNS record type"
    whoami: &whoami
      whoami:
        type: "string"
        description: "mandatory username"
    key_id: &key_id
      key_id:
        type: "string"
        description: "Key ID, Use only 0-9, five characters long. String format."
        pattern: "^[0-9]{5}$"
    deletelast: &deletelast
      deletelast:
        type: "boolean"
        description: "By default you can not delete the last KSK. This would cripple\
          \ DNSSEC. You can override this with this boolean. This will disable DNSSEC\
          \ completely."
        default: false
    roundrobin: &roundrobin
      roundrobin:
        type: "boolean"
        default: false
    ttl: &ttl
      ttl:
        type: "integer"
        description: "DNS record TTL"
        minimum: 300
        default: 3600
    lock_time: &lock_time
      lock_time:
        type: "integer"
        description: "Request a lock on this zone for a number of seconds.\
                  \ After the lock time has expired, the zone will be auto-committed\
                  \ as is. Default 60."
        default: 60
    contact: &contact
      contact:
        type: "string"
        description: "email address for SOA record. Formatted as FQDN with trailing dot."
        pattern: "^(([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])\\.)*([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])\\.$"
    refresh: &refresh
      refresh:
        type: "integer"
        description: "refresh for SOA record"
    minttl: &minttl
      minttl:
        type: "integer"
        description: "Minimal TTL for SOA record"
    retry: &retry
      retry:
        type: "integer"
        description: "Retry for SOA record"
paths:
  /dnssec/ksk/{zone}:
    post:
      tags:
        - "AuthDNS"
      description: "Create new Key Signing Key"
      operationId: "swagger_server.controllers.auth_dns_controller.dnssec_ksk_zone_post"
      parameters:
        - <<: *zone
        - in: "body"
          name: "paramBody"
          description: "Put JSON body here."
          required: true
          schema:
            $ref: "#/definitions/paramBody"
      responses:
        200:
          description: "Create new Key Signing Key succeeded"
          schema:
            type: "string"
            title: "statusmsg"
        404:
          description: "Create new Key Signing Key failed"
          schema:
            type: "string"
            title: "statusmsg"
      security:
        - basicAuth: []
      x-tags:
        - tag: "AuthDNS"
    delete:
      tags:
        - "AuthDNS"
      description: "Delete Key Signing Key"
      operationId: "swagger_server.controllers.auth_dns_controller.dnssec_ksk_zone_delete"
      parameters:
        - <<: *zone
        - in: "body"
          name: "paramBody"
          description: "Put JSON body here."
          required: true
          schema:
            $ref: "#/definitions/paramBody1"
      responses:
        200:
          description: "Delete Key Signing Key succeeded"
          schema:
            type: "string"
            title: "statusmsg"
        404:
          description: "Delete Key Signing Key failed"
          schema:
            type: "string"
            title: "statusmsg"
      security:
        - basicAuth: []
      x-tags:
        - tag: "AuthDNS"
  /dnssec/zsk/{zone}:
    post:
      tags:
        - "AuthDNS"
      description: "Create new Zone Signing Key"
      operationId: "swagger_server.controllers.auth_dns_controller.dnssec_zsk_zone_post"
      parameters:
        - <<: *zone
        - in: "body"
          name: "paramBody"
          description: "Put JSON body here."
          required: true
          schema:
            $ref: "#/definitions/paramBody2"
      responses:
        200:
          description: "Create new Zone Signing Key succeeded"
          schema:
            type: "string"
            title: "statusmsg"
        404:
          description: "Create new Zone Signing Key failed"
          schema:
            type: "string"
            title: "statusmsg"
      security:
        - basicAuth: []
      x-tags:
        - tag: "AuthDNS"
    delete:
      tags:
        - "AuthDNS"
      description: "Delete Zone Signing Key"
      operationId: "swagger_server.controllers.auth_dns_controller.dnssec_zsk_zone_delete"
      parameters:
        - <<: *zone
        - in: "body"
          name: "paramBody"
          description: "Put JSON body here."
          required: true
          schema:
            $ref: "#/definitions/paramBody3"
      responses:
        200:
          description: "Delete Zone Signing Key succeeded"
          schema:
            type: "string"
            title: "statusmsg"
        404:
          description: "Delete Zone Signing Key failed"
          schema:
            type: "string"
            title: "statusmsg"
      security:
        - basicAuth: []
      x-tags:
        - tag: "AuthDNS"
  /dnssec/{zone}:
    get:
      tags:
        - "AuthDNS"
      description: "Gets `zone` dnssec stats.\n"
      operationId: "swagger_server.controllers.auth_dns_controller.dnssec_zone_get"
      parameters:
        - <<: *zone
      responses:
        200:
          description: "Zone DNSSEC stats"
          schema:
            $ref: "#/definitions/zone"
        404:
          description: "Find failed"
          schema:
            type: "string"
            title: "statusmsg"
      security:
        - basicAuth: []
      x-tags:
        - tag: "AuthDNS"
    put:
      tags:
        - "AuthDNS"
      description: "Update `zone` dnssec by syncing keys with registrar.\n"
      operationId: "swagger_server.controllers.auth_dns_controller.dnssec_zone_put"
      parameters:
        - <<: *zone
      responses:
        200:
          description: "Sync succeeded"
          schema:
            type: "string"
            title: "statusmsg"
        404:
          description: "Sync failed"
          schema:
            type: "string"
            title: "statusmsg"
      security:
        - basicAuth: []
      x-tags:
        - tag: "AuthDNS"
  /find/{view}/{zone}:
    get:
      tags:
        - "AuthDNS"
      description: "Gets `zone` objects.\n"
      operationId: "swagger_server.controllers.auth_dns_controller.find_view_zone_get"
      parameters:
        - <<: *zone_all
        - name: "view"
          in: "path"
          description: "Name of view. Specify 'all' for all views."
          required: true
          type: "string"
        - name: "regex"
          in: "query"
          description: "Put (part of) record here of at least two characters or use a regex!"
          required: true
          type: "string"
          minLength: 2
      responses:
        200:
          description: "Zone content"
          schema:
            type: "array"
            title: "ArrayOfViews"
            items:
              $ref: "#/definitions/View"
        404:
          description: "Find failed"
          schema:
            type: "string"
            title: "statusmsg"
      security:
        - basicAuth: []
      x-tags:
        - tag: "AuthDNS"
  /listviews:
    get:
      tags:
        - "AuthDNS"
      description: "Get list of views"
      operationId: "swagger_server.controllers.auth_dns_controller.listviews_get"
      parameters: []
      responses:
        200:
          description: "Successful response!"
          schema:
            type: "array"
            title: "List of views"
            items:
              type: "string"
              title: "View"
        404:
          description: "List views failed!"
          schema:
            type: "string"
            title: "statusmsg"
      security:
        - basicAuth: []
      x-tags:
        - tag: "AuthDNS"
  /listzones/{view}:
    get:
      tags:
       - "AuthDNS"
      description: "Gets list of zones."
      operationId: "swagger_server.controllers.auth_dns_controller.listzones_view_get"
      parameters:
        - <<: *view
      responses:
        200:
          description: "Successful response - List of zones"
          schema:
            $ref: "#/definitions/inline_response200"
        404:
          description: "Listzones failed"
          schema:
            type: "string"
            title: "statusmsg"
      security:
        - basicAuth: []
      x-tags:
        - tag: "AuthDNS"
  /record/{view}/{zone}:
    post:
      tags:
      - "AuthDNS"
      description: "Add record to zone"
      operationId: "swagger_server.controllers.auth_dns_controller.record_view_zone_post"
      parameters:
        - <<: *zone
        - <<: *view
        - in: "body"
          name: "paramBody"
          description: "Put JSON body here."
          required: true
          schema:
            $ref: "#/definitions/paramBody5"
      responses:
        200:
          description: "Add record succeeded"
          schema:
            type: "string"
            title: "statusmsg"
        404:
          description: "Add record failed"
          schema:
            type: "string"
            title: "statusmsg"
      security:
        - basicAuth: []
      x-tags:
        - tag: "AuthDNS"
    put:
      tags:
        - "AuthDNS"
      description: "Update a record"
      operationId: "swagger_server.controllers.auth_dns_controller.record_view_zone_put"
      parameters:
        - name: "view"
          in: "path"
          description: "Name of view"
          required: true
          type: "string"
        - <<: *zone
        - in: "body"
          name: "paramBody"
          description: "Put JSON body here."
          required: true
          schema:
            $ref: "#/definitions/paramBody4"
      responses:
        200:
          description: "modify succeeded"
          schema:
            type: "string"
            title: "statusmsg"
        404:
          description: "modify failed"
          schema:
            type: "string"
            title: "statusmsg"
      security:
        - basicAuth: []
      x-tags:
        - tag: "AuthDNS"
    delete:
      tags:
        - "AuthDNS"
      description: "Delete a record from a zone"
      operationId: "swagger_server.controllers.auth_dns_controller.record_view_zone_delete"
      parameters:
        - <<: *zone
        - <<: *view
        - in: "body"
          name: "paramBody"
          description: "Put JSON body here."
          required: true
          schema:
            $ref: "#/definitions/paramBody6"
      responses:
        200:
          description: "delete record succeeded"
          schema:
            type: "string"
            title: "statusmsg"
        404:
          description: "delete record failed"
          schema:
            type: "string"
            title: "statusmsg"
      security:
        - basicAuth: []
      x-tags:
        - tag: "AuthDNS"
  /commit/{view}/{zone}:
    post:
      tags:
        - "AuthDNS"
      description: "Commit zone. Apply changes."
      operationId: "swagger_server.controllers.auth_dns_controller.commit_view_zone_post"
      parameters:
        - <<: *view
        - <<: *zone
        - in: "body"
          name: "paramBody"
          description: "Put JSON body here."
          required: true
          schema:
            $ref: "#/definitions/paramBody8"
      responses:
        200:
          description: "Create zone succeeded"
          schema:
            type: "string"
            title: "statusmsg"
        404:
          description: "Create zone failed"
          schema:
            type: "string"
            title: "statusmsg"
      security:
        - basicAuth: []
      x-tags:
        - tag: "AuthDNS"
  /zone/{view}/{zone}:
    get:
      tags:
        - "AuthDNS"
      description: "View zone content"
      operationId: "swagger_server.controllers.auth_dns_controller.zone_view_zone_get"
      parameters:
        - <<: *view
        - <<: *zone
      responses:
        200:
          description: "Zone content"
          schema:
            type: "array"
            title: "ArrayOfViews"
            items:
              $ref: "#/definitions/View"
        404:
          description: "Zone not found"
      security:
        - basicAuth: []
      x-tags:
        - tag: "AuthDNS"
    post:
      tags:
        - "AuthDNS"
      description: "Create new zone"
      operationId: "swagger_server.controllers.auth_dns_controller.zone_view_zone_post"
      parameters:
        - <<: *view
        - <<: *zone
        - in: "body"
          name: "paramBody"
          description: "Put JSON body here."
          required: true
          schema:
            $ref: "#/definitions/paramBody7"
      responses:
        200:
          description: "Create zone succeeded"
          schema:
            type: "string"
            title: "statusmsg"
        404:
          description: "Create zone failed"
          schema:
            type: "string"
            title: "statusmsg"
      security:
        - basicAuth: []
      x-tags:
        - tag: "AuthDNS"
    delete:
      tags:
        - "AuthDNS"
      description: "Delete a zone"
      operationId: "swagger_server.controllers.auth_dns_controller.zone_view_zone_delete"
      parameters:
        - <<: *view
        - <<: *zone
        - in: "body"
          name: "paramBody"
          description: "Put JSON body here."
          required: true
          schema:
            $ref: "#/definitions/paramBody9"
      responses:
        200:
          description: "delete or deletezone succeeded"
          schema:
            type: "string"
            title: "statusmsg"
        404:
          description: "delete or deletezone failed"
          schema:
            type: "string"
            title: "statusmsg"
      security:
        - basicAuth: []
      x-tags:
        - tag: "AuthDNS"
securityDefinitions:
  basicAuth:
    description: "HTTP Basic Authentication (SUPPORTASP)."
    type: "basic"
definitions:
  paramBody:
    type: "object"
    properties:
      <<: *whoami
      <<: *lock_time
  paramBody1:
    type: "object"
    properties:
      <<: *whoami
      <<: *key_id
      <<: *deletelast
      <<: *lock_time
  paramBody2:
    type: "object"
    properties:
      <<: *whoami
      <<: *lock_time
  listzones_domain_block:
    properties:
      zone:
        type: "string"
      dnssec:
        type: "boolean"
  zone_zone_type block:
    properties:
      key stats:
        type: "array"
        items:
          type: "string"
  zone_zone:
    properties:
      type block:
        $ref: "#/definitions/zone_zone_type block"
  zone:
    properties:
      dnssec:
        type: "boolean"
      zone:
        $ref: "#/definitions/zone_zone"
  findviewzone_view:
    properties:
      zone:
        type: "array"
        items:
          type: "string"
  View:
    properties:
      view:
        $ref: "#/definitions/findviewzone_view"
  inline_response200:
    properties:
      view:
        type: "array"
        items:
          $ref: "#/definitions/listzones_domain_block"
  paramBody3:
    type: "object"
    properties:
      <<: *whoami
      <<: *key_id
      <<: *deletelast
      <<: *lock_time
  paramBody4:
    type: "object"
    properties:
      <<: *record
      <<: *value
      <<: *type
      <<: *whoami
      <<: *ttl
      <<: *contact
      <<: *refresh
      <<: *minttl
      <<: *retry
      <<: *lock_time
      ns:
        type: "string"
        description: "hostname of primary NS server (SOA record only)"
        pattern: "^(([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])\\.)*([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])\\.$"
      expire:
        type: "integer"
        description: "Expire field in SOA record"
      newvalue:
        type: "string"
        description: "New value of record"
  paramBody5:
    type: "object"
    properties:
      <<: *record
      <<: *value
      <<: *type
      <<: *roundrobin
      <<: *whoami
      <<: *ttl
      <<: *lock_time
  paramBody6:
    type: "object"
    properties:
      <<: *record
      <<: *value
      <<: *type
      <<: *whoami
      <<: *lock_time
  paramBody7:
    type: "object"
    properties:
      <<: *whoami
      <<: *lock_time
      dnssec:
        type: "boolean"
        description: "Handle DNSSEC True/False"
        default: true
  paramBody8:
    type: "object"
    properties:
      <<: *whoami
  paramBody9:
    type: "object"
    properties:
      <<: *whoami

Swagger Editor Version 2.10.3

Issue I have in my API definition a few values which I check with 'pattern'. Eg:

x-types: #These anchors are reused throughout the yaml file.
    zone: &zone
      name: "zone"
      in: "path"
      description: "Name of zone. Allowed characters: a-z (lowercase!), 0-9, '-' and '.' (dot not at the beginning and not more than one in a row). Zone must also end with a dot."
      required: true
      type: "string"
      pattern: "^(([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])\\.)*([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])\\.$"

Yes I use YAML anchors if the value is reused often.

If I generate a server (python-flask), the models in question are missing an 'import re'. Eg:

class ParamBody4(Model):
...
        if record is not None and not re.search('', record):
            raise ValueError("Invalid value for `record`, must be a follow pattern or equal to `/^[_]?(([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])\\.)*([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])$/`")

That wont work without 're' being imported.

from __future__ import absolute_import
from .base_model_ import Model
from datetime import date, datetime
from typing import List, Dict
from ..util import deserialize_model

Copied from original issue: swagger-api/swagger-editor#1173

jaccoh commented 7 years ago

Thanks for moving it to the correct project.

cbornet commented 7 years ago

@jaccoh It's easy to fix. Would you do the PR ?

jaccoh commented 7 years ago

sure

but I'm not familiar with the code. Im not sure hardcoding an include is the best way to go if its not needed?

wing328 commented 7 years ago

@jaccoh I wonder if you can pull the latest master to give it a try as I've previously fixed some issues related to model import in Python Flask generator. Thanks.

jaccoh commented 7 years ago

Just tested after cloning the master branch. No dice.

Am I correct in assuming mustache should put that import in there because of this?:

{{#imports}}{{import}}
{{/imports}}

Other imports are simply hardcoded in 'modules/swagger-codegen/target/classes/flaskConnexion/model.mustache'.

wing328 commented 7 years ago

Am I correct in assuming mustache should put that import in there because of this?:

Yes, that's correct.

wing328 commented 7 years ago

@jaccoh I might have missed it. Can you show the error message you got the starting the Flask server?

jaccoh commented 7 years ago

It will actually start:

(test)  jhoeve-a@laptop-jacco  ~/ff/swagger-codegen/out/test   master  python -m swagger_server   
 * Running on http://127.0.0.1:8080/ (Press CTRL+C to quit)

But when I call the function from the API:

[2017-03-13 16:19:18,343] ERROR in app: Exception on /api/v3/view/internal/zone/test2.nl./type/A/record/www [PUT]
Traceback (most recent call last):
  File "/home/jhoeve-a/ff/swagger-codegen/out/test/.virtualenv/test/lib/python3.5/site-packages/flask/app.py", line 1982, in wsgi_app
    response = self.full_dispatch_request()
  File "/home/jhoeve-a/ff/swagger-codegen/out/test/.virtualenv/test/lib/python3.5/site-packages/flask/app.py", line 1614, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/home/jhoeve-a/ff/swagger-codegen/out/test/.virtualenv/test/lib/python3.5/site-packages/flask/app.py", line 1517, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/home/jhoeve-a/ff/swagger-codegen/out/test/.virtualenv/test/lib/python3.5/site-packages/flask/_compat.py", line 33, in reraise
    raise value
  File "/home/jhoeve-a/ff/swagger-codegen/out/test/.virtualenv/test/lib/python3.5/site-packages/flask/app.py", line 1612, in full_dispatch_request
    rv = self.dispatch_request()
  File "/home/jhoeve-a/ff/swagger-codegen/out/test/.virtualenv/test/lib/python3.5/site-packages/flask/app.py", line 1598, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/home/jhoeve-a/ff/swagger-codegen/out/test/.virtualenv/test/lib/python3.5/site-packages/connexion/decorators/validation.py", line 127, in wrapper
    response = function(*args, **kwargs)
  File "/home/jhoeve-a/ff/swagger-codegen/out/test/.virtualenv/test/lib/python3.5/site-packages/connexion/decorators/validation.py", line 290, in wrapper
    response = function(*args, **kwargs)
  File "/home/jhoeve-a/ff/swagger-codegen/out/test/.virtualenv/test/lib/python3.5/site-packages/connexion/decorators/produces.py", line 118, in wrapper
    data, status_code, headers = self.get_full_response(function(*args, **kwargs))
  File "/home/jhoeve-a/ff/swagger-codegen/out/test/.virtualenv/test/lib/python3.5/site-packages/connexion/decorators/parameter.py", line 156, in wrapper
    return function(*args, **kwargs)
  File "/home/jhoeve-a/ff/swagger-codegen/out/test/swagger_server/controllers/auth_dns_controller.py", line 246, in record_view_zone_put
    paramBody = ParamBody1.from_dict(connexion.request.get_json())
  File "/home/jhoeve-a/ff/swagger-codegen/out/test/swagger_server/models/param_body1.py", line 82, in from_dict
    return deserialize_model(dikt, cls)
  File "/home/jhoeve-a/ff/swagger-codegen/out/test/swagger_server/util.py", line 116, in deserialize_model
    setattr(instance, attr, _deserialize(value, attr_type))
  File "/home/jhoeve-a/ff/swagger-codegen/out/test/swagger_server/models/param_body1.py", line 244, in contact
    if contact is not None and not re.search('', contact):
NameError: name 're' is not defined

Thats because of:

(test)  jhoeve-a@laptop-jacco  ~/ff/swagger-codegen/out/test   master  grep 're\.search' * -ri
swagger_server/models/param_body1.py:        if ns is not None and not re.search('', ns):
swagger_server/models/param_body1.py:        if contact is not None and not re.search('', contact):

You could argue those are re.search are kind of useless in that area.

On a note: The swagger definition is slightly different from the one in the post... but the idea is the same.

wing328 commented 7 years ago

@jaccoh the issue should be fixed by #5127. Please pull the latest master to give it a try.

Thanks for the PR by @fabito

jaccoh commented 7 years ago

The 'import re' is now there. But the regexp aren't:

 jhoeve-a@laptop-jacco  ~/GitCollections/swagger-codegen/out/test/swagger_server/models   master  grep 're.search' *
param_body1.py:        if ns is not None and not re.search('', ns):
param_body1.py:        if contact is not None and not re.search('', contact):
wing328 commented 7 years ago

@jaccoh right. I'll take a look.

wing328 commented 7 years ago

@jaccoh I've fixed the issue via #5155

Please pull the latest master to give it another try.

jaccoh commented 7 years ago

done and done

Looks good:

 jhoeve-a@laptop-jacco  ~/GitCollections/swagger-codegen   master  cat out/test/swagger_server/models/param_body1.py | grep search
        if ns is not None and not re.search('^(([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])\\.)*([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])\\.$', ns):
        if contact is not None and not re.search('^(([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])\\.)*([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])\\.$', contact):
wing328 commented 7 years ago

@jaccoh thanks for verifying the change, and thanks @fabito again for the PR (#5127)