diff --git a/src/Pipfile b/src/Pipfile index 2a94867a7..b94d5aed8 100644 --- a/src/Pipfile +++ b/src/Pipfile @@ -32,4 +32,3 @@ types-requests = "*" django-stubs = "*" django-webtest = "*" types-cachetools = "*" -graphviz = ">=0.4" diff --git a/src/Pipfile.lock b/src/Pipfile.lock index 25ce7a6ae..b9abf9893 100644 --- a/src/Pipfile.lock +++ b/src/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "80ebab4c3aa382d11cc102ff541e23fd3a43e7943eb66ad58b2805304dbaf4fe" + "sha256": "f3c73d2389ee9b1648528a855174d19d20b67f64a2337a660ebeaf613db31488" }, "pipfile-spec": 6, "requires": {}, @@ -260,7 +260,7 @@ "hashes": [ "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.18.2" }, "gunicorn": { @@ -526,7 +526,7 @@ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.16.0" }, "sqlparse": { @@ -584,7 +584,7 @@ "sha256:58d5c3d29f5a36ffeb94f02f0d786cd53014cf9b3b3951d42e0080d8a9498d30", "sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==4.11.1" }, "black": { @@ -683,7 +683,7 @@ "sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd", "sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==4.0.9" }, "gitpython": { @@ -694,20 +694,12 @@ "markers": "python_version >= '3.7'", "version": "==3.1.29" }, - "graphviz": { - "hashes": [ - "sha256:587c58a223b51611c0cf461132da386edd896a029524ca61a1462b880bf97977", - "sha256:8c58f14adaa3b947daf26c19bc1e98c4e0702cdc31cf99153e6f06904d492bf8" - ], - "index": "pypi", - "version": "==0.20.1" - }, "mccabe": { "hashes": [ "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==0.7.0" }, "mypy": { @@ -790,7 +782,7 @@ "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785", "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==2.9.1" }, "pyflakes": { @@ -798,7 +790,7 @@ "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2", "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==2.5.0" }, "pyyaml": { @@ -844,7 +836,7 @@ "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==6.0" }, "six": { @@ -852,7 +844,7 @@ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.16.0" }, "smmap": { @@ -860,7 +852,7 @@ "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94", "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==5.0.0" }, "soupsieve": { @@ -868,7 +860,7 @@ "sha256:3b2503d3c7084a42b1ebd08116e5f81aadfaea95863628c80a3b774a11b7c759", "sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==2.3.2.post1" }, "sqlparse": { @@ -892,7 +884,7 @@ "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" ], - "markers": "python_full_version < '3.11.0a7'", + "markers": "python_version < '3.11'", "version": "==2.0.1" }, "types-cachetools": { @@ -953,7 +945,7 @@ "sha256:73aae30359291c14fa3b956f8b5ca31960e420c28c1bec002547fb04928cf89b", "sha256:b64ef5141be559cfade448f044fa45c2260351edcb6a8ef6b7e00c7dcef0c323" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.8.7" }, "webtest": { @@ -961,7 +953,7 @@ "sha256:2a001a9efa40d2a7e5d9cd8d1527c75f41814eb6afce2c3d207402547b1e5ead", "sha256:54bd969725838d9861a9fa27f8d971f79d275d94ae255f5c501f53bb6d9929eb" ], - "markers": "python_version >= '3.6' and python_version < '4'", + "markers": "python_version < '4' and python_full_version >= '3.6.0'", "version": "==3.0.0" } } diff --git a/src/api/views.py b/src/api/views.py index c25f98b97..042a447e3 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -1,8 +1,6 @@ """Internal API views""" -import re - from django.core.exceptions import BadRequest from django.views.decorators.http import require_http_methods from django.http import JsonResponse @@ -13,22 +11,11 @@ import requests from cachetools.func import ttl_cache +from registrar.models import Website + DOMAIN_FILE_URL = ( "https://raw.githubusercontent.com/cisagov/dotgov-data/main/current-full.csv" ) -# a domain name is alphanumeric or hyphen, up to 63 characters, doesn't -# begin or end with a hyphen, followed by a TLD of 2-6 alphabetic characters -DOMAIN_REGEX = re.compile(r"^(?!-)[A-Za-z0-9-]{1,63}(?= 4.0 - from django.utils.encoding import force_str as force_text - from django.core.management.base import ALL_CHECKS - _requires_system_checks = ALL_CHECKS - -from django_fsm import FSMFieldMixin, GET_STATE, RETURN_VALUE - -try: - from django.db.models import get_apps, get_app, get_models, get_model - - NEW_META_API = False -except ImportError: - from django.apps import apps - - NEW_META_API = True - -from django import VERSION - -HAS_ARGPARSE = VERSION >= (1, 10) - - -def all_fsm_fields_data(model): - if NEW_META_API: - return [(field, model) for field in model._meta.get_fields() if isinstance(field, FSMFieldMixin)] - else: - return [(field, model) for field in model._meta.fields if isinstance(field, FSMFieldMixin)] - - -def node_name(field, state): - opts = field.model._meta - return "%s.%s.%s.%s" % (opts.app_label, opts.verbose_name.replace(" ", "_"), field.name, state) - - -def node_label(field, state): - if type(state) == int or (type(state) == bool and hasattr(field, "choices")): - return force_text(dict(field.choices).get(state)) - else: - return state - - -def generate_dot(fields_data): - result = graphviz.Digraph() - - for field, model in fields_data: - sources, targets, edges, any_targets, any_except_targets = set(), set(), set(), set(), set() - - # dump nodes and edges - for transition in field.get_all_transitions(model): - if transition.source == "*": - any_targets.add((transition.target, transition.name)) - elif transition.source == "+": - any_except_targets.add((transition.target, transition.name)) - else: - _targets = ( - (state for state in transition.target.allowed_states) - if isinstance(transition.target, (GET_STATE, RETURN_VALUE)) - else (transition.target,) - ) - source_name_pair = ( - ((state, node_name(field, state)) for state in transition.source.allowed_states) - if isinstance(transition.source, (GET_STATE, RETURN_VALUE)) - else ((transition.source, node_name(field, transition.source)),) - ) - for source, source_name in source_name_pair: - if transition.on_error: - on_error_name = node_name(field, transition.on_error) - targets.add((on_error_name, node_label(field, transition.on_error))) - edges.add((source_name, on_error_name, (("style", "dotted"),))) - for target in _targets: - add_transition(source, target, transition.name, source_name, field, sources, targets, edges) - - targets.update( - set((node_name(field, target), node_label(field, target)) for target, _ in chain(any_targets, any_except_targets)) - ) - for target, name in any_targets: - target_name = node_name(field, target) - all_nodes = sources | targets - for source_name, label in all_nodes: - sources.add((source_name, label)) - edges.add((source_name, target_name, (("label", name),))) - - for target, name in any_except_targets: - target_name = node_name(field, target) - all_nodes = sources | targets - all_nodes.remove(((target_name, node_label(field, target)))) - for source_name, label in all_nodes: - sources.add((source_name, label)) - edges.add((source_name, target_name, (("label", name),))) - - # construct subgraph - opts = field.model._meta - subgraph = graphviz.Digraph( - name="cluster_%s_%s_%s" % (opts.app_label, opts.object_name, field.name), - graph_attr={"label": "%s.%s.%s" % (opts.app_label, opts.object_name, field.name)}, - ) - - final_states = targets - sources - for name, label in final_states: - subgraph.node(name, label=label, shape="doublecircle") - for name, label in (sources | targets) - final_states: - subgraph.node(name, label=label, shape="circle") - if field.default: # Adding initial state notation - if label == field.default: - initial_name = node_name(field, "_initial") - subgraph.node(name=initial_name, label="", shape="point") - subgraph.edge(initial_name, name) - for source_name, target_name, attrs in edges: - subgraph.edge(source_name, target_name, **dict(attrs)) - - result.subgraph(subgraph) - - return result - - -def add_transition(transition_source, transition_target, transition_name, source_name, field, sources, targets, edges): - target_name = node_name(field, transition_target) - sources.add((source_name, node_label(field, transition_source))) - targets.add((target_name, node_label(field, transition_target))) - edges.add((source_name, target_name, (("label", transition_name),))) - - -def get_graphviz_layouts(): - try: - import graphviz - - return graphviz.backend.ENGINES - except Exception: - return {"sfdp", "circo", "twopi", "dot", "neato", "fdp", "osage", "patchwork"} - - -class Command(BaseCommand): - requires_system_checks = _requires_system_checks - - if not HAS_ARGPARSE: - option_list = BaseCommand.option_list + ( - make_option( - "--output", - "-o", - action="store", - dest="outputfile", - help=( - "Render output file. Type of output dependent on file extensions. " "Use png or jpg to render graph to image." - ), - ), - # NOQA - make_option( - "--layout", - "-l", - action="store", - dest="layout", - default="dot", - help=("Layout to be used by GraphViz for visualization. " "Layouts: %s." % " ".join(get_graphviz_layouts())), - ), - ) - args = "[appname[.model[.field]]]" - else: - - def add_arguments(self, parser): - parser.add_argument( - "--output", - "-o", - action="store", - dest="outputfile", - help=( - "Render output file. Type of output dependent on file extensions. " "Use png or jpg to render graph to image." - ), - ) - parser.add_argument( - "--layout", - "-l", - action="store", - dest="layout", - default="dot", - help=("Layout to be used by GraphViz for visualization. " "Layouts: %s." % " ".join(get_graphviz_layouts())), - ) - parser.add_argument("args", nargs="*", help=("[appname[.model[.field]]]")) - - help = "Creates a GraphViz dot file with transitions for selected fields" - - def render_output(self, graph, **options): - filename, format = options["outputfile"].rsplit(".", 1) - - graph.engine = options["layout"] - graph.format = format - graph.render(filename) - - def handle(self, *args, **options): - fields_data = [] - if len(args) != 0: - for arg in args: - field_spec = arg.split(".") - - if len(field_spec) == 1: - if NEW_META_API: - app = apps.get_app(field_spec[0]) - models = apps.get_models(app) - else: - app = get_app(field_spec[0]) - models = get_models(app) - for model in models: - fields_data += all_fsm_fields_data(model) - elif len(field_spec) == 2: - if NEW_META_API: - model = apps.get_model(field_spec[0], field_spec[1]) - else: - model = get_model(field_spec[0], field_spec[1]) - fields_data += all_fsm_fields_data(model) - elif len(field_spec) == 3: - if NEW_META_API: - model = apps.get_model(field_spec[0], field_spec[1]) - else: - model = get_model(field_spec[0], field_spec[1]) - fields_data += all_fsm_fields_data(model) - else: - if NEW_META_API: - for model in apps.get_models(): - fields_data += all_fsm_fields_data(model) - else: - for app in get_apps(): - for model in get_models(app): - fields_data += all_fsm_fields_data(model) - - dotdata = generate_dot(fields_data) - - if options["outputfile"]: - self.render_output(dotdata, **options) - else: - print(dotdata) diff --git a/src/registrar/migrations/0002_contact_website_domainapplication.py b/src/registrar/migrations/0002_contact_website_domainapplication.py index 2da8d2598..15f586b3c 100644 --- a/src/registrar/migrations/0002_contact_website_domainapplication.py +++ b/src/registrar/migrations/0002_contact_website_domainapplication.py @@ -3,7 +3,7 @@ from django.conf import settings from django.db import migrations, models import django.db.models.deletion -import django_fsm +import django_fsm # type: ignore class Migration(migrations.Migration): diff --git a/src/registrar/models/models.py b/src/registrar/models/models.py index 39f9d0b8a..1f8c2ac86 100644 --- a/src/registrar/models/models.py +++ b/src/registrar/models/models.py @@ -1,10 +1,10 @@ +import re + from django.core.exceptions import ObjectDoesNotExist from django.contrib.auth.models import AbstractUser from django.db import models -from django_fsm import FSMField, transition - -from ..api.views.available import string_could_be_domain +from django_fsm import FSMField, transition # type: ignore class User(AbstractUser): @@ -98,6 +98,20 @@ class Website(models.Model): # enough. website = models.CharField(max_length=255, null=False, help_text="") + # a domain name is alphanumeric or hyphen, up to 63 characters, doesn't + # begin or end with a hyphen, followed by a TLD of 2-6 alphabetic characters + DOMAIN_REGEX = re.compile(r"^(?!-)[A-Za-z0-9-]{1,63}(?