From 1f3a770d2547848590f39e9d9b9bdffeb94eec14 Mon Sep 17 00:00:00 2001 From: Parker Berberian Date: Wed, 10 Oct 2018 16:06:47 -0400 Subject: [PATCH] Lab as a Service 2.0 See changes here: https://wiki.opnfv.org/display/INF/Pharos+Laas Change-Id: I59ada5f98e70a28d7f8c14eab3239597e236ca26 Signed-off-by: Sawyer Bergeron Signed-off-by: Parker Berberian --- Makefile | 4 +- booking_communication_agent.py | 8 + config.env.sample | 5 +- config/nginx/pharos_dashboard.conf | 26 +- data/UNH_IOL/HPE_Proliant/hpe13.yaml | 53 + data/UNH_IOL/HPE_Proliant/hpe14.yaml | 41 + data/UNH_IOL/HPE_Proliant/hpe15.yaml | 41 + data/UNH_IOL/HPE_Proliant/hpe16.yaml | 41 + data/UNH_IOL/HPE_Proliant/hpe17.yaml | 41 + data/UNH_IOL/HPE_Proliant/hpe18.yaml | 41 + data/UNH_IOL/HPE_Proliant/hpe19.yaml | 41 + data/UNH_IOL/HPE_Proliant/hpe20.yaml | 41 + data/UNH_IOL/hostlist.json | 14 + docker-compose.yml | 1 + requirements.txt | 12 +- src/account/admin.py | 5 +- src/account/migrations/0001_initial.py | 33 +- src/account/migrations/0002_auto_20180110_1636.py | 33 - src/account/migrations/0002_lab_description.py | 19 + src/account/migrations/0003_auto_20180110_1639.py | 24 - src/account/migrations/0003_publicnetwork.py | 25 + src/account/migrations/__init__.py | 10 - src/account/models.py | 127 ++ src/account/tasks.py | 4 +- src/account/urls.py | 7 + src/account/views.py | 48 + src/api/admin.py | 28 + src/api/migrations/0001_initial.py | 185 ++ src/api/migrations/__init__.py | 4 +- src/api/models.py | 713 ++++++++ src/api/serializers.py | 53 - src/{jenkins => api/serializers}/__init__.py | 4 +- src/api/serializers/booking_serializer.py | 156 ++ src/api/serializers/old_serializers.py | 28 + src/api/tests/__init__.py | 8 + src/api/tests/test_serializers.py | 213 +++ src/api/urls.py | 13 +- src/api/views.py | 101 +- src/booking/__init__.py | 2 - src/booking/admin.py | 4 +- src/booking/forms.py | 65 - src/booking/migrations/0001_initial.py | 48 +- src/booking/migrations/0002_booking_changeid.py | 38 - src/booking/migrations/0003_auto_20180108_2024.py | 25 - src/booking/migrations/0004_booking_ext_count.py | 27 - src/booking/migrations/__init__.py | 10 - src/booking/models.py | 60 +- src/booking/stats.py | 56 + src/booking/tests/__init__.py | 2 - src/booking/tests/test_models.py | 216 ++- src/booking/tests/test_views.py | 106 -- src/booking/urls.py | 16 +- src/booking/views.py | 138 +- src/dashboard/__init__.py | 2 - src/dashboard/admin.py | 6 +- src/dashboard/context_processors.py | 12 + src/dashboard/exceptions.py | 46 + src/dashboard/migrations/0001_initial.py | 64 - .../migrations/0002_auto_20170505_0815.py | 42 - .../migrations/0003_resource_resource_lab.py | 22 - src/dashboard/models.py | 90 +- src/dashboard/populate_db_iol.py | 346 ++++ src/dashboard/tasks.py | 113 +- src/dashboard/templatetags/jenkins_filters.py | 38 - src/dashboard/tests/test_models.py | 69 - src/dashboard/tests/test_views.py | 75 - src/dashboard/urls.py | 17 +- src/dashboard/views.py | 196 +-- src/jenkins/adapter.py | 137 -- src/jenkins/migrations/0001_initial.py | 53 - src/jenkins/models.py | 62 - src/jenkins/tasks.py | 64 - src/jenkins/tests.py | 129 -- src/manage.py | 0 src/notifier/__init__.py | 8 + src/notifier/admin.py | 4 +- src/notifier/apps.py | 3 +- src/notifier/dispatchers.py | 4 +- src/notifier/manager.py | 98 ++ src/notifier/migrations/0001_initial.py | 25 +- src/notifier/models.py | 33 +- .../admin.py => notifier/tests/test_dispatcher.py} | 14 +- src/notifier/tests/test_models.py | 29 + src/notifier/urls.py | 21 + src/notifier/views.py | 34 + src/pharos_dashboard/settings.py | 52 +- src/pharos_dashboard/urls.py | 11 +- .../migrations => resource_inventory}/__init__.py | 4 +- src/resource_inventory/admin.py | 29 + src/{jenkins => resource_inventory}/apps.py | 5 +- src/resource_inventory/migrations/0001_initial.py | 328 ++++ .../migrations/0002_auto_20180919_1459.py | 18 + .../migrations/0003_vlan_public.py | 18 + src/resource_inventory/migrations/__init__.py | 0 src/resource_inventory/models.py | 299 ++++ src/resource_inventory/resource_manager.py | 197 +++ src/resource_inventory/tests/test_managers.py | 236 +++ src/resource_inventory/tests/test_models.py | 162 ++ src/resource_inventory/urls.py | 34 + src/resource_inventory/views.py | 24 + src/static/css/graph_common.css | 162 ++ src/static/css/theme.css | 6 + src/static/img/mxgraph/add.png | Bin 0 -> 1564 bytes src/static/img/mxgraph/button.gif | Bin 0 -> 137 bytes src/static/img/mxgraph/camera.png | Bin 0 -> 887 bytes src/static/img/mxgraph/check.png | Bin 0 -> 253 bytes src/static/img/mxgraph/close.gif | Bin 0 -> 70 bytes src/static/img/mxgraph/close.png | Bin 0 -> 1910 bytes src/static/img/mxgraph/collapsed.gif | Bin 0 -> 877 bytes src/static/img/mxgraph/connector.gif | Bin 0 -> 954 bytes src/static/img/mxgraph/copy.png | Bin 0 -> 728 bytes src/static/img/mxgraph/cut.png | Bin 0 -> 781 bytes src/static/img/mxgraph/delete2.png | Bin 0 -> 914 bytes src/static/img/mxgraph/dot.gif | Bin 0 -> 517 bytes src/static/img/mxgraph/error.gif | Bin 0 -> 907 bytes src/static/img/mxgraph/expanded.gif | Bin 0 -> 878 bytes src/static/img/mxgraph/export1.png | Bin 0 -> 857 bytes src/static/img/mxgraph/fit_to_size.png | Bin 0 -> 529 bytes src/static/img/mxgraph/gradient_background.jpg | Bin 0 -> 6164 bytes src/static/img/mxgraph/green-dot.gif | Bin 0 -> 326 bytes src/static/img/mxgraph/group.png | Bin 0 -> 899 bytes src/static/img/mxgraph/handle-connect.png | Bin 0 -> 1300 bytes src/static/img/mxgraph/handle-main.png | Bin 0 -> 379 bytes src/static/img/mxgraph/icons48/column.png | Bin 0 -> 1787 bytes src/static/img/mxgraph/icons48/earth.png | Bin 0 -> 4520 bytes src/static/img/mxgraph/icons48/gear.png | Bin 0 -> 4418 bytes src/static/img/mxgraph/icons48/keys.png | Bin 0 -> 4295 bytes src/static/img/mxgraph/icons48/mail_new.png | Bin 0 -> 3944 bytes src/static/img/mxgraph/icons48/server.png | Bin 0 -> 3556 bytes src/static/img/mxgraph/icons48/table.png | Bin 0 -> 1574 bytes src/static/img/mxgraph/key.png | Bin 0 -> 300 bytes src/static/img/mxgraph/loading.gif | Bin 0 -> 7517 bytes src/static/img/mxgraph/maximize.gif | Bin 0 -> 843 bytes src/static/img/mxgraph/minimize.gif | Bin 0 -> 64 bytes src/static/img/mxgraph/navigate_minus.png | Bin 0 -> 485 bytes src/static/img/mxgraph/navigate_plus.png | Bin 0 -> 709 bytes src/static/img/mxgraph/normalize.gif | Bin 0 -> 845 bytes src/static/img/mxgraph/paste.png | Bin 0 -> 783 bytes src/static/img/mxgraph/plus.png | Bin 0 -> 236 bytes src/static/img/mxgraph/point.gif | Bin 0 -> 55 bytes src/static/img/mxgraph/press32.png | Bin 0 -> 2261 bytes src/static/img/mxgraph/print32.png | Bin 0 -> 2111 bytes src/static/img/mxgraph/printer.png | Bin 0 -> 896 bytes src/static/img/mxgraph/redo.png | Bin 0 -> 895 bytes src/static/img/mxgraph/resize.gif | Bin 0 -> 74 bytes src/static/img/mxgraph/separator.gif | Bin 0 -> 146 bytes src/static/img/mxgraph/sidebar_bg.gif | Bin 0 -> 80 bytes src/static/img/mxgraph/spacer.gif | Bin 0 -> 43 bytes src/static/img/mxgraph/submenu.gif | Bin 0 -> 56 bytes src/static/img/mxgraph/toolbar_bg.gif | Bin 0 -> 155 bytes src/static/img/mxgraph/transparent.gif | Bin 0 -> 90 bytes src/static/img/mxgraph/undo.png | Bin 0 -> 879 bytes src/static/img/mxgraph/view_1_1.png | Bin 0 -> 849 bytes src/static/img/mxgraph/view_1_132.png | Bin 0 -> 2199 bytes src/static/img/mxgraph/view_next.png | Bin 0 -> 918 bytes src/static/img/mxgraph/view_previous.png | Bin 0 -> 912 bytes src/static/img/mxgraph/warning.gif | Bin 0 -> 276 bytes src/static/img/mxgraph/warning.png | Bin 0 -> 425 bytes src/static/img/mxgraph/window-title.gif | Bin 0 -> 275 bytes src/static/img/mxgraph/window.gif | Bin 0 -> 75 bytes src/static/img/mxgraph/wires-grid.gif | Bin 0 -> 50 bytes src/static/img/mxgraph/zoom_in.png | Bin 0 -> 858 bytes src/static/img/mxgraph/zoom_in32.png | Bin 0 -> 2184 bytes src/static/img/mxgraph/zoom_out.png | Bin 0 -> 847 bytes src/static/img/mxgraph/zoom_out32.png | Bin 0 -> 2150 bytes src/static/js/mxClient.min.js | 1808 ++++++++++++++++++++ src/templates/account/booking_list.html | 52 + src/templates/account/configuration_list.html | 28 + src/templates/account/details.html | 9 + src/templates/account/image_list.html | 27 + src/templates/account/resource_list.html | 28 + src/templates/account/user_list.html | 2 +- src/templates/base.html | 118 +- src/templates/booking/booking_calendar.html | 65 + src/templates/booking/booking_detail.html | 361 +++- src/templates/booking/booking_grid_item.html | 11 + src/templates/booking/booking_list.html | 6 +- src/templates/booking/stats.html | 60 + src/templates/booking/steps/booking_confirm.html | 25 + src/templates/booking/steps/booking_meta.html | 66 + src/templates/booking/steps/resource_select.html | 73 + src/templates/booking/steps/swconfig_select.html | 73 + .../config_bundle/steps/config_software.html | 72 + .../config_bundle/steps/define_software.html | 102 ++ src/templates/dashboard/ci_pods.html | 61 - src/templates/dashboard/dev_pods.html | 70 - src/templates/dashboard/grid.html | 10 + src/templates/dashboard/host_profile_detail.html | 71 + src/templates/dashboard/jenkins_slaves.html | 46 - src/templates/dashboard/lab_detail.html | 157 ++ src/templates/dashboard/lab_list.html | 87 + src/templates/dashboard/landing.html | 88 + src/templates/dashboard/login.html | 8 + .../dashboard/multiple_select_filter_widget.html | 402 +++++ src/templates/dashboard/pdf.yaml | 95 + src/templates/dashboard/resource.html | 3 +- src/templates/dashboard/resource_all.html | 5 +- src/templates/dashboard/resource_detail.html | 52 - .../dashboard/searchable_select_multiple.html | 408 +++++ src/templates/dashboard/table.html | 1 + src/templates/layout.html | 10 +- src/templates/notifier/inbox.html | 86 + src/templates/notifier/notification.html | 34 + src/templates/resource/hosts.html | 45 + src/templates/resource/mxClient.min.js | 1808 ++++++++++++++++++++ src/templates/resource/steps/define_hardware.html | 37 + src/templates/resource/steps/host_info.html | 43 + src/templates/resource/steps/pod_definition.html | 653 +++++++ src/templates/resource/uncommon.css | 162 ++ src/templates/snapshot_workflow/steps/meta.html | 27 + .../snapshot_workflow/steps/select_host.html | 95 + src/templates/workflow/confirm.html | 125 ++ src/templates/workflow/no_workflow.html | 7 + src/templates/workflow/resource_select.html | 50 + src/templates/workflow/viewport-base.html | 426 +++++ src/templates/workflow/viewport-element.html | 69 + src/workflow/__init__.py | 8 + src/workflow/apps.py | 15 + src/workflow/booking_workflow.py | 295 ++++ src/workflow/forms.py | 446 +++++ src/workflow/models.py | 508 ++++++ src/workflow/resource_bundle_workflow.py | 427 +++++ src/workflow/snapshot_workflow.py | 111 ++ src/workflow/sw_bundle_workflow.py | 238 +++ .../migrations => workflow/tests}/__init__.py | 4 +- src/workflow/tests/constants.py | 198 +++ src/workflow/tests/test_steps.py | 271 +++ src/workflow/tests/test_steps_render.py | 36 + src/workflow/tests/test_workflows.py | 96 ++ src/workflow/urls.py | 34 + src/workflow/views.py | 108 ++ src/workflow/workflow_factory.py | 149 ++ src/workflow/workflow_manager.py | 251 +++ test.sh | 12 + 234 files changed, 15935 insertions(+), 2004 deletions(-) create mode 100644 data/UNH_IOL/HPE_Proliant/hpe13.yaml create mode 100644 data/UNH_IOL/HPE_Proliant/hpe14.yaml create mode 100644 data/UNH_IOL/HPE_Proliant/hpe15.yaml create mode 100644 data/UNH_IOL/HPE_Proliant/hpe16.yaml create mode 100644 data/UNH_IOL/HPE_Proliant/hpe17.yaml create mode 100644 data/UNH_IOL/HPE_Proliant/hpe18.yaml create mode 100644 data/UNH_IOL/HPE_Proliant/hpe19.yaml create mode 100644 data/UNH_IOL/HPE_Proliant/hpe20.yaml create mode 100644 data/UNH_IOL/hostlist.json delete mode 100644 src/account/migrations/0002_auto_20180110_1636.py create mode 100644 src/account/migrations/0002_lab_description.py delete mode 100644 src/account/migrations/0003_auto_20180110_1639.py create mode 100644 src/account/migrations/0003_publicnetwork.py create mode 100644 src/api/admin.py create mode 100644 src/api/migrations/0001_initial.py create mode 100644 src/api/models.py delete mode 100644 src/api/serializers.py rename src/{jenkins => api/serializers}/__init__.py (85%) create mode 100644 src/api/serializers/booking_serializer.py create mode 100644 src/api/serializers/old_serializers.py create mode 100644 src/api/tests/__init__.py create mode 100644 src/api/tests/test_serializers.py delete mode 100644 src/booking/forms.py delete mode 100644 src/booking/migrations/0002_booking_changeid.py delete mode 100644 src/booking/migrations/0003_auto_20180108_2024.py delete mode 100644 src/booking/migrations/0004_booking_ext_count.py create mode 100644 src/booking/stats.py delete mode 100644 src/booking/tests/test_views.py create mode 100644 src/dashboard/context_processors.py create mode 100644 src/dashboard/exceptions.py delete mode 100644 src/dashboard/migrations/0001_initial.py delete mode 100644 src/dashboard/migrations/0002_auto_20170505_0815.py delete mode 100644 src/dashboard/migrations/0003_resource_resource_lab.py create mode 100644 src/dashboard/populate_db_iol.py delete mode 100644 src/dashboard/templatetags/jenkins_filters.py delete mode 100644 src/dashboard/tests/test_models.py delete mode 100644 src/dashboard/tests/test_views.py delete mode 100644 src/jenkins/adapter.py delete mode 100644 src/jenkins/migrations/0001_initial.py delete mode 100644 src/jenkins/models.py delete mode 100644 src/jenkins/tasks.py delete mode 100644 src/jenkins/tests.py mode change 100644 => 100755 src/manage.py create mode 100644 src/notifier/manager.py rename src/{jenkins/admin.py => notifier/tests/test_dispatcher.py} (58%) create mode 100644 src/notifier/tests/test_models.py create mode 100644 src/notifier/urls.py create mode 100644 src/notifier/views.py rename src/{jenkins/migrations => resource_inventory}/__init__.py (85%) create mode 100644 src/resource_inventory/admin.py rename src/{jenkins => resource_inventory}/apps.py (89%) create mode 100644 src/resource_inventory/migrations/0001_initial.py create mode 100644 src/resource_inventory/migrations/0002_auto_20180919_1459.py create mode 100644 src/resource_inventory/migrations/0003_vlan_public.py create mode 100644 src/resource_inventory/migrations/__init__.py create mode 100644 src/resource_inventory/models.py create mode 100644 src/resource_inventory/resource_manager.py create mode 100644 src/resource_inventory/tests/test_managers.py create mode 100644 src/resource_inventory/tests/test_models.py create mode 100644 src/resource_inventory/urls.py create mode 100644 src/resource_inventory/views.py create mode 100644 src/static/css/graph_common.css create mode 100644 src/static/img/mxgraph/add.png create mode 100644 src/static/img/mxgraph/button.gif create mode 100644 src/static/img/mxgraph/camera.png create mode 100644 src/static/img/mxgraph/check.png create mode 100644 src/static/img/mxgraph/close.gif create mode 100644 src/static/img/mxgraph/close.png create mode 100644 src/static/img/mxgraph/collapsed.gif create mode 100644 src/static/img/mxgraph/connector.gif create mode 100644 src/static/img/mxgraph/copy.png create mode 100644 src/static/img/mxgraph/cut.png create mode 100644 src/static/img/mxgraph/delete2.png create mode 100644 src/static/img/mxgraph/dot.gif create mode 100644 src/static/img/mxgraph/error.gif create mode 100644 src/static/img/mxgraph/expanded.gif create mode 100644 src/static/img/mxgraph/export1.png create mode 100644 src/static/img/mxgraph/fit_to_size.png create mode 100644 src/static/img/mxgraph/gradient_background.jpg create mode 100644 src/static/img/mxgraph/green-dot.gif create mode 100644 src/static/img/mxgraph/group.png create mode 100644 src/static/img/mxgraph/handle-connect.png create mode 100644 src/static/img/mxgraph/handle-main.png create mode 100644 src/static/img/mxgraph/icons48/column.png create mode 100644 src/static/img/mxgraph/icons48/earth.png create mode 100644 src/static/img/mxgraph/icons48/gear.png create mode 100644 src/static/img/mxgraph/icons48/keys.png create mode 100644 src/static/img/mxgraph/icons48/mail_new.png create mode 100644 src/static/img/mxgraph/icons48/server.png create mode 100644 src/static/img/mxgraph/icons48/table.png create mode 100644 src/static/img/mxgraph/key.png create mode 100644 src/static/img/mxgraph/loading.gif create mode 100644 src/static/img/mxgraph/maximize.gif create mode 100644 src/static/img/mxgraph/minimize.gif create mode 100644 src/static/img/mxgraph/navigate_minus.png create mode 100644 src/static/img/mxgraph/navigate_plus.png create mode 100644 src/static/img/mxgraph/normalize.gif create mode 100644 src/static/img/mxgraph/paste.png create mode 100644 src/static/img/mxgraph/plus.png create mode 100644 src/static/img/mxgraph/point.gif create mode 100644 src/static/img/mxgraph/press32.png create mode 100644 src/static/img/mxgraph/print32.png create mode 100644 src/static/img/mxgraph/printer.png create mode 100644 src/static/img/mxgraph/redo.png create mode 100644 src/static/img/mxgraph/resize.gif create mode 100644 src/static/img/mxgraph/separator.gif create mode 100644 src/static/img/mxgraph/sidebar_bg.gif create mode 100644 src/static/img/mxgraph/spacer.gif create mode 100644 src/static/img/mxgraph/submenu.gif create mode 100644 src/static/img/mxgraph/toolbar_bg.gif create mode 100644 src/static/img/mxgraph/transparent.gif create mode 100644 src/static/img/mxgraph/undo.png create mode 100644 src/static/img/mxgraph/view_1_1.png create mode 100644 src/static/img/mxgraph/view_1_132.png create mode 100644 src/static/img/mxgraph/view_next.png create mode 100644 src/static/img/mxgraph/view_previous.png create mode 100644 src/static/img/mxgraph/warning.gif create mode 100644 src/static/img/mxgraph/warning.png create mode 100644 src/static/img/mxgraph/window-title.gif create mode 100644 src/static/img/mxgraph/window.gif create mode 100644 src/static/img/mxgraph/wires-grid.gif create mode 100644 src/static/img/mxgraph/zoom_in.png create mode 100644 src/static/img/mxgraph/zoom_in32.png create mode 100644 src/static/img/mxgraph/zoom_out.png create mode 100644 src/static/img/mxgraph/zoom_out32.png create mode 100644 src/static/js/mxClient.min.js create mode 100644 src/templates/account/booking_list.html create mode 100644 src/templates/account/configuration_list.html create mode 100644 src/templates/account/details.html create mode 100644 src/templates/account/image_list.html create mode 100644 src/templates/account/resource_list.html create mode 100644 src/templates/booking/booking_grid_item.html create mode 100644 src/templates/booking/stats.html create mode 100644 src/templates/booking/steps/booking_confirm.html create mode 100644 src/templates/booking/steps/booking_meta.html create mode 100644 src/templates/booking/steps/resource_select.html create mode 100644 src/templates/booking/steps/swconfig_select.html create mode 100644 src/templates/config_bundle/steps/config_software.html create mode 100644 src/templates/config_bundle/steps/define_software.html delete mode 100644 src/templates/dashboard/ci_pods.html delete mode 100644 src/templates/dashboard/dev_pods.html create mode 100644 src/templates/dashboard/grid.html create mode 100644 src/templates/dashboard/host_profile_detail.html delete mode 100644 src/templates/dashboard/jenkins_slaves.html create mode 100644 src/templates/dashboard/lab_detail.html create mode 100644 src/templates/dashboard/lab_list.html create mode 100644 src/templates/dashboard/landing.html create mode 100644 src/templates/dashboard/login.html create mode 100644 src/templates/dashboard/multiple_select_filter_widget.html create mode 100644 src/templates/dashboard/pdf.yaml create mode 100644 src/templates/dashboard/searchable_select_multiple.html create mode 100644 src/templates/notifier/inbox.html create mode 100644 src/templates/notifier/notification.html create mode 100644 src/templates/resource/hosts.html create mode 100644 src/templates/resource/mxClient.min.js create mode 100644 src/templates/resource/steps/define_hardware.html create mode 100644 src/templates/resource/steps/host_info.html create mode 100644 src/templates/resource/steps/pod_definition.html create mode 100644 src/templates/resource/uncommon.css create mode 100644 src/templates/snapshot_workflow/steps/meta.html create mode 100644 src/templates/snapshot_workflow/steps/select_host.html create mode 100644 src/templates/workflow/confirm.html create mode 100644 src/templates/workflow/no_workflow.html create mode 100644 src/templates/workflow/resource_select.html create mode 100644 src/templates/workflow/viewport-base.html create mode 100644 src/templates/workflow/viewport-element.html create mode 100644 src/workflow/__init__.py create mode 100644 src/workflow/apps.py create mode 100644 src/workflow/booking_workflow.py create mode 100644 src/workflow/forms.py create mode 100644 src/workflow/models.py create mode 100644 src/workflow/resource_bundle_workflow.py create mode 100644 src/workflow/snapshot_workflow.py create mode 100644 src/workflow/sw_bundle_workflow.py rename src/{dashboard/migrations => workflow/tests}/__init__.py (85%) create mode 100644 src/workflow/tests/constants.py create mode 100644 src/workflow/tests/test_steps.py create mode 100644 src/workflow/tests/test_steps_render.py create mode 100644 src/workflow/tests/test_workflows.py create mode 100644 src/workflow/urls.py create mode 100644 src/workflow/views.py create mode 100644 src/workflow/workflow_factory.py create mode 100644 src/workflow/workflow_manager.py create mode 100755 test.sh diff --git a/Makefile b/Makefile index e5f4fb2..31243b0 100644 --- a/Makefile +++ b/Makefile @@ -32,10 +32,10 @@ shell-db: docker exec -ti ps01 bash log-nginx: - docker-compose logs nginx + docker-compose logs nginx log-web: - docker-compose logs web + docker-compose logs web log-ps: docker-compose logs postgres diff --git a/booking_communication_agent.py b/booking_communication_agent.py index c52e98b..dc4ea4d 100644 --- a/booking_communication_agent.py +++ b/booking_communication_agent.py @@ -1,3 +1,11 @@ +############################################################################## +# Copyright (c) 2018 Sawyer Bergeron and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## from dashboard_notification.notification import Notification from dashboard_api.api import DashboardAPI diff --git a/config.env.sample b/config.env.sample index 5072779..7677c2f 100644 --- a/config.env.sample +++ b/config.env.sample @@ -2,6 +2,7 @@ DASHBOARD_URL=http://labs.opnfv.org # SECURITY WARNING: don't run with debug turned on in production! DEBUG=False +TEST=False POSTGRES_DB=sample_name POSTGRES_USER=sample_user @@ -26,10 +27,6 @@ JIRA_USER_PASSWORD=sample_jira_pass RABBITMQ_DEFAULT_USER=opnfv RABBITMQ_DEFAULT_PASS=opnfvopnfv -# Cleanup: time is in days -BOOKING_EXPIRE_TIME=30 -BOOKING_MAXIMUM_NUMBER=10 - #Jenkins Build Server JENKINS_URL=https://build.opnfv.org/ci diff --git a/config/nginx/pharos_dashboard.conf b/config/nginx/pharos_dashboard.conf index 87b6f8e..6f32979 100644 --- a/config/nginx/pharos_dashboard.conf +++ b/config/nginx/pharos_dashboard.conf @@ -1,24 +1,24 @@ upstream web { - ip_hash; - server web:8000; + ip_hash; + server web:8000; } # portal -server { - listen 80; - server_name localhost; - charset utf-8; +server { + listen 80; + server_name localhost; + charset utf-8; - location /static { + location /static { alias /static; - } + } - location /media { + location /media { alias /media; - } + } - location / { + location / { proxy_set_header Host $host; - proxy_pass http://web/; - } + proxy_pass http://web/; + } } diff --git a/data/UNH_IOL/HPE_Proliant/hpe13.yaml b/data/UNH_IOL/HPE_Proliant/hpe13.yaml new file mode 100644 index 0000000..fd40aff --- /dev/null +++ b/data/UNH_IOL/HPE_Proliant/hpe13.yaml @@ -0,0 +1,53 @@ +--- +disk: + - + name: sda + size: 894.2G + - + name: loop0 + size: 100G + - + name: loop1 + size: 2G + - + name: loop2 + size: 792M + - + name: loop3 + size: 825M +cpu: + arch: x86_64 + cpus: 2 + cores: 88 +memory: 503G +interface: + - + name: eno49 + mac: '48:df:37:1d:54:00' + busaddr: '0000:04:00.0' + speed: 10000 + - + name: ens1f0 + mac: '3c:fd:fe:b2:c3:70' + busaddr: '0000:05:00.0' + speed: 10000 + - + name: ens1f1 + mac: '3c:fd:fe:b2:c3:71' + busaddr: '0000:05:00.1' + speed: 10000 + - + name: ens1f2 + mac: '3c:fd:fe:b2:c3:72' + busaddr: '0000:05:00.2' + speed: 10000 + - + name: ens4f0 + mac: '3c:fd:fe:b2:ae:d0' + busaddr: '0000:88:00.0' + speed: 10000 + - + name: ens4f1 + mac: '3c:fd:fe:b2:ae:d1' + busaddr: '0000:88:00.1' + speed: 10000 diff --git a/data/UNH_IOL/HPE_Proliant/hpe14.yaml b/data/UNH_IOL/HPE_Proliant/hpe14.yaml new file mode 100644 index 0000000..612f4d0 --- /dev/null +++ b/data/UNH_IOL/HPE_Proliant/hpe14.yaml @@ -0,0 +1,41 @@ +--- +disk: + - + name: sda + size: 894.2G +cpu: + arch: x86_64 + cpus: 2 + cores: 88 +memory: 503G +interface: + - + name: eno49 + mac: '48:df:37:1d:4d:60' + busaddr: '0000:04:00.0' + speed: 10000 + - + name: ens1f0 + mac: '3c:fd:fe:b2:b2:00' + busaddr: '0000:05:00.0' + speed: 10000 + - + name: ens1f1 + mac: '3c:fd:fe:b2:b2:01' + busaddr: '0000:05:00.1' + speed: 10000 + - + name: ens1f2 + mac: '3c:fd:fe:b2:b2:02' + busaddr: '0000:05:00.2' + speed: 10000 + - + name: ens4f0 + mac: '3c:fd:fe:b2:b1:d8' + busaddr: '0000:88:00.0' + speed: 10000 + - + name: ens4f1 + mac: '3c:fd:fe:b2:b1:d9' + busaddr: '0000:88:00.1' + speed: 10000 diff --git a/data/UNH_IOL/HPE_Proliant/hpe15.yaml b/data/UNH_IOL/HPE_Proliant/hpe15.yaml new file mode 100644 index 0000000..c6799ac --- /dev/null +++ b/data/UNH_IOL/HPE_Proliant/hpe15.yaml @@ -0,0 +1,41 @@ +--- +disk: + - + name: sda + size: 894.2G +cpu: + arch: x86_64 + cpus: 2 + cores: 88 +memory: 503G +interface: + - + name: eno49 + mac: '48:df:37:22:c5:c0' + busaddr: '0000:04:00.0' + speed: 10000 + - + name: ens1f0 + mac: '3c:fd:fe:b2:ae:68' + busaddr: '0000:05:00.0' + speed: 10000 + - + name: ens1f1 + mac: '3c:fd:fe:b2:ae:69' + busaddr: '0000:05:00.1' + speed: 10000 + - + name: ens1f2 + mac: '3c:fd:fe:b2:ae:6a' + busaddr: '0000:05:00.2' + speed: 10000 + - + name: ens4f0 + mac: '3c:fd:fe:b2:ad:90' + busaddr: '0000:88:00.0' + speed: 10000 + - + name: ens4f1 + mac: '3c:fd:fe:b2:ad:91' + busaddr: '0000:88:00.1' + speed: 10000 diff --git a/data/UNH_IOL/HPE_Proliant/hpe16.yaml b/data/UNH_IOL/HPE_Proliant/hpe16.yaml new file mode 100644 index 0000000..3148532 --- /dev/null +++ b/data/UNH_IOL/HPE_Proliant/hpe16.yaml @@ -0,0 +1,41 @@ +--- +disk: + - + name: sda + size: 894.2G +cpu: + arch: x86_64 + cpus: 2 + cores: 88 +memory: 503G +interface: + - + name: eth0 + mac: '48:df:37:22:c5:30' + busaddr: '0000:04:00.0' + speed: 10000 + - + name: eth2 + mac: '3c:fd:fe:b2:b1:60' + busaddr: '0000:05:00.0' + speed: 10000 + - + name: eth3 + mac: '3c:fd:fe:b2:b1:61' + busaddr: '0000:05:00.1' + speed: 10000 + - + name: eth4 + mac: '3c:fd:fe:b2:b1:62' + busaddr: '0000:05:00.2' + speed: 10000 + - + name: eth6 + mac: '3c:fd:fe:b2:b3:b8' + busaddr: '0000:88:00.0' + speed: 10000 + - + name: eth7 + mac: '3c:fd:fe:b2:b3:b9' + busaddr: '0000:88:00.1' + speed: 10000 diff --git a/data/UNH_IOL/HPE_Proliant/hpe17.yaml b/data/UNH_IOL/HPE_Proliant/hpe17.yaml new file mode 100644 index 0000000..4b13ae2 --- /dev/null +++ b/data/UNH_IOL/HPE_Proliant/hpe17.yaml @@ -0,0 +1,41 @@ +--- +disk: + - + name: sda + size: 894.2G +cpu: + arch: x86_64 + cpus: 2 + cores: 88 +memory: 503G +interface: + - + name: eno49 + mac: '48:df:37:1d:48:e0' + busaddr: '0000:04:00.0' + speed: 10000 + - + name: ens1f0 + mac: '3c:fd:fe:b2:ad:80' + busaddr: '0000:05:00.0' + speed: 10000 + - + name: ens1f1 + mac: '3c:fd:fe:b2:ad:81' + busaddr: '0000:05:00.1' + speed: 10000 + - + name: ens1f2 + mac: '3c:fd:fe:b2:ad:82' + busaddr: '0000:05:00.2' + speed: 10000 + - + name: ens4f0 + mac: '3c:fd:fe:b2:b2:d0' + busaddr: '0000:88:00.0' + speed: 10000 + - + name: ens4f1 + mac: '3c:fd:fe:b2:b2:d1' + busaddr: '0000:88:00.1' + speed: 10000 diff --git a/data/UNH_IOL/HPE_Proliant/hpe18.yaml b/data/UNH_IOL/HPE_Proliant/hpe18.yaml new file mode 100644 index 0000000..aa411f1 --- /dev/null +++ b/data/UNH_IOL/HPE_Proliant/hpe18.yaml @@ -0,0 +1,41 @@ +--- +disk: + - + name: sda + size: 894.2G +cpu: + arch: x86_64 + cpus: 2 + cores: 88 +memory: 503G +interface: + - + name: eno49 + mac: '48:df:37:19:a9:b0' + busaddr: '0000:04:00.0' + speed: 10000 + - + name: ens1f0 + mac: '3c:fd:fe:b2:ae:b8' + busaddr: '0000:05:00.0' + speed: 10000 + - + name: ens1f1 + mac: '3c:fd:fe:b2:ae:b9' + busaddr: '0000:05:00.1' + speed: 10000 + - + name: ens1f2 + mac: '3c:fd:fe:b2:ae:ba' + busaddr: '0000:05:00.2' + speed: 10000 + - + name: ens4f0 + mac: '3c:fd:fe:b2:af:50' + busaddr: '0000:88:00.0' + speed: 10000 + - + name: ens4f1 + mac: '3c:fd:fe:b2:af:51' + busaddr: '0000:88:00.1' + speed: 10000 diff --git a/data/UNH_IOL/HPE_Proliant/hpe19.yaml b/data/UNH_IOL/HPE_Proliant/hpe19.yaml new file mode 100644 index 0000000..40e538c --- /dev/null +++ b/data/UNH_IOL/HPE_Proliant/hpe19.yaml @@ -0,0 +1,41 @@ +--- +disk: + - + name: sda + size: 894.2G +cpu: + arch: x86_64 + cpus: 2 + cores: 88 +memory: 503G +interface: + - + name: eno49 + mac: '48:df:37:22:b5:90' + busaddr: '0000:04:00.0' + speed: 10000 + - + name: ens1f0 + mac: '3c:fd:fe:b2:ad:20' + busaddr: '0000:05:00.0' + speed: 10000 + - + name: ens1f1 + mac: '3c:fd:fe:b2:ad:21' + busaddr: '0000:05:00.1' + speed: 10000 + - + name: ens1f2 + mac: '3c:fd:fe:b2:ad:22' + busaddr: '0000:05:00.2' + speed: 10000 + - + name: ens4f0 + mac: '3c:fd:fe:b2:b3:10' + busaddr: '0000:88:00.0' + speed: 10000 + - + name: ens4f1 + mac: '3c:fd:fe:b2:b3:11' + busaddr: '0000:88:00.1' + speed: 10000 diff --git a/data/UNH_IOL/HPE_Proliant/hpe20.yaml b/data/UNH_IOL/HPE_Proliant/hpe20.yaml new file mode 100644 index 0000000..6c83e79 --- /dev/null +++ b/data/UNH_IOL/HPE_Proliant/hpe20.yaml @@ -0,0 +1,41 @@ +--- +disk: + - + name: sda + size: 894.2G +cpu: + arch: x86_64 + cpus: 2 + cores: 88 +memory: 503G +interface: + - + name: eno49 + mac: '48:df:37:1d:49:a0' + busaddr: '0000:04:00.0' + speed: 10000 + - + name: ens1f0 + mac: '3c:fd:fe:b2:b1:c8' + busaddr: '0000:05:00.0' + speed: 10000 + - + name: ens1f1 + mac: '3c:fd:fe:b2:b1:c9' + busaddr: '0000:05:00.1' + speed: 10000 + - + name: ens1f2 + mac: '3c:fd:fe:b2:b1:ca' + busaddr: '0000:05:00.2' + speed: 10000 + - + name: ens4f0 + mac: '3c:fd:fe:b2:ae:a0' + busaddr: '0000:88:00.0' + speed: 10000 + - + name: ens4f1 + mac: '3c:fd:fe:b2:ae:a1' + busaddr: '0000:88:00.1' + speed: 10000 diff --git a/data/UNH_IOL/hostlist.json b/data/UNH_IOL/hostlist.json new file mode 100644 index 0000000..cd03859 --- /dev/null +++ b/data/UNH_IOL/hostlist.json @@ -0,0 +1,14 @@ +{ + "profiles": { + "HPE_Proliant": [ + "hpe13", + "hpe14", + "hpe15", + "hpe16", + "hpe17", + "hpe18", + "hpe19", + "hpe20" + ] + } +} diff --git a/docker-compose.yml b/docker-compose.yml index 19e9afd..2f96343 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,7 @@ --- ############################################################################## # Copyright (c) 2016 Max Breitenfeldt and others. +# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others. # # All rights reserved. This program and the accompanying materials # are made available under the terms of the Apache License, Version 2.0 diff --git a/requirements.txt b/requirements.txt index 19365bc..9ea10a4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,11 @@ celery==3.1.23 cryptography==2.3.1 -Django==1.10 -django-bootstrap3==7.0.1 -django-crispy-forms==1.6.0 -django-filter==0.14.0 +Django==2.1 +django-bootstrap3==10.0.1 +django-crispy-forms==1.7.2 +django-filter==2.0.0 django-registration==2.1.2 -djangorestframework==3.4.6 +djangorestframework==3.8.2 gunicorn==19.6.0 jira==1.0.7 jsonpickle==0.9.3 @@ -16,3 +16,5 @@ psycopg2==2.6.2 PyJWT==1.4.2 requests==2.11.0 django-fernet-fields==0.5 +pyyaml==3.13 +pytz==2018.5 diff --git a/src/account/admin.py b/src/account/admin.py index 6f77122..b4c142c 100644 --- a/src/account/admin.py +++ b/src/account/admin.py @@ -1,5 +1,6 @@ ############################################################################## # Copyright (c) 2016 Max Breitenfeldt and others. +# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others. # # All rights reserved. This program and the accompanying materials # are made available under the terms of the Apache License, Version 2.0 @@ -10,7 +11,9 @@ from django.contrib import admin -from account.models import UserProfile, Lab +from account.models import UserProfile, Lab, VlanManager, PublicNetwork admin.site.register(UserProfile) admin.site.register(Lab) +admin.site.register(VlanManager) +admin.site.register(PublicNetwork) diff --git a/src/account/migrations/0001_initial.py b/src/account/migrations/0001_initial.py index 591f702..c8b5bdc 100644 --- a/src/account/migrations/0001_initial.py +++ b/src/account/migrations/0001_initial.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10 on 2016-11-03 13:33 -from __future__ import unicode_literals +# Generated by Django 2.1 on 2018-09-14 14:48 import account.models from django.conf import settings @@ -17,6 +15,18 @@ class Migration(migrations.Migration): ] operations = [ + migrations.CreateModel( + name='Lab', + fields=[ + ('name', models.CharField(max_length=200, primary_key=True, serialize=False, unique=True)), + ('contact_email', models.EmailField(blank=True, max_length=200, null=True)), + ('contact_phone', models.CharField(blank=True, max_length=20, null=True)), + ('status', models.IntegerField(default=0)), + ('location', models.TextField(default='unknown')), + ('api_token', models.CharField(max_length=50)), + ('lab_user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), migrations.CreateModel( name='UserProfile', fields=[ @@ -24,15 +34,32 @@ class Migration(migrations.Migration): ('timezone', models.CharField(default='UTC', max_length=100)), ('ssh_public_key', models.FileField(blank=True, null=True, upload_to=account.models.upload_to)), ('pgp_public_key', models.FileField(blank=True, null=True, upload_to=account.models.upload_to)), + ('email_addr', models.CharField(default='email@mail.com', max_length=300)), ('company', models.CharField(max_length=200)), ('oauth_token', models.CharField(max_length=1024)), ('oauth_secret', models.CharField(max_length=1024)), ('jira_url', models.CharField(default='', max_length=100)), ('full_name', models.CharField(default='', max_length=100)), + ('booking_privledge', models.BooleanField(default=False)), ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], options={ 'db_table': 'user_profile', }, ), + migrations.CreateModel( + name='VlanManager', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('vlans', models.TextField()), + ('block_size', models.IntegerField()), + ('allow_overlapping', models.BooleanField()), + ('reserved_vlans', models.TextField()), + ], + ), + migrations.AddField( + model_name='lab', + name='vlan_manager', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='account.VlanManager'), + ), ] diff --git a/src/account/migrations/0002_auto_20180110_1636.py b/src/account/migrations/0002_auto_20180110_1636.py deleted file mode 100644 index 6170acb..0000000 --- a/src/account/migrations/0002_auto_20180110_1636.py +++ /dev/null @@ -1,33 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10 on 2018-01-10 16:36 -from __future__ import unicode_literals - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('account', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='Lab', - fields=[ - ('id', models.CharField(max_length=200, primary_key=True, serialize=False)), - ('name', models.CharField(max_length=200, unique=True)), - ('contact_email', models.EmailField(blank=True, max_length=200, null=True)), - ('contact_phone', models.CharField(blank=True, max_length=20, null=True)), - ('lab_user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - ), - migrations.AddField( - model_name='userprofile', - name='email_addr', - field=models.CharField(default='email@mail.com', max_length=300), - ), - ] diff --git a/src/account/migrations/0002_lab_description.py b/src/account/migrations/0002_lab_description.py new file mode 100644 index 0000000..445501a --- /dev/null +++ b/src/account/migrations/0002_lab_description.py @@ -0,0 +1,19 @@ +# Generated by Django 2.1 on 2018-09-14 20:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='lab', + name='description', + field=models.CharField(default='Lab description default', max_length=240), + preserve_default=False, + ), + ] diff --git a/src/account/migrations/0003_auto_20180110_1639.py b/src/account/migrations/0003_auto_20180110_1639.py deleted file mode 100644 index d0bc4d6..0000000 --- a/src/account/migrations/0003_auto_20180110_1639.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10 on 2018-01-10 16:39 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('account', '0002_auto_20180110_1636'), - ] - - operations = [ - migrations.RemoveField( - model_name='lab', - name='id', - ), - migrations.AlterField( - model_name='lab', - name='name', - field=models.CharField(max_length=200, primary_key=True, serialize=False, unique=True), - ), - ] diff --git a/src/account/migrations/0003_publicnetwork.py b/src/account/migrations/0003_publicnetwork.py new file mode 100644 index 0000000..71e5caa --- /dev/null +++ b/src/account/migrations/0003_publicnetwork.py @@ -0,0 +1,25 @@ +# Generated by Django 2.1 on 2018-09-26 14:41 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0002_lab_description'), + ] + + operations = [ + migrations.CreateModel( + name='PublicNetwork', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('vlan', models.IntegerField()), + ('in_use', models.BooleanField(default=False)), + ('cidr', models.CharField(default='0.0.0.0/0', max_length=50)), + ('gateway', models.CharField(default='0.0.0.0', max_length=50)), + ('lab', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='account.Lab')), + ], + ), + ] diff --git a/src/account/migrations/__init__.py b/src/account/migrations/__init__.py index b5914ce..e69de29 100644 --- a/src/account/migrations/__init__.py +++ b/src/account/migrations/__init__.py @@ -1,10 +0,0 @@ -############################################################################## -# Copyright (c) 2016 Max Breitenfeldt and others. -# -# All rights reserved. This program and the accompanying materials -# are made available under the terms of the Apache License, Version 2.0 -# which accompanies this distribution, and is available at -# http://www.apache.org/licenses/LICENSE-2.0 -############################################################################## - - diff --git a/src/account/models.py b/src/account/models.py index aad4c50..18a8cbb 100644 --- a/src/account/models.py +++ b/src/account/models.py @@ -10,6 +10,14 @@ from django.contrib.auth.models import User from django.db import models +import json +import random + + +class LabStatus(object): + UP = 0 + TEMP_DOWN = 100 + DOWN = 200 def upload_to(object, filename): @@ -28,6 +36,7 @@ class UserProfile(models.Model): jira_url = models.CharField(max_length=100, default='') full_name = models.CharField(max_length=100, default='') + booking_privledge = models.BooleanField(default=False) class Meta: db_table = 'user_profile' @@ -35,11 +44,129 @@ class UserProfile(models.Model): def __str__(self): return self.user.username + +class VlanManager(models.Model): + # list of length 4096 containing either 0 (not available) or 1 (available) + vlans = models.TextField() + block_size = models.IntegerField() + allow_overlapping = models.BooleanField() + # list of length 4096 containing either 0 (not rexerved) or 1 (reserved) + reserved_vlans = models.TextField() + + def get_vlan(self, count=1): + allocated = [] + vlans = json.loads(self.vlans) + for i in range(count): + new_vlan = vlans.index(1) # will throw if none available + vlans[new_vlan] = 0 + allocated.append(new_vlan) + if count is 1: + return allocated[0] + return allocated + + def get_public_vlan(self): + return PublicNetwork.objects.filter(lab=self.lab_set.first(), in_use=False).first() + + def reserve_public_vlan(self, vlan): + net = PublicNetwork.objects.get(lab=self.lab_set.first(), vlan=vlan, in_use=False) + net.in_use = True + net.save() + + def release_public_vlan(self, vlan): + net = PublicNetwork.objects.get(lab=self.lab_set.first(), vlan=vlan, in_use=True) + net.in_use = False + net.save() + + def public_vlan_is_available(self, vlan): + net = PublicNetwork.objects.get(lab=self.lab_set.first(), vlan=vlan) + return not net.in_use + + + def is_available(self, vlans): + """ + 'vlans' is either a single vlan id integer or a list of integers + will return true (available) or false + """ + if self.allow_overlapping: + return True + + reserved = json.loads(self.reserved_vlans) + vlan_master_list = json.loads(self.vlans) + try: + iter(vlans) + except: + vlans = [vlans] + + for vlan in vlans: + if not vlan_master_list[vlan] or reserved[vlan]: + return False + return True + + def release_vlans(self, vlans): + """ + 'vlans' is either a single vlan id integer or a list of integers + will make the vlans available + doesnt return a value + """ + my_vlans = json.loads(self.vlans) + + try: + iter(vlans) + except: + vlans = [vlans] + + for vlan in vlans: + my_vlans[vlan] = 1 + self.vlans = json.dumps(my_vlans) + self.save() + + def reserve_vlans(self, vlans): + my_vlans = json.loads(self.vlans) + + try: + iter(vlans) + except: + vlans = [vlans] + + vlans = set(vlans) + + for vlan in vlans: + if my_vlans[vlan] is 0: + raise ValueError("vlan " + str(vlan) + " is not available") + + my_vlans[vlan] = 0 + self.vlans = json.dumps(my_vlans) + self.save() + + + class Lab(models.Model): lab_user = models.OneToOneField(User, on_delete=models.CASCADE) name = models.CharField(max_length=200, primary_key=True, unique=True, null=False, blank=False) contact_email = models.EmailField(max_length=200, null=True, blank=True) contact_phone = models.CharField(max_length=20, null=True, blank=True) + status = models.IntegerField(default=LabStatus.UP) + vlan_manager = models.ForeignKey(VlanManager, on_delete=models.CASCADE, null=True) + location = models.TextField(default="unknown") + api_token = models.CharField(max_length=50) + description = models.CharField(max_length=240) + + @staticmethod + def make_api_token(): + alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + key = "" + for i in range(45): + key += random.choice(alphabet) + return key + def __str__(self): return self.name + + +class PublicNetwork(models.Model): + vlan = models.IntegerField() + lab = models.ForeignKey(Lab, on_delete=models.CASCADE) + in_use = models.BooleanField(default=False) + cidr = models.CharField(max_length=50, default="0.0.0.0/0") + gateway = models.CharField(max_length=50, default="0.0.0.0") diff --git a/src/account/tasks.py b/src/account/tasks.py index bfb865d..fe51974 100644 --- a/src/account/tasks.py +++ b/src/account/tasks.py @@ -1,5 +1,6 @@ ############################################################################## # Copyright (c) 2016 Max Breitenfeldt and others. +# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others. # # All rights reserved. This program and the accompanying materials # are made available under the terms of the Apache License, Version 2.0 @@ -28,7 +29,6 @@ def sync_jira_accounts(): user.email = user_dict['emailAddress'] user.userprofile.url = user_dict['self'] user.userprofile.full_name = user_dict['displayName'] - print(user_dict) user.userprofile.save() - user.save() \ No newline at end of file + user.save() diff --git a/src/account/urls.py b/src/account/urls.py index 3962a0c..6ce2115 100644 --- a/src/account/urls.py +++ b/src/account/urls.py @@ -1,5 +1,6 @@ ############################################################################## # Copyright (c) 2016 Max Breitenfeldt and others. +# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others. # # All rights reserved. This program and the accompanying materials # are made available under the terms of the Apache License, Version 2.0 @@ -27,10 +28,16 @@ from django.conf.urls import url from account.views import * +app_name = "account" urlpatterns = [ url(r'^settings/', AccountSettingsView.as_view(), name='settings'), url(r'^authenticated/$', JiraAuthenticatedView.as_view(), name='authenticated'), url(r'^login/$', JiraLoginView.as_view(), name='login'), url(r'^logout/$', JiraLogoutView.as_view(), name='logout'), url(r'^users/$', UserListView.as_view(), name='users'), + url(r'^my/resources', account_resource_view, name="my-resources"), + url(r'^my/bookings', account_booking_view, name="my-bookings"), + url(r'^my/images', account_images_view, name="my-images"), + url(r'^my/configurations', account_configuration_view, name="my-configurations"), + url(r'^my/$', account_detail_view, name="my-account"), ] diff --git a/src/account/views.py b/src/account/views.py index e6a0e5d..04d21b8 100644 --- a/src/account/views.py +++ b/src/account/views.py @@ -1,5 +1,6 @@ ############################################################################## # Copyright (c) 2016 Max Breitenfeldt and others. +# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others. # # All rights reserved. This program and the accompanying materials # are made available under the terms of the Apache License, Version 2.0 @@ -21,12 +22,15 @@ from django.contrib.auth.models import User from django.urls import reverse from django.utils.decorators import method_decorator from django.views.generic import RedirectView, TemplateView, UpdateView +from django.shortcuts import render from jira import JIRA from rest_framework.authtoken.models import Token from account.forms import AccountSettingsForm from account.jira_util import SignatureMethod_RSA_SHA1 from account.models import UserProfile +from booking.models import Booking +from resource_inventory.models import GenericResourceBundle, ConfigBundle, Image @method_decorator(login_required, name='dispatch') @@ -153,3 +157,47 @@ class UserListView(TemplateView): context = super(UserListView, self).get_context_data(**kwargs) context.update({'title': "Dashboard Users", 'users': users}) return context + + +def account_detail_view(request): + template = "account/details.html" + return render(request, template) + +def account_resource_view(request): + """ + gathers a users genericResoureBundles and + turns them into displayable objects + """ + if not request.user.is_authenticated: + return render(request, "dashboard/login.html", {'title': 'Authentication Required'}) + template = "account/resource_list.html" + resources = list(GenericResourceBundle.objects.filter(owner=request.user)) + context = {"resources": resources, "title": "My Resources"} + return render(request, template, context=context) + +def account_booking_view(request): + if not request.user.is_authenticated: + return render(request, "dashboard/login.html", {'title': 'Authentication Required'}) + template = "account/booking_list.html" + bookings = list(Booking.objects.filter(owner=request.user)) + collab_bookings = list(request.user.collaborators.all()) + context = {"title": "My Bookings", "bookings": bookings, "collab_bookings": collab_bookings} + return render(request, template, context=context) + +def account_configuration_view(request): + if not request.user.is_authenticated: + return render(request, "dashboard/login.html", {'title': 'Authentication Required'}) + template = "account/configuration_list.html" + configs = list(ConfigBundle.objects.filter(owner=request.user)) + context = {"title": "Configuration List", "configurations": configs} + return render(request, template, context=context) + +def account_images_view(request): + if not request.user.is_authenticated: + return render(request, "dashboard/login.html", {'title': 'Authentication Required'}) + template = "account/image_list.html" + my_images = Image.objects.filter(owner=request.user) + public_images = Image.objects.filter(public=True) + context = {"title": "Images", "images": my_images, "public_images": public_images } + return render(request, template, context=context) + diff --git a/src/api/admin.py b/src/api/admin.py new file mode 100644 index 0000000..f1bc70a --- /dev/null +++ b/src/api/admin.py @@ -0,0 +1,28 @@ +############################################################################## +# Copyright (c) 2016 Max Breitenfeldt and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + + +from django.apps import AppConfig +from django.contrib import admin + +from api.models import * + + +class ApiConfig(AppConfig): + name = 'apiJobs' + +admin.site.register(Job) +admin.site.register(OpnfvApiConfig) +admin.site.register(HardwareConfig) +admin.site.register(NetworkConfig) +admin.site.register(SoftwareConfig) +admin.site.register(AccessRelation) +admin.site.register(SoftwareRelation) +admin.site.register(HostHardwareRelation) +admin.site.register(HostNetworkRelation) diff --git a/src/api/migrations/0001_initial.py b/src/api/migrations/0001_initial.py new file mode 100644 index 0000000..abe6f5e --- /dev/null +++ b/src/api/migrations/0001_initial.py @@ -0,0 +1,185 @@ +############################################################################## +# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## +# Generated by Django 2.1 on 2018-09-14 14:48 + +import api.models +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('booking', '__first__'), + ('resource_inventory', '__first__'), + ] + + operations = [ + migrations.CreateModel( + name='AccessRelation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.IntegerField(default=0)), + ('task_id', models.CharField(default=api.models.get_task_uuid, max_length=37)), + ('lab_token', models.CharField(default='null', max_length=50)), + ('message', models.TextField(default='')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='HostHardwareRelation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.IntegerField(default=0)), + ('task_id', models.CharField(default=api.models.get_task_uuid, max_length=37)), + ('lab_token', models.CharField(default='null', max_length=50)), + ('message', models.TextField(default='')), + ('host', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.Host')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='HostNetworkRelation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.IntegerField(default=0)), + ('task_id', models.CharField(default=api.models.get_task_uuid, max_length=37)), + ('lab_token', models.CharField(default='null', max_length=50)), + ('message', models.TextField(default='')), + ('host', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.Host')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Job', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.IntegerField(default=0)), + ('delta', models.TextField()), + ('complete', models.BooleanField(default=False)), + ('booking', models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to='booking.Booking')), + ], + ), + migrations.CreateModel( + name='OpnfvApiConfig', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('installer', models.CharField(max_length=100)), + ('scenario', models.CharField(max_length=100)), + ('delta', models.TextField()), + ('roles', models.ManyToManyField(to='resource_inventory.Host')), + ], + ), + migrations.CreateModel( + name='SoftwareRelation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.IntegerField(default=0)), + ('task_id', models.CharField(default=api.models.get_task_uuid, max_length=37)), + ('lab_token', models.CharField(default='null', max_length=50)), + ('message', models.TextField(default='')), + ('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.Job')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='TaskConfig', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ], + ), + migrations.CreateModel( + name='AccessConfig', + fields=[ + ('taskconfig_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='api.TaskConfig')), + ('access_type', models.CharField(max_length=50)), + ('revoke', models.BooleanField(default=False)), + ('context', models.TextField(default='')), + ('delta', models.TextField()), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + bases=('api.taskconfig',), + ), + migrations.CreateModel( + name='HardwareConfig', + fields=[ + ('taskconfig_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='api.TaskConfig')), + ('image', models.CharField(default='defimage', max_length=100)), + ('power', models.CharField(default='off', max_length=100)), + ('hostname', models.CharField(default='hostname', max_length=100)), + ('ipmi_create', models.BooleanField(default=False)), + ('delta', models.TextField()), + ], + bases=('api.taskconfig',), + ), + migrations.CreateModel( + name='NetworkConfig', + fields=[ + ('taskconfig_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='api.TaskConfig')), + ('delta', models.TextField()), + ('interfaces', models.ManyToManyField(to='resource_inventory.Interface')), + ], + bases=('api.taskconfig',), + ), + migrations.CreateModel( + name='SoftwareConfig', + fields=[ + ('taskconfig_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='api.TaskConfig')), + ('opnfv', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.OpnfvApiConfig')), + ], + bases=('api.taskconfig',), + ), + migrations.AddField( + model_name='hostnetworkrelation', + name='job', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.Job'), + ), + migrations.AddField( + model_name='hosthardwarerelation', + name='job', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.Job'), + ), + migrations.AddField( + model_name='accessrelation', + name='job', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.Job'), + ), + migrations.AddField( + model_name='softwarerelation', + name='config', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='api.SoftwareConfig'), + ), + migrations.AddField( + model_name='hostnetworkrelation', + name='config', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='api.NetworkConfig'), + ), + migrations.AddField( + model_name='hosthardwarerelation', + name='config', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='api.HardwareConfig'), + ), + migrations.AddField( + model_name='accessrelation', + name='config', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='api.AccessConfig'), + ), + ] diff --git a/src/api/migrations/__init__.py b/src/api/migrations/__init__.py index b5914ce..e0408fa 100644 --- a/src/api/migrations/__init__.py +++ b/src/api/migrations/__init__.py @@ -1,10 +1,8 @@ ############################################################################## -# Copyright (c) 2016 Max Breitenfeldt and others. +# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others. # # All rights reserved. This program and the accompanying materials # are made available under the terms of the Apache License, Version 2.0 # which accompanies this distribution, and is available at # http://www.apache.org/licenses/LICENSE-2.0 ############################################################################## - - diff --git a/src/api/models.py b/src/api/models.py new file mode 100644 index 0000000..f1e9130 --- /dev/null +++ b/src/api/models.py @@ -0,0 +1,713 @@ +############################################################################## +# Copyright (c) 2018 Sawyer Bergeron, Parker Berberian, and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + + +from django.db import models +from django.core.exceptions import PermissionDenied + +import json +import uuid + +from resource_inventory.models import * +from booking.models import Booking + + +class JobStatus(object): + NEW = 0 + CURRENT = 100 + DONE = 200 + ERROR = 300 + + +class LabManagerTracker(object): + + @classmethod + def get(cls, lab_name, token): + """ + Takes in a lab name (from a url path) + returns a lab manager instance for that lab, if it exists + """ + try: + lab = Lab.objects.get(name=lab_name) + except: + raise PermissionDenied("Lab not found") + if lab.api_token == token: + return LabManager(lab) + raise PermissionDenied("Lab not authorized") + + +class LabManager(object): + """ + This is the class that will ultimately handle all REST calls to + lab endpoints. + handles jobs, inventory, status, etc + may need to create helper classes + """ + + def __init__(self, lab): + self.lab = lab + + def get_profile(self): + prof = {} + prof['name'] = self.lab.name + prof['contact'] = { + "phone": self.lab.contact_phone, + "email": self.lab.contact_email + } + prof['host_count'] = [] + for host in HostProfile.objects.filter(labs=self.lab): + count = Host.objects.filter(profile=host, lab=self.lab).count() + prof['host_count'].append({ + "type": host.name, + "count": count + }) + return prof + + def get_inventory(self): + inventory = {} + hosts = Host.objects.filter(lab=self.lab) + images = Image.objects.filter(from_lab=self.lab) + profiles = HostProfile.objects.filter(labs=self.lab) + inventory['hosts'] = self.serialize_hosts(hosts) + inventory['images'] = self.serialize_images(images) + inventory['host_types'] = self.serialize_host_profiles(profiles) + return inventory + + def get_status(self): + return {"status": self.lab.status} + + def set_status(self, payload): + {} + + def get_current_jobs(self): + jobs = Job.objects.filter(booking__lab=self.lab) + + return self.serialize_jobs(jobs, status=JobStatus.CURRENT) + + def get_new_jobs(self): + jobs = Job.objects.filter(booking__lab=self.lab) + + return self.serialize_jobs(jobs, status=JobStatus.NEW) + + def get_done_jobs(self): + jobs = Job.objects.filter(booking__lab=self.lab) + + return self.serialize_jobs(jobs, status=JobStatus.DONE) + + def get_job(self, jobid): + return Job.objects.get(pk=jobid).to_dict() + + def update_job(self, jobid, data): + {} + + def serialize_jobs(self, jobs, status=JobStatus.NEW): + job_ser = [] + for job in jobs: + jsonized_job = job.get_delta(status) + if len(jsonized_job['payload']) < 1: + continue + job_ser.append(jsonized_job) + + return job_ser + + def serialize_hosts(self, hosts): + host_ser = [] + for host in hosts: + h = {} + h['interfaces'] = [] + h['hostname'] = host.name + h['host_type'] = host.profile.name + for iface in host.interfaces.all(): + eth = {} + eth['mac'] = iface.mac_address + eth['busaddr'] = iface.bus_address + eth['name'] = iface.name + eth['switchport'] = {"switch_name": iface.switch_name, "port_name": iface.port_name} + h['interfaces'].append(eth) + return host_ser + + def serialize_images(self, images): + images_ser = [] + for image in images: + images_ser.append({ + "name": image.name, + "lab_id": image.lab_id, + "dashboard_id": image.id + }) + return images_ser + + def serialize_host_profiles(self, profiles): + profile_ser = [] + for profile in profiles: + p = {} + p['cpu'] = { + "cores": profile.cpuprofile.first().cores, + "arch": profile.cpuprofile.first().architecture, + "cpus": profile.cpuprofile.first().cpus, + } + p['disks'] = [] + for disk in profile.storageprofile.all(): + d = { + "size": disk.size, + "type": disk.media_type, + "name": disk.name + } + p['disks'].append(d) + p['description'] = profile.description + p['interfaces'] = [] + for iface in profile.interfaceprofile.all(): + p['interfaces'].append({ + "speed": iface.speed, + "name": iface.name + }) + + p['ram'] = {"amount": profile.ramprofile.first().amount} + p['name'] = profile.name + profile_ser.append(p) + return profile_ser + + +class Job(models.Model): + """ + This is the class that is serialized and put into the api + """ + booking = models.OneToOneField(Booking, on_delete=models.CASCADE, null=True) + status = models.IntegerField(default=JobStatus.NEW) + complete = models.BooleanField(default=False) + + def to_dict(self): + d = {} + j = {} + j['id'] = self.id + for relation in AccessRelation.objects.filter(job=self): + if 'access' not in d: + d['access'] = {} + d['access'][relation.task_id] = relation.config.to_dict() + for relation in SoftwareRelation.objects.filter(job=self): + if 'software' not in d: + d['software'] = {} + d['software'][relation.task_id] = relation.config.to_dict() + for relation in HostHardwareRelation.objects.filter(job=self): + if 'hardware' not in d: + d['hardware'] = {} + d['hardware'][relation.task_id] = relation.config.to_dict() + for relation in HostNetworkRelation.objects.filter(job=self): + if 'network' not in d: + d['network'] = {} + d['network'][relation.task_id] = relation.config.to_dict() + + j['payload'] = d + + return j + + def get_tasklist(self, status="all"): + tasklist = [] + clist = [HostHardwareRelation, AccessRelation, HostNetworkRelation, SoftwareRelation] + if status == "all": + for cls in clist: + tasklist += list(cls.objects.filter(job=self)) + else: + for cls in clist: + tasklist += list(cls.objects.filter(job=self).filter(status=status)) + return tasklist + + def get_delta(self, status): + d = {} + j = {} + j['id'] = self.id + for relation in AccessRelation.objects.filter(job=self).filter(status=status): + if 'access' not in d: + d['access'] = {} + d['access'][relation.task_id] = relation.config.get_delta() + for relation in SoftwareRelation.objects.filter(job=self).filter(status=status): + if 'software' not in d: + d['software'] = {} + d['software'][relation.task_id] = relation.config.get_delta() + for relation in HostHardwareRelation.objects.filter(job=self).filter(status=status): + if 'hardware' not in d: + d['hardware'] = {} + d['hardware'][relation.task_id] = relation.config.get_delta() + for relation in HostNetworkRelation.objects.filter(job=self).filter(status=status): + if 'network' not in d: + d['network'] = {} + d['network'][relation.task_id] = relation.config.get_delta() + + j['payload'] = d + return j + + def to_json(self): + return json.dumps(self.to_dict()) + + +class TaskConfig(models.Model): + def to_dict(self): + pass + + def get_delta(self): + pass + + def to_json(self): + return json.dumps(self.to_dict()) + + def clear_delta(self): + self.delta = '{}' + +class OpnfvApiConfig(models.Model): + + installer = models.CharField(max_length=100) + scenario = models.CharField(max_length=100) + roles = models.ManyToManyField(Host) + delta = models.TextField() + + def to_dict(self): + d = {} + if self.installer: + d['installer'] = self.installer + if self.scenario: + d['scenario'] = self.scenario + + hosts = self.roles.all() + if hosts.exists(): + d['roles'] = [] + for host in self.roles.all(): + d['roles'].append({host.labid: host.config.opnfvRole.name}) + + return d + + def to_json(self): + return json.dumps(self.to_dict()) + + def set_installer(self, installer): + self.installer = installer + d = json.loads(self.delta) + d['installer'] = installer + self.delta = json.dumps(d) + + def set_scenario(self, scenario): + self.scenario = scenario + d = json.loads(self.delta) + d['scenario'] = scenario + self.delta = json.dumps(d) + + def add_role(self, host): + self.roles.add(host) + d = json.loads(self.delta) + if 'role' not in d: + d['role'] = [] + d['roles'].append({host.labid: host.config.opnfvRole.name}) + self.delta = json.dumps(d) + + def clear_delta(self): + self.delta = '{}' + + def get_delta(self): + if not self.delta: + self.delta = self.to_json() + self.save() + return json.loads(self.delta) + +class AccessConfig(TaskConfig): + access_type = models.CharField(max_length=50) + user = models.ForeignKey(User, on_delete=models.CASCADE) + revoke = models.BooleanField(default=False) + context = models.TextField(default="") + delta = models.TextField() + + def to_dict(self): + d = {} + d['access_type'] = self.access_type + d['user'] = self.user.id + d['revoke'] = self.revoke + d['context'] = self.context + return d + + def get_delta(self): + if not self.delta: + self.delta = self.to_json() + self.save() + d = json.loads(self.delta) + d["lab_token"] = self.accessrelation.lab_token + + return d + + def to_json(self): + return json.dumps(self.to_dict()) + + def clear_delta(self): + d = {} + d["lab_token"] = self.accessrelation.lab_token + self.delta = json.dumps(d) + + def set_access_type(self, access_type): + self.access_type = access_type + d = json.loads(self.delta) + d['access_type'] = access_type + self.delta = json.dumps(d) + + def set_user(self, user): + self.user = user + d = json.loads(self.delta) + d['user'] = self.user.id + self.delta = json.dumps(d) + + def set_revoke(self, revoke): + self.revoke = revoke + d = json.loads(self.delta) + d['revoke'] = revoke + self.delta = json.dumps(d) + + def set_context(self, context): + self.context = context + d = json.loads(self.delta) + d['context'] = context + self.delta = json.dumps(d) + +class SoftwareConfig(TaskConfig): + """ + handled opnfv installations, etc + """ + opnfv = models.ForeignKey(OpnfvApiConfig, on_delete=models.CASCADE) + + def to_dict(self): + d = {} + if self.opnfv: + d['opnfv'] = self.opnfv.to_dict() + + d["lab_token"] = self.softwarerelation.lab_token + self.delta = json.dumps(d) + + return d + + def get_delta(self): + d = {} + d['opnfv'] = self.opnfv.get_delta() + d['lab_token'] = self.softwarerelation.lab_token + + return d + + def clear_delta(self): + self.opnfv.clear_delta() + + def to_json(self): + return json.dumps(self.to_dict()) + +class HardwareConfig(TaskConfig): + """ + handles imaging, user accounts, etc + """ + image = models.CharField(max_length=100, default="defimage") + power = models.CharField(max_length=100, default="off") + hostname = models.CharField(max_length=100, default="hostname") + ipmi_create = models.BooleanField(default=False) + delta = models.TextField() + + def to_dict(self): + d = {} + d['image'] = self.image + d['power'] = self.power + d['hostname'] = self.hostname + d['ipmi_create'] = str(self.ipmi_create) + d['id'] = self.hosthardwarerelation.host.labid + return d + + def to_json(self): + return json.dumps(self.to_dict()) + + def get_delta(self): + if not self.delta: + self.delta = self.to_json() + self.save() + d = json.loads(self.delta) + d['lab_token'] = self.hosthardwarerelation.lab_token + return d + + def clear_delta(self): + d = {} + d["id"] = self.hosthardwarerelation.host.labid + d["lab_token"] = self.hosthardwarerelation.lab_token + self.delta = json.dumps(d) + + def set_image(self, image): + self.image = image + d = json.loads(self.delta) + d['image'] = self.image + self.delta = json.dumps(d) + + def set_power(self, power): + self.power = power + d = json.loads(self.delta) + d['power'] = power + self.delta = json.dumps(d) + + def set_hostname(self, hostname): + self.hostname = hostname + d = json.loads(self.delta) + d['hostname'] = hostname + self.delta = json.dumps(d) + + def set_ipmi_create(self, ipmi_create): + self.ipmi_create = ipmi_create + d = json.loads(self.delta) + d['ipmi_create'] = ipmi_create + self.delta = json.dumps(d) + + +class NetworkConfig(TaskConfig): + """ + handles network configuration + """ + interfaces = models.ManyToManyField(Interface) + delta = models.TextField() + + def to_dict(self): + d = {} + hid = self.hostnetworkrelation.host.labid + d[hid] = {} + for interface in self.interfaces.all(): + d[hid][interface.mac_address] = [] + for vlan in interface.config.all(): + d[hid][interface.mac_address].append({"vlan_id": vlan.vlan_id, "tagged": vlan.tagged}) + + return d + + def to_json(self): + return json.dumps(self.to_dict()) + + def get_delta(self): + if not self.delta: + self.delta = self.to_json() + self.save() + d = json.loads(self.delta) + d['lab_token'] = self.hostnetworkrelation.lab_token + return d + + def clear_delta(self): + pass + + def add_interface(self, interface): + self.interfaces.add(interface) + d = json.loads(self.delta) + hid = self.hostnetworkrelation.host.labid + if hid not in d: + d[hid] = {} + d[hid][interface.mac_address] = [] + for vlan in interface.config.all(): + d[hid][interface.mac_address].append({"vlan_id": vlan.vlan_id, "tagged": vlan.tagged}) + self.delta = json.dumps(d) + + +def get_task(task_id): + for taskclass in [AccessRelation, SoftwareRelation, HostHardwareRelation, HostNetworkRelation]: + try: + ret = taskclass.objects.get(task_id=task_id) + return ret + except taskclass.DoesNotExist: + pass + from django.core.exceptions import ObjectDoesNotExist + raise ObjectDoesNotExist("Could not find matching TaskRelation instance") + + +def get_task_uuid(): + return str(uuid.uuid4()) + + +class TaskRelation(models.Model): + status = models.IntegerField(default=JobStatus.NEW) + job = models.ForeignKey(Job, on_delete=models.CASCADE) + config = models.OneToOneField(TaskConfig, on_delete=models.CASCADE) + task_id = models.CharField(default=get_task_uuid, max_length=37) + lab_token = models.CharField(default="null", max_length=50) + message = models.TextField(default="") + + def delete(self, *args, **kwargs): + self.config.delete() + return super(self.__class__, self).delete(*args, **kwargs) + + def type_str(self): + return "Generic Task" + + class Meta: + abstract = True + + +class AccessRelation(TaskRelation): + config = models.OneToOneField(AccessConfig, on_delete=models.CASCADE) + + def type_str(self): + return "Access Task" + + def delete(self, *args, **kwargs): + self.config.delete() + return super(self.__class__, self).delete(*args, **kwargs) + + +class SoftwareRelation(TaskRelation): + config = models.OneToOneField(SoftwareConfig, on_delete=models.CASCADE) + + def type_str(self): + return "Software Configuration Task" + + def delete(self, *args, **kwargs): + self.config.delete() + return super(self.__class__, self).delete(*args, **kwargs) + + +class HostHardwareRelation(TaskRelation): + host = models.ForeignKey(Host, on_delete=models.CASCADE) + config = models.OneToOneField(HardwareConfig, on_delete=models.CASCADE) + + def type_str(self): + return "Hardware Configuration Task" + + def get_delta(self): + return self.config.to_dict() + + def delete(self, *args, **kwargs): + self.config.delete() + return super(self.__class__, self).delete(*args, **kwargs) + + +class HostNetworkRelation(TaskRelation): + host = models.ForeignKey(Host, on_delete=models.CASCADE) + config = models.OneToOneField(NetworkConfig, on_delete=models.CASCADE) + + def type_str(self): + return "Network Configuration Task" + + def delete(self, *args, **kwargs): + self.config.delete() + return super(self.__class__, self).delete(*args, **kwargs) + + +class JobFactory(object): + + @classmethod + def makeCompleteJob(cls, booking): + hosts = Host.objects.filter(bundle=booking.resource) + job = None + try: + job = Job.objects.get(booking=booking) + except: + job = Job.objects.create(status=JobStatus.NEW, booking=booking) + cls.makeHardwareConfigs( + hosts=hosts, + job=job + ) + cls.makeNetworkConfigs( + hosts=hosts, + job=job + ) + cls.makeSoftware( + hosts=hosts, + job=job + ) + cls.makeAccessConfig( + users=booking.collaborators.all(), + access_type="vpn", + revoke=False, + job=job + ) + cls.makeAccessConfig( + users=[booking.owner], + access_type="vpn", + revoke=False, + job=job + ) + + @classmethod + def makeHardwareConfigs(cls, hosts=[], job=Job()): + for host in hosts: + hardware_config = None + try: + hardware_config = HardwareConfig.objects.get(relation__host=host) + except: + hardware_config = HardwareConfig() + + relation = HostHardwareRelation() + relation.host = host + relation.job = job + relation.config = hardware_config + relation.config.save() + relation.config = relation.config + relation.save() + + hardware_config.clear_delta() + hardware_config.set_image(host.config.image.lab_id) + hardware_config.set_hostname(host.template.resource.name) + hardware_config.set_power("on") + hardware_config.set_ipmi_create(True) + hardware_config.save() + + @classmethod + def makeAccessConfig(cls, users, access_type, revoke=False, job=Job()): + for user in users: + relation = AccessRelation() + relation.job = job + config = AccessConfig() + config.access_type = access_type + config.user = user + config.save() + relation.config = config + relation.save() + config.clear_delta() + config.set_access_type(access_type) + config.set_revoke(revoke) + config.set_user(user) + config.save() + + @classmethod + def makeNetworkConfigs(cls, hosts=[], job=Job()): + for host in hosts: + network_config = None + try: + network_config = NetworkConfig.objects.get(relation__host=host) + except: + network_config = NetworkConfig.objects.create() + + relation = HostNetworkRelation() + relation.host = host + relation.job = job + network_config.save() + relation.config = network_config + relation.save() + network_config.clear_delta() + + for interface in host.interfaces.all(): + network_config.add_interface(interface) + network_config.save() + + @classmethod + def makeSoftware(cls, hosts=[], job=Job()): + def init_config(host): + opnfv_config = OpnfvApiConfig() + if host is not None: + opnfv = host.config.bundle.opnfv_config.first() + opnfv_config.installer = opnfv.installer.name + opnfv_config.scenario = opnfv.scenario.name + opnfv_config.save() + return opnfv_config + + try: + host = None + if len(hosts) > 0: + host = hosts[0] + opnfv_config = init_config(host) + + for host in hosts: + opnfv_config.roles.add(host) + software_config = SoftwareConfig.objects.create(opnfv=opnfv_config) + software_config.save() + software_relation = SoftwareRelation.objects.create(job=job, config=software_config) + software_relation.save() + return software_relation + except: + return None + + def makeAccess(cls, user, access_type, revoke): + pass diff --git a/src/api/serializers.py b/src/api/serializers.py deleted file mode 100644 index 10e1975..0000000 --- a/src/api/serializers.py +++ /dev/null @@ -1,53 +0,0 @@ -############################################################################## -# Copyright (c) 2016 Max Breitenfeldt and others. -# -# All rights reserved. This program and the accompanying materials -# are made available under the terms of the Apache License, Version 2.0 -# which accompanies this distribution, and is available at -# http://www.apache.org/licenses/LICENSE-2.0 -############################################################################## - - -from rest_framework import serializers - -from account.models import UserProfile -from notifier.models import Notifier -from booking.models import Booking -from dashboard.models import Server, Resource, ResourceStatus - -class BookingSerializer(serializers.ModelSerializer): - installer_name = serializers.CharField(source='installer.name') - scenario_name = serializers.CharField(source='scenario.name') - opsys_name = serializers.CharField(source='opsys.name') - - class Meta: - model = Booking - fields = ('id', 'changeid', 'reset', 'user', 'resource_id', 'opsys_name', 'start', 'end', 'installer_name', 'scenario_name', 'purpose') - - -class ServerSerializer(serializers.ModelSerializer): - class Meta: - model = Server - fields = ('id', 'resource_id', 'name', 'model', 'cpu', 'ram', 'storage') - - -class ResourceSerializer(serializers.ModelSerializer): - class Meta: - model = Resource - fields = ('id', 'name', 'description', 'resource_lab', 'url', 'server_set', 'dev_pod') - -class ResourceStatusSerializer(serializers.ModelSerializer): - class Meta: - model = ResourceStatus - fields = ('id', 'resource', 'timestamp','type', 'title', 'content') - -class NotifierSerializer(serializers.ModelSerializer): - class Meta: - model = Notifier - fields = ('id', 'title', 'content', 'user', 'sender', 'message_type', 'msg_sent') - -class UserSerializer(serializers.ModelSerializer): - username = serializers.CharField(source='user.username') - class Meta: - model = UserProfile - fields = ('user', 'username', 'ssh_public_key', 'pgp_public_key', 'email_addr') diff --git a/src/jenkins/__init__.py b/src/api/serializers/__init__.py similarity index 85% rename from src/jenkins/__init__.py rename to src/api/serializers/__init__.py index b5914ce..e0408fa 100644 --- a/src/jenkins/__init__.py +++ b/src/api/serializers/__init__.py @@ -1,10 +1,8 @@ ############################################################################## -# Copyright (c) 2016 Max Breitenfeldt and others. +# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others. # # All rights reserved. This program and the accompanying materials # are made available under the terms of the Apache License, Version 2.0 # which accompanies this distribution, and is available at # http://www.apache.org/licenses/LICENSE-2.0 ############################################################################## - - diff --git a/src/api/serializers/booking_serializer.py b/src/api/serializers/booking_serializer.py new file mode 100644 index 0000000..e891de4 --- /dev/null +++ b/src/api/serializers/booking_serializer.py @@ -0,0 +1,156 @@ +############################################################################## +# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + + +from rest_framework import serializers + +from resource_inventory.models import * + +class BookingField(serializers.Field): + + def to_representation(self, booking): + """ + Takes in a booking object. + Returns a dictionary of primitives representing that booking + """ + ser = {} + ser['id'] = booking.id + # main loop to grab relevant info out of booking + host_configs = {} # mapping hostname -> config + networks = {} # mapping vlan id -> network_hosts + for host in booking.resource.hosts.all(): + host_configs[host.name] = HostConfiguration.objects.get(host=host.template) + if "jumphost" not in ser and host_configs[host.name].opnfvRole.name.lower() == "jumphost": + ser['jumphost'] = host.name + #host is a Host model + for i in range(len(host.interfaces.all())): + interface = host.interfaces.all()[i] + #interface is an Interface model + for vlan in interface.config.all(): + #vlan is Vlan model + if vlan.id not in networks: + networks[vlan.id] = [] + net_host = {"hostname": host.name, "tagged": vlan.tagged, "interface":i} + networks[vlan.id].append(net_host) + #creates networking object of proper form + networking = [] + for vlanid in networks: + network = {} + network['vlan_id'] = vlanid + network['hosts'] = networks[vlanid] + + ser['networking'] = networking + + #creates hosts object of correct form + hosts = [] + for hostname in host_configs: + host = {"hostname": hostname} + host['deploy_image'] = True # TODO? + image = host_configs[hostname].image + host['image'] = { + "name": image.name, + "lab_id": image.lab_id, + "dashboard_id": image.id + } + hosts.append(host) + + ser['hosts'] = hosts + + return ser + + def to_internal_value(self, data): + """ + Takes in a dictionary of primitives + Returns a booking object + + This is not going to be implemented or allowed. + If someone needs to create a booking through the api, + they will send a different booking object + """ + return None + +class BookingSerializer(serializers.Serializer): + + booking = BookingField() + +#Host Type stuff, for inventory + +class CPUSerializer(serializers.ModelSerializer): + class Meta: + model = CpuProfile + fields = ('cores', 'architecture', 'cpus') + +class DiskSerializer(serializers.ModelSerializer): + class Meta: + model = DiskProfile + fields = ('size', 'media_type', 'name') + +class InterfaceProfileSerializer(serializers.ModelSerializer): + class Meta: + model = InterfaceProfile + fields = ('speed', 'name') + +class RamSerializer(serializers.ModelSerializer): + class Meta: + model = RamProfile + fields = ('amount', 'channels') + +class HostTypeSerializer(serializers.Serializer): + name = serializers.CharField(max_length=200) + ram = RamSerializer() + interface = InterfaceProfileSerializer() + description = serializers.CharField(max_length=1000) + disks = DiskSerializer() + cpu = CPUSerializer() + +#the rest of the inventory stuff +class NetworkSerializer(serializers.Serializer): + cidr = serializers.CharField(max_length=200) + gateway = serializers.IPAddressField(max_length=200) + vlan = serializers.IntegerField() + +class ImageSerializer(serializers.ModelSerializer): + lab_id = serializers.IntegerField() + id = serializers.IntegerField(source="dashboard_id") + name = serializers.CharField(max_length=50) + description = serializers.CharField(max_length=200) + class Meta: + model = Image + +class InterfaceField(serializers.Field): + def to_representation(self, interface): + pass + + def to_internal_value(self, data): + """ + takes in a serialized interface and creates an Interface model + """ + mac = data['mac'] + bus_address = data['busaddr'] + switch_name = data['switchport']['switch_name'] + port_name = data['switchport']['port_name'] + # TODO config?? + return Interface.objects.create( + mac_address=mac, + bus_address=bus_address, + switch_name=switch_name, + port_name=port_name + ) + +class InventoryHostSerializer(serializers.Serializer): + hostname = serializers.CharField(max_length=100) + host_type = serializers.CharField(max_length=100) + interfaces = InterfaceField() + + +class InventorySerializer(serializers.Serializer): + hosts = InventoryHostSerializer() + networks = NetworkSerializer() + images = ImageSerializer() + host_types = HostTypeSerializer() diff --git a/src/api/serializers/old_serializers.py b/src/api/serializers/old_serializers.py new file mode 100644 index 0000000..f50b90b --- /dev/null +++ b/src/api/serializers/old_serializers.py @@ -0,0 +1,28 @@ +############################################################################## +# Copyright (c) 2016 Max Breitenfeldt and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + + +from rest_framework import serializers + +from account.models import UserProfile +from notifier.models import Notifier + + +class NotifierSerializer(serializers.ModelSerializer): + class Meta: + model = Notifier + fields = ('id', 'title', 'content', 'user', 'sender', 'message_type', 'msg_sent') + + +class UserSerializer(serializers.ModelSerializer): + username = serializers.CharField(source='user.username') + + class Meta: + model = UserProfile + fields = ('user', 'username', 'ssh_public_key', 'pgp_public_key', 'email_addr') diff --git a/src/api/tests/__init__.py b/src/api/tests/__init__.py new file mode 100644 index 0000000..fe2a32d --- /dev/null +++ b/src/api/tests/__init__.py @@ -0,0 +1,8 @@ +############################################################################## +# Copyright (c) 2016 Parker Berberian and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## \ No newline at end of file diff --git a/src/api/tests/test_serializers.py b/src/api/tests/test_serializers.py new file mode 100644 index 0000000..c49010c --- /dev/null +++ b/src/api/tests/test_serializers.py @@ -0,0 +1,213 @@ +############################################################################## +# Copyright (c) 2018 Sawyer Bergeron and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## +from django.test import TestCase +from booking.models import Booking +from resource_inventory.models import * +from account.models import Lab +from api.serializers.booking_serializer import * +from datetime import timedelta +from django.utils import timezone +from django.contrib.auth.models import Permission, User + + +class BookingSerializerTestCase(TestCase): + + count = 0 + + def makeHostConfigurations(self, hosts, config): + lab_user = User.objects.create(username="asfasdfasdf") + owner = User.objects.create(username="asfasdfasdffffff") + lab = Lab.objects.create( + lab_user=lab_user, + name="TestLab123123", + contact_email="mail@email.com", + contact_phone="" + ) + jumphost=True + for host in hosts: + image = Image.objects.create( + lab_id=12, + from_lab=lab, + name="this is a test image", + owner=owner + ) + name = "jumphost" + if not jumphost: + name = "compute" + role = OPNFVRole.objects.create( + name=name, + description="stuff" + ) + + HostConfiguration.objects.create( + host=host, + image=image, + bundle=config, + opnfvRole=role + ) + jumphost=False + + + def setUp(self): + self.serializer = BookingField() + lab_user = User.objects.create(username="lab user") + lab = Lab.objects.create(name="test lab", lab_user=lab_user) + # create hostProfile + hostProfile = HostProfile.objects.create( + host_type=0, + name='Test profile', + description='a test profile' + ) + interfaceProfile = InterfaceProfile.objects.create( + speed=1000, + name='eno3', + host=hostProfile + ) + diskProfile = DiskProfile.objects.create( + size=1000, + media_type="SSD", + name='/dev/sda', + host=hostProfile + ) + cpuProfile = CpuProfile.objects.create( + cores=96, + architecture="x86_64", + cpus=2, + host=hostProfile + ) + ramProfile = RamProfile.objects.create( + amount=256, + channels=4, + host=hostProfile + ) + + #create GenericResourceBundle + genericBundle = GenericResourceBundle.objects.create() + + gres1 = GenericResource.objects.create( + bundle=genericBundle, + name='generic resource ' + str(self.count) + ) + self.count += 1 + gHost1 = GenericHost.objects.create( + resource=gres1, + profile=hostProfile + ) + + gres2 = GenericResource.objects.create( + bundle=genericBundle, + name='generic resource ' + str(self.count) + ) + self.count += 1 + gHost2 = GenericHost.objects.create( + resource=gres2, + profile=hostProfile + ) + user1 = User.objects.create(username='user1') + + add_booking_perm = Permission.objects.get(codename='add_booking') + user1.user_permissions.add(add_booking_perm) + + user1 = User.objects.get(pk=user1.id) + + conf = ConfigBundle.objects.create(owner=user1, name="test conf") + self.makeHostConfigurations([gHost1, gHost2], conf) + + #actual resource bundle + bundle = ResourceBundle.objects.create( + template = genericBundle + ) + + host1 = Host.objects.create( + template=gHost1, + booked=True, + name='host1', + bundle=bundle, + profile=hostProfile, + lab=lab + ) + + host2 = Host.objects.create( + template=gHost2, + booked=True, + name='host2', + bundle=bundle, + profile=hostProfile, + lab=lab + ) + + vlan1 = Vlan.objects.create(vlan_id=300, tagged=False) + vlan2 = Vlan.objects.create(vlan_id=300, tagged=False) + + iface1 = Interface.objects.create( + mac_address='00:11:22:33:44:55', + bus_address='some bus address', + switch_name='switch1', + port_name='port10', + host=host1 + ) + + iface1.config = [vlan1] + + iface2 = Interface.objects.create( + mac_address='00:11:22:33:44:56', + bus_address='some bus address', + switch_name='switch1', + port_name='port12', + host=host2 + ) + + iface2.config = [vlan2] + + # finally, can create booking + self.booking = Booking.objects.create( + owner=user1, + start = timezone.now(), + end = timezone.now() + timedelta(weeks=1), + purpose='Testing', + resource=bundle, + config_bundle=conf + ) + + serialized_booking = {} + + host1 = {} + host1['hostname'] = 'host1' + host1['image'] = {} # TODO: Images + host1['deploy_image'] = True + host2 = {} + host2['hostname'] = 'host2' + host2['image'] = {} # TODO: Images + host2['deploy_image'] = True + + serialized_booking['hosts'] = [host1, host2] + + net = {} + net['name'] = 'network_name' + net['vlan_id'] = 300 + netHost1 = {} + netHost1['hostname'] = 'host1' + netHost1['tagged'] = False + netHost1['interface'] = 0 + netHost2 = {} + netHost2['hostname'] = 'host2' + netHost2['tagged'] = False + netHost2['interface'] = 0 + net['hosts'] = [netHost1, netHost2] + + serialized_booking['networking'] = [net] + serialized_booking['jumphost'] = 'host1' + + self.serialized_booking = serialized_booking + + def test_to_representation(self): + keys = ['hosts', 'networking', 'jumphost'] + serialized_form = self.serializer.to_representation(self.booking) + for key in keys: + self.assertEquals(serialized_form[key], self.serialized_booking) diff --git a/src/api/urls.py b/src/api/urls.py index c2cd510..94f8279 100644 --- a/src/api/urls.py +++ b/src/api/urls.py @@ -1,5 +1,6 @@ ############################################################################## # Copyright (c) 2016 Max Breitenfeldt and others. +# Copyright (c) 2018 Sawyer Bergeron, Parker Berberian, and others # # All rights reserved. This program and the accompanying materials # are made available under the terms of the Apache License, Version 2.0 @@ -24,19 +25,25 @@ Including another URLconf 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) """ from django.conf.urls import url, include +from django.urls import path from rest_framework import routers from api.views import * router = routers.DefaultRouter() -router.register(r'resources', ResourceViewSet) -router.register(r'servers', ServerViewSet) router.register(r'bookings', BookingViewSet) -router.register(r'resource_status', ResourceStatusViewSet) router.register(r'notifier', NotifierViewSet) router.register(r'user', UserViewSet) urlpatterns = [ url(r'^', include(router.urls)), + path('labs//profile', lab_profile), + path('labs//status', lab_status), + path('labs//inventory', lab_inventory), + path('labs//jobs/', specific_job), + path('labs//jobs//', specific_task), + path('labs//jobs/new', new_jobs), + path('labs//jobs/current', current_jobs), + path('labs//jobs/done', done_jobs), url(r'^token$', GenerateTokenView.as_view(), name='generate_token'), ] diff --git a/src/api/views.py b/src/api/views.py index b873ef1..cefd131 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -1,5 +1,6 @@ ############################################################################## # Copyright (c) 2016 Max Breitenfeldt and others. +# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others. # # All rights reserved. This program and the accompanying materials # are made available under the terms of the Apache License, Version 2.0 @@ -8,17 +9,23 @@ ############################################################################## -from django.contrib.auth.decorators import login_required, user_passes_test +from django.contrib.auth.decorators import login_required from django.shortcuts import redirect from django.utils.decorators import method_decorator from django.views import View +from django.http.response import JsonResponse from rest_framework import viewsets from rest_framework.authtoken.models import Token +from django.views.decorators.csrf import csrf_exempt -from api.serializers import * +import json + +from api.serializers.booking_serializer import * +from api.serializers.old_serializers import NotifierSerializer, UserSerializer from account.models import UserProfile from booking.models import Booking -from dashboard.models import Resource, Server, ResourceStatus +from notifier.models import Notifier +from api.models import * class BookingViewSet(viewsets.ModelViewSet): @@ -27,29 +34,16 @@ class BookingViewSet(viewsets.ModelViewSet): filter_fields = ('resource', 'id') -class ServerViewSet(viewsets.ModelViewSet): - queryset = Server.objects.all() - serializer_class = ServerSerializer - filter_fields = ('resource', 'name') - - -class ResourceViewSet(viewsets.ModelViewSet): - queryset = Resource.objects.all() - serializer_class = ResourceSerializer - filter_fields = ('name', 'id') - -class ResourceStatusViewSet(viewsets.ModelViewSet): - queryset = ResourceStatus.objects.all() - serializer_class = ResourceStatusSerializer - class NotifierViewSet(viewsets.ModelViewSet): queryset = Notifier.objects.none() serializer_class = NotifierSerializer + class UserViewSet(viewsets.ModelViewSet): queryset = UserProfile.objects.all() serializer_class = UserSerializer + @method_decorator(login_required, name='dispatch') class GenerateTokenView(View): def get(self, request, *args, **kwargs): @@ -59,3 +53,74 @@ class GenerateTokenView(View): token.delete() Token.objects.create(user=user) return redirect('account:settings') + + +def lab_inventory(request, lab_name=""): + lab_token = request.META.get('HTTP_AUTH_TOKEN') + lab_manager = LabManagerTracker.get(lab_name, lab_token) + return JsonResponse(lab_manager.get_inventory(), safe=False) + + +def lab_status(request, lab_name=""): + lab_token = request.META.get('HTTP_AUTH_TOKEN') + lab_manager = LabManagerTracker.get(lab_name, lab_token) + if request.method == "POST": + return JsonResponse(lab_manager.set_status(request.POST), safe=False) + return JsonResponse(lab_manager.get_status(), safe=False) + + +def lab_profile(request, lab_name=""): + lab_token = request.META.get('HTTP_AUTH_TOKEN') + lab_manager = LabManagerTracker.get(lab_name, lab_token) + return JsonResponse(lab_manager.get_profile(), safe=False) + + +@csrf_exempt +def specific_task(request, lab_name="", job_id="", task_id=""): + lab_token = request.META.get('HTTP_AUTH_TOKEN') + LabManagerTracker.get(lab_name, lab_token) # Authorize caller, but we dont need the result + + if request.method == "POST": + task = get_task(task_id) + if 'status' in request.POST: + task.status = request.POST.get('status') + if 'message' in request.POST: + task.message = request.POST.get('message') + task.save() + d = {} + d['task'] = task.config.get_delta() + m = {} + m['status'] = task.status + m['job'] = str(task.job) + m['message'] = task.message + d['meta'] = m + response = json.dumps(d) + return JsonResponse(response) + elif request.method == "GET": + return JsonResponse(get_task(task_id).config.get_delta()) + + +def specific_job(request, lab_name="", job_id=""): + lab_token = request.META.get('HTTP_AUTH_TOKEN') + lab_manager = LabManagerTracker.get(lab_name, lab_token) + if request.method == "POST": + return JsonResponse(lab_manager.update_job(job_id, request.POST), safe=False) + return JsonResponse(lab_manager.get_job(job_id), safe=False) + + +def new_jobs(request, lab_name=""): + lab_token = request.META.get('HTTP_AUTH_TOKEN') + lab_manager = LabManagerTracker.get(lab_name, lab_token) + return JsonResponse(lab_manager.get_new_jobs(), safe=False) + + +def current_jobs(request, lab_name=""): + lab_token = request.META.get('HTTP_AUTH_TOKEN') + lab_manager = LabManagerTracker.get(lab_name, lab_token) + return JsonResponse(lab_manager.get_current_jobs(), safe=False) + + +def done_jobs(request, lab_name=""): + lab_token = request.META.get('HTTP_AUTH_TOKEN') + lab_manager = LabManagerTracker.get(lab_name, lab_token) + return JsonResponse(lab_manager.get_done_jobs(), safe=False) diff --git a/src/booking/__init__.py b/src/booking/__init__.py index b5914ce..b6fef6c 100644 --- a/src/booking/__init__.py +++ b/src/booking/__init__.py @@ -6,5 +6,3 @@ # which accompanies this distribution, and is available at # http://www.apache.org/licenses/LICENSE-2.0 ############################################################################## - - diff --git a/src/booking/admin.py b/src/booking/admin.py index 51e1031..2beb05b 100644 --- a/src/booking/admin.py +++ b/src/booking/admin.py @@ -1,5 +1,6 @@ ############################################################################## # Copyright (c) 2016 Max Breitenfeldt and others. +# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others. # # All rights reserved. This program and the accompanying materials # are made available under the terms of the Apache License, Version 2.0 @@ -13,6 +14,3 @@ from django.contrib import admin from booking.models import * admin.site.register(Booking) -admin.site.register(Opsys) -admin.site.register(Installer) -admin.site.register(Scenario) diff --git a/src/booking/forms.py b/src/booking/forms.py deleted file mode 100644 index 9d71b42..0000000 --- a/src/booking/forms.py +++ /dev/null @@ -1,65 +0,0 @@ -############################################################################## -# Copyright (c) 2016 Max Breitenfeldt and others. -# -# All rights reserved. This program and the accompanying materials -# are made available under the terms of the Apache License, Version 2.0 -# which accompanies this distribution, and is available at -# http://www.apache.org/licenses/LICENSE-2.0 -############################################################################## - - -import django.forms as forms - -from booking.models import Installer, Scenario, Opsys -from datetime import datetime - -class BookingForm(forms.Form): - fields = ['start', 'end', 'purpose', 'opsys', 'reset', 'installer', 'scenario'] - - start = forms.DateTimeField() - end = forms.DateTimeField() - reset = forms.ChoiceField(choices = ((True, 'Yes'),(False, 'No')), label="Reset System", initial='False', required=False) - purpose = forms.CharField(max_length=300) - opsys = forms.ModelChoiceField(queryset=Opsys.objects.all(), required=False) - opsys.label = "Operating System" - installer = forms.ModelChoiceField(queryset=Installer.objects.all(), required=False) - scenario = forms.ModelChoiceField(queryset=Scenario.objects.all(), required=False) - -class BookingEditForm(forms.Form): - fields = ['start', 'end', 'purpose', 'opsys', 'reset', 'installer', 'scenario'] - - start = forms.DateTimeField() - end = forms.DateTimeField() - purpose = forms.CharField(max_length=300) - opsys = forms.ModelChoiceField(queryset=Opsys.objects.all(), required=False) - installer = forms.ModelChoiceField(queryset=Installer.objects.all(), required=False) - scenario = forms.ModelChoiceField(queryset=Scenario.objects.all(), required=False) - reset = forms.ChoiceField(choices = ((True, 'Yes'),(False, 'No')), label="Reset System", initial='False', required=True) - - - def __init__(self, *args, **kwargs ): - cloned_kwargs = {} - cloned_kwargs['purpose'] = kwargs.pop('purpose') - cloned_kwargs['start'] = kwargs.pop('start') - cloned_kwargs['end'] = kwargs.pop('end') - if 'installer' in kwargs: - cloned_kwargs['installer'] = kwargs.pop('installer') - if 'scenario' in kwargs: - cloned_kwargs['scenario'] = kwargs.pop('scenario') - super(BookingEditForm, self).__init__( *args, **kwargs) - - self.fields['purpose'].initial = cloned_kwargs['purpose'] - self.fields['start'].initial = cloned_kwargs['start'].strftime('%m/%d/%Y %H:%M') - self.fields['end'].initial = cloned_kwargs['end'].strftime('%m/%d/%Y %H:%M') - try: - self.fields['installer'].initial = cloned_kwargs['installer'].id - except KeyError: - pass - except AttributeError: - pass - try: - self.fields['scenario'].initial = cloned_kwargs['scenario'].id - except KeyError: - pass - except AttributeError: - pass diff --git a/src/booking/migrations/0001_initial.py b/src/booking/migrations/0001_initial.py index 6932dae..20415fe 100644 --- a/src/booking/migrations/0001_initial.py +++ b/src/booking/migrations/0001_initial.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10 on 2016-11-03 13:33 -from __future__ import unicode_literals +# Generated by Django 2.1 on 2018-09-14 14:48 from django.conf import settings from django.db import migrations, models @@ -12,8 +10,9 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('dashboard', '0001_initial'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('account', '0001_initial'), + ('resource_inventory', '__first__'), ] operations = [ @@ -23,9 +22,17 @@ class Migration(migrations.Migration): ('id', models.AutoField(primary_key=True, serialize=False)), ('start', models.DateTimeField()), ('end', models.DateTimeField()), - ('jira_issue_id', models.IntegerField(null=True)), - ('jira_issue_status', models.CharField(max_length=50)), + ('reset', models.BooleanField(default=False)), + ('jira_issue_id', models.IntegerField(blank=True, null=True)), + ('jira_issue_status', models.CharField(blank=True, max_length=50)), ('purpose', models.CharField(max_length=300)), + ('ext_count', models.IntegerField(default=2)), + ('project', models.CharField(blank=True, default='', max_length=100, null=True)), + ('collaborators', models.ManyToManyField(related_name='collaborators', to=settings.AUTH_USER_MODEL)), + ('config_bundle', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='resource_inventory.ConfigBundle')), + ('lab', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='account.Lab')), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='owner', to=settings.AUTH_USER_MODEL)), + ('resource', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='resource_inventory.ResourceBundle')), ], options={ 'db_table': 'booking', @@ -38,6 +45,14 @@ class Migration(migrations.Migration): ('name', models.CharField(max_length=30)), ], ), + migrations.CreateModel( + name='Opsys', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=100)), + ('sup_installers', models.ManyToManyField(blank=True, to='booking.Installer')), + ], + ), migrations.CreateModel( name='Scenario', fields=[ @@ -46,23 +61,8 @@ class Migration(migrations.Migration): ], ), migrations.AddField( - model_name='booking', - name='installer', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='booking.Installer'), - ), - migrations.AddField( - model_name='booking', - name='resource', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='dashboard.Resource'), - ), - migrations.AddField( - model_name='booking', - name='scenario', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='booking.Scenario'), - ), - migrations.AddField( - model_name='booking', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + model_name='installer', + name='sup_scenarios', + field=models.ManyToManyField(blank=True, to='booking.Scenario'), ), ] diff --git a/src/booking/migrations/0002_booking_changeid.py b/src/booking/migrations/0002_booking_changeid.py deleted file mode 100644 index 33af8fd..0000000 --- a/src/booking/migrations/0002_booking_changeid.py +++ /dev/null @@ -1,38 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10 on 2017-12-13 15:06 -from __future__ import unicode_literals - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('booking', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='Opsys', - fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('name', models.CharField(max_length=100)), - ], - ), - migrations.AddField( - model_name='booking', - name='opsys', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='booking.Opsys'), - ), - migrations.AddField( - model_name='booking', - name='changeid', - field=models.TextField(default='no change ID'), - ), - migrations.AlterField( - model_name='booking', - name='changeid', - field=models.TextField(blank=True, default='no change ID', null=True), - ), - ] diff --git a/src/booking/migrations/0003_auto_20180108_2024.py b/src/booking/migrations/0003_auto_20180108_2024.py deleted file mode 100644 index 93cecc2..0000000 --- a/src/booking/migrations/0003_auto_20180108_2024.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10 on 2018-01-08 20:24 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('booking', '0002_booking_changeid'), - ] - - operations = [ - migrations.AddField( - model_name='booking', - name='reset', - field=models.BooleanField(default=False), - ), - migrations.AlterField( - model_name='booking', - name='changeid', - field=models.TextField(blank=True, default='initial', null=True), - ), - ] \ No newline at end of file diff --git a/src/booking/migrations/0004_booking_ext_count.py b/src/booking/migrations/0004_booking_ext_count.py deleted file mode 100644 index 6bcc3ce..0000000 --- a/src/booking/migrations/0004_booking_ext_count.py +++ /dev/null @@ -1,27 +0,0 @@ -############################################################################## -# Copyright (c) 2018 Sawyer Bergeron and others. -# -# All rights reserved. This program and the accompanying materials -# are made available under the terms of the Apache License, Version 2.0 -# which accompanies this distribution, and is available at -# http://www.apache.org/licenses/LICENSE-2.0 -############################################################################## - -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('booking', '0003_auto_20180108_2024'), - ] - - operations = [ - migrations.AddField( - model_name='booking', - name='ext_count', - field=models.IntegerField(default=2), - ), - ] diff --git a/src/booking/migrations/__init__.py b/src/booking/migrations/__init__.py index b5914ce..e69de29 100644 --- a/src/booking/migrations/__init__.py +++ b/src/booking/migrations/__init__.py @@ -1,10 +0,0 @@ -############################################################################## -# Copyright (c) 2016 Max Breitenfeldt and others. -# -# All rights reserved. This program and the accompanying materials -# are made available under the terms of the Apache License, Version 2.0 -# which accompanies this distribution, and is available at -# http://www.apache.org/licenses/LICENSE-2.0 -############################################################################## - - diff --git a/src/booking/models.py b/src/booking/models.py index 8762d46..adfc28d 100644 --- a/src/booking/models.py +++ b/src/booking/models.py @@ -1,5 +1,6 @@ ############################################################################## # Copyright (c) 2016 Max Breitenfeldt and others. +# Copyright (c) 2018 Sawyer Bergeron, Parker Berberian, and others. # # All rights reserved. This program and the accompanying materials # are made available under the terms of the Apache License, Version 2.0 @@ -8,67 +9,61 @@ ############################################################################## +from resource_inventory.models import ResourceBundle, ConfigBundle +from account.models import Lab from django.conf import settings from django.contrib.auth.models import User from django.db import models from jira import JIRA from jira import JIRAError -from django.utils.crypto import get_random_string -import hashlib +import resource_inventory.resource_manager -from dashboard.models import Resource - -class Installer(models.Model): +class Scenario(models.Model): id = models.AutoField(primary_key=True) - name = models.CharField(max_length=30) + name = models.CharField(max_length=300) def __str__(self): return self.name -class Scenario(models.Model): + +class Installer(models.Model): id = models.AutoField(primary_key=True) - name = models.CharField(max_length=300) + name = models.CharField(max_length=30) + sup_scenarios = models.ManyToManyField(Scenario, blank=True) def __str__(self): return self.name + class Opsys(models.Model): id = models.AutoField(primary_key=True) name = models.CharField(max_length=100) + sup_installers = models.ManyToManyField(Installer, blank=True) def __str__(self): return self.name + class Booking(models.Model): id = models.AutoField(primary_key=True) - changeid = models.TextField(default='initial', blank=True, null=True) - user = models.ForeignKey(User, models.CASCADE) # delete if user is deleted - resource = models.ForeignKey(Resource, models.PROTECT) + owner = models.ForeignKey(User, models.CASCADE, related_name='owner') # delete if user is deleted + collaborators = models.ManyToManyField(User, related_name='collaborators') start = models.DateTimeField() end = models.DateTimeField() reset = models.BooleanField(default=False) - jira_issue_id = models.IntegerField(null=True) - jira_issue_status = models.CharField(max_length=50) - - opsys = models.ForeignKey(Opsys, models.DO_NOTHING, null=True) - installer = models.ForeignKey(Installer, models.DO_NOTHING, null=True) - scenario = models.ForeignKey(Scenario, models.DO_NOTHING, null=True) + jira_issue_id = models.IntegerField(null=True, blank=True) + jira_issue_status = models.CharField(max_length=50, blank=True) purpose = models.CharField(max_length=300, blank=False) ext_count = models.IntegerField(default=2) + resource = models.ForeignKey(ResourceBundle, on_delete=models.SET_NULL, null=True) #need to decide behavior here on delete + config_bundle = models.ForeignKey(ConfigBundle, on_delete=models.SET_NULL, null=True) + project = models.CharField(max_length=100, default="", blank=True, null=True) + lab = models.ForeignKey(Lab, null=True, on_delete=models.SET_NULL) class Meta: db_table = 'booking' - def get_jira_issue(self): - try: - jira = JIRA(server=settings.JIRA_URL, - basic_auth=(settings.JIRA_USER_NAME, settings.JIRA_USER_PASSWORD)) - issue = jira.issue(self.jira_issue_id) - return issue - except JIRAError: - return None - def save(self, *args, **kwargs): """ Save the booking if self.user is authorized and there is no overlapping booking. @@ -83,11 +78,14 @@ class Booking(models.Model): conflicting_dates = conflicting_dates.filter(start__lt=self.end) if conflicting_dates.count() > 0: raise ValueError('This booking overlaps with another booking') - if not self.changeid: - self.changeid = self.id - else: - self.changeid = hashlib.md5(self.changeid.encode() + get_random_string(length=32).encode()).hexdigest() return super(Booking, self).save(*args, **kwargs) + def delete(self, *args, **kwargs): + res = self.resource + self.resource = None + self.save() + resource_inventory.resource_manager.ResourceManager.getInstance().deleteResourceBundle(res) + return super(self.__class__, self).delete(*args, **kwargs) + def __str__(self): - return str(self.resource) + ' from ' + str(self.start) + ' until ' + str(self.end) + return str(self.purpose) + ' from ' + str(self.start) + ' until ' + str(self.end) diff --git a/src/booking/stats.py b/src/booking/stats.py new file mode 100644 index 0000000..31f7ef1 --- /dev/null +++ b/src/booking/stats.py @@ -0,0 +1,56 @@ +############################################################################## +# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## +from booking.models import Booking +import datetime +import pytz + + +class StatisticsManager(object): + + @staticmethod + def getContinuousBookingTimeSeries(span=28): + """ + Will return a dictionary of names and 2-D array of x and y data points. + e.g. {"plot1": [["x1", "x2", "x3"],["y1", "y2", "y3]]} + x values will be dates in string + every change (booking start / end) will be reflected, instead of one data point per day + y values are the integer number of bookings/users active at some point in the given date + span is the number of days to plot. The last x value will always be the current time + """ + x_set = set() + x = [] + y = [] + users = [] + now = datetime.datetime.now(pytz.utc) + delta = datetime.timedelta(days=span) + end = now-delta + bookings = Booking.objects.filter(start__lte=now, end__gte=end) + for booking in bookings: + x_set.add(booking.start) + if booking.end < now: + x_set.add(booking.end) + + x_set.add(now) + x_set.add(end) + + x_list = list(x_set) + x_list.sort(reverse=True) + for time in x_list: + x.append(str(time)) + active = Booking.objects.filter(start__lte=time, end__gt=time) + booking_count = len(active) + users_set = set() + for booking in active: + users_set.add(booking.owner) + for user in booking.collaborators.all(): + users_set.add(user) + y.append(booking_count) + users.append(len(users_set)) + + return {"booking": [x, y], "user": [x, users]} diff --git a/src/booking/tests/__init__.py b/src/booking/tests/__init__.py index b5914ce..b6fef6c 100644 --- a/src/booking/tests/__init__.py +++ b/src/booking/tests/__init__.py @@ -6,5 +6,3 @@ # which accompanies this distribution, and is available at # http://www.apache.org/licenses/LICENSE-2.0 ############################################################################## - - diff --git a/src/booking/tests/test_models.py b/src/booking/tests/test_models.py index b4cd113..a83f817 100644 --- a/src/booking/tests/test_models.py +++ b/src/booking/tests/test_models.py @@ -1,5 +1,6 @@ ############################################################################## # Copyright (c) 2016 Max Breitenfeldt and others. +# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others. # # All rights reserved. This program and the accompanying materials # are made available under the terms of the Apache License, Version 2.0 @@ -15,29 +16,28 @@ from django.test import TestCase from django.utils import timezone from booking.models import * -from dashboard.models import Resource -from jenkins.models import JenkinsSlave - +from resource_inventory.models import ResourceBundle, GenericResourceBundle, ConfigBundle class BookingModelTestCase(TestCase): + count = 0 def setUp(self): - self.slave = JenkinsSlave.objects.create(name='test', url='test') self.owner = User.objects.create(username='owner') - self.res1 = Resource.objects.create(name='res1', slave=self.slave, description='x', - url='x',owner=self.owner) - self.res2 = Resource.objects.create(name='res2', slave=self.slave, description='x', - url='x',owner=self.owner) - + self.res1 = ResourceBundle.objects.create( + template=GenericResourceBundle.objects.create(name="gbundle" + str(self.count)) + ) + self.count += 1 + self.res2 = ResourceBundle.objects.create( + template=GenericResourceBundle.objects.create(name="gbundle2" + str(self.count)) + ) + self.count += 1 self.user1 = User.objects.create(username='user1') self.add_booking_perm = Permission.objects.get(codename='add_booking') self.user1.user_permissions.add(self.add_booking_perm) self.user1 = User.objects.get(pk=self.user1.id) - - self.installer = Installer.objects.create(name='TestInstaller') - self.scenario = Scenario.objects.create(name='TestScenario') + self.config_bundle = ConfigBundle.objects.create(owner=self.user1, name="test config") def test_start_end(self): """ @@ -46,11 +46,25 @@ class BookingModelTestCase(TestCase): """ start = timezone.now() end = start - timedelta(weeks=1) - self.assertRaises(ValueError, Booking.objects.create, start=start, end=end, - resource=self.res1, user=self.user1) + self.assertRaises( + ValueError, + Booking.objects.create, + start=start, + end=end, + resource=self.res1, + owner=self.user1, + config_bundle=self.config_bundle + ) end = start - self.assertRaises(ValueError, Booking.objects.create, start=start, end=end, - resource=self.res1, user=self.user1) + self.assertRaises( + ValueError, + Booking.objects.create, + start=start, + end=end, + resource=self.res1, + owner=self.user1, + config_bundle=self.config_bundle + ) def test_conflicts(self): """ @@ -60,35 +74,153 @@ class BookingModelTestCase(TestCase): start = timezone.now() end = start + timedelta(weeks=1) self.assertTrue( - Booking.objects.create(start=start, end=end, user=self.user1, resource=self.res1)) - - self.assertRaises(ValueError, Booking.objects.create, start=start, - end=end, resource=self.res1, user=self.user1) - self.assertRaises(ValueError, Booking.objects.create, start=start + timedelta(days=1), - end=end - timedelta(days=1), resource=self.res1, user=self.user1) + Booking.objects.create( + start=start, + end=end, + owner=self.user1, + resource=self.res1, + config_bundle=self.config_bundle + ) + ) + + self.assertRaises( + ValueError, + Booking.objects.create, + start=start, + end=end, + resource=self.res1, + owner=self.user1, + config_bundle=self.config_bundle + ) + self.assertRaises( + ValueError, + Booking.objects.create, + start=start + timedelta(days=1), + end=end - timedelta(days=1), + resource=self.res1, + owner=self.user1, + config_bundle=self.config_bundle + ) + + self.assertRaises( + ValueError, + Booking.objects.create, + start=start - timedelta(days=1), + end=end, + resource=self.res1, + owner=self.user1, + config_bundle=self.config_bundle + ) + + self.assertRaises( + ValueError, + Booking.objects.create, + start=start - timedelta(days=1), + end=end - timedelta(days=1), + resource=self.res1, + owner=self.user1, + config_bundle=self.config_bundle + ) + + self.assertRaises( + ValueError, + Booking.objects.create, + start=start, + end=end + timedelta(days=1), + resource=self.res1, + owner=self.user1, + config_bundle=self.config_bundle + ) + + self.assertRaises( + ValueError, + Booking.objects.create, + start=start + timedelta(days=1), + end=end + timedelta(days=1), + resource=self.res1, + owner=self.user1, + config_bundle=self.config_bundle + ) - self.assertRaises(ValueError, Booking.objects.create, start=start - timedelta(days=1), - end=end, resource=self.res1, user=self.user1) - self.assertRaises(ValueError, Booking.objects.create, start=start - timedelta(days=1), - end=end - timedelta(days=1), resource=self.res1, user=self.user1) - - self.assertRaises(ValueError, Booking.objects.create, start=start, - end=end + timedelta(days=1), resource=self.res1, user=self.user1) - self.assertRaises(ValueError, Booking.objects.create, start=start + timedelta(days=1), - end=end + timedelta(days=1), resource=self.res1, user=self.user1) + self.assertTrue( + Booking.objects.create( + start=start - timedelta(days=1), + end=start, + owner=self.user1, + resource=self.res1, + config_bundle=self.config_bundle + ) + ) + self.assertTrue( + Booking.objects.create( + start=end, + end=end + timedelta(days=1), + owner=self.user1, + resource=self.res1, + config_bundle=self.config_bundle + ) + ) - self.assertTrue(Booking.objects.create(start=start - timedelta(days=1), end=start, - user=self.user1, resource=self.res1)) - self.assertTrue(Booking.objects.create(start=end, end=end + timedelta(days=1), - user=self.user1, resource=self.res1)) + self.assertTrue( + Booking.objects.create( + start=start - timedelta(days=2), + end=start - timedelta(days=1), + owner=self.user1, + resource=self.res1, + config_bundle=self.config_bundle + ) + ) self.assertTrue( - Booking.objects.create(start=start - timedelta(days=2), end=start - timedelta(days=1), - user=self.user1, resource=self.res1)) + Booking.objects.create( + start=end + timedelta(days=1), + end=end + timedelta(days=2), + owner=self.user1, + resource=self.res1, + config_bundle=self.config_bundle + ) + ) + self.assertTrue( - Booking.objects.create(start=end + timedelta(days=1), end=end + timedelta(days=2), - user=self.user1, resource=self.res1)) + Booking.objects.create( + start=start, + end=end, + owner=self.user1, + resource=self.res2, + config_bundle=self.config_bundle + ) + ) + + def test_extensions(self): + """ + saving a booking with an extended end time is allows to happen twice, + and each extension must be a maximum of one week long + """ + start = timezone.now() + end = start + timedelta(weeks=1) self.assertTrue( - Booking.objects.create(start=start, end=end, - user=self.user1, resource=self.res2, scenario=self.scenario, - installer=self.installer)) \ No newline at end of file + Booking.objects.create( + start=start, + end=end, + owner=self.user1, + resource=self.res1, + config_bundle=self.config_bundle + ) + ) + + booking = Booking.objects.all().first() # should be only thing in db + + self.assertEquals(booking.ext_count, 2) + booking.end = booking.end + timedelta(days=3) + try: + booking.save() + except Exception: + self.fail("save() threw an exception") + booking.end = booking.end + timedelta(weeks=2) + self.assertRaises(ValueError, booking.save) + booking.end = booking.end - timedelta(days=8) + try: + self.assertTrue(booking.save()) + except Exception: + self.fail("save() threw an exception") + diff --git a/src/booking/tests/test_views.py b/src/booking/tests/test_views.py deleted file mode 100644 index c1da013..0000000 --- a/src/booking/tests/test_views.py +++ /dev/null @@ -1,106 +0,0 @@ -############################################################################## -# Copyright (c) 2016 Max Breitenfeldt and others. -# -# All rights reserved. This program and the accompanying materials -# are made available under the terms of the Apache License, Version 2.0 -# which accompanies this distribution, and is available at -# http://www.apache.org/licenses/LICENSE-2.0 -############################################################################## - - -from datetime import timedelta - -from django.test import Client -from django.test import TestCase -from django.urls import reverse -from django.utils import timezone -from django.utils.encoding import force_text -from registration.forms import User - -from account.models import UserProfile -from booking.models import Booking -from dashboard.models import Resource -from jenkins.models import JenkinsSlave - - -class BookingViewTestCase(TestCase): - def setUp(self): - self.client = Client() - self.slave = JenkinsSlave.objects.create(name='test', url='test') - self.owner = User.objects.create(username='owner') - self.res1 = Resource.objects.create(name='res1', slave=self.slave, description='x', - url='x',owner=self.owner) - self.user1 = User.objects.create(username='user1') - self.user1.set_password('user1') - self.user1profile = UserProfile.objects.create(user=self.user1) - self.user1.save() - - self.user1 = User.objects.get(pk=self.user1.id) - - - def test_resource_bookings_json(self): - url = reverse('booking:bookings_json', kwargs={'resource_id': 0}) - self.assertEqual(self.client.get(url).status_code, 404) - - url = reverse('booking:bookings_json', kwargs={'resource_id': self.res1.id}) - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - self.assertJSONEqual(force_text(response.content), {"bookings": []}) - booking1 = Booking.objects.create(start=timezone.now(), - end=timezone.now() + timedelta(weeks=1), user=self.user1, - resource=self.res1) - response = self.client.get(url) - json = response.json() - self.assertEqual(response.status_code, 200) - self.assertIn('bookings', json) - self.assertEqual(len(json['bookings']), 1) - self.assertIn('start', json['bookings'][0]) - self.assertIn('end', json['bookings'][0]) - self.assertIn('id', json['bookings'][0]) - self.assertIn('purpose', json['bookings'][0]) - - def test_booking_form_view(self): - url = reverse('booking:create', kwargs={'resource_id': 0}) - self.assertEqual(self.client.get(url).status_code, 404) - - # authenticated user - url = reverse('booking:create', kwargs={'resource_id': self.res1.id}) - self.client.login(username='user1',password='user1') - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed('booking/booking_calendar.html') - self.assertTemplateUsed('booking/booking_form.html') - self.assertIn('resource', response.context) - - - def test_booking_view(self): - start = timezone.now() - end = start + timedelta(weeks=1) - booking = Booking.objects.create(start=start, end=end, user=self.user1, resource=self.res1) - - url = reverse('booking:detail', kwargs={'booking_id':0}) - response = self.client.get(url) - self.assertEqual(response.status_code, 404) - - url = reverse('booking:detail', kwargs={'booking_id':booking.id}) - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed('booking/booking_detail.html') - self.assertIn('booking', response.context) - - def test_booking_list_view(self): - start = timezone.now() - timedelta(weeks=2) - end = start + timedelta(weeks=1) - Booking.objects.create(start=start, end=end, user=self.user1, resource=self.res1) - - url = reverse('booking:list') - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed('booking/booking_list.html') - self.assertTrue(len(response.context['bookings']) == 0) - - start = timezone.now() - end = start + timedelta(weeks=1) - Booking.objects.create(start=start, end=end, user=self.user1, resource=self.res1) - response = self.client.get(url) - self.assertTrue(len(response.context['bookings']) == 1) \ No newline at end of file diff --git a/src/booking/urls.py b/src/booking/urls.py index ed3b1d4..88fbb0a 100644 --- a/src/booking/urls.py +++ b/src/booking/urls.py @@ -1,5 +1,6 @@ ############################################################################## # Copyright (c) 2016 Max Breitenfeldt and others. +# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others. # # All rights reserved. This program and the accompanying materials # are made available under the terms of the Apache License, Version 2.0 @@ -27,20 +28,19 @@ from django.conf.urls import url from booking.views import * +app_name = "booking" urlpatterns = [ - url(r'^(?P[0-9]+)/$', BookingFormView.as_view(), name='create'), - url(r'^(?P[0-9]+)/edit/(?P[0-9]+)/$', BookingEditFormView.as_view(), name='edit'), - url(r'^(?P[0-9]+)/bookings_json/$', ResourceBookingsJSON.as_view(), - name='bookings_json'), - url(r'^detail/$', BookingView.as_view(), name='detail_prefix'), - url(r'^detail/(?P[0-9]+)/$', BookingView.as_view(), name='detail'), + url(r'^detail/(?P[0-9]+)/$', booking_detail_view, name='detail'), + url(r'^(?P[0-9]+)/$', booking_detail_view, name='booking_detail'), url(r'^delete/$', BookingDeleteView.as_view(), name='delete_prefix'), url(r'^delete/(?P[0-9]+)/$', BookingDeleteView.as_view(), name='delete'), url(r'^delete/(?P[0-9]+)/confirm/$', bookingDelete, name='delete_booking'), - url(r'^list/$', BookingListView.as_view(), name='list') -] \ No newline at end of file + url(r'^list/$', BookingListView.as_view(), name='list'), + url(r'^stats/$', booking_stats_view, name='stats'), + url(r'^stats/json$', booking_stats_json, name='stats_json'), +] diff --git a/src/booking/views.py b/src/booking/views.py index a52cfe2..c139b4c 100644 --- a/src/booking/views.py +++ b/src/booking/views.py @@ -1,5 +1,6 @@ ############################################################################## # Copyright (c) 2016 Max Breitenfeldt and others. +# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others. # # All rights reserved. This program and the accompanying materials # are made available under the terms of the Apache License, Version 2.0 @@ -7,39 +8,38 @@ # http://www.apache.org/licenses/LICENSE-2.0 ############################################################################## -from django.conf import settings from django.contrib import messages -from django.contrib.auth.mixins import LoginRequiredMixin -from django.http import JsonResponse from django.shortcuts import get_object_or_404 +from django.http import JsonResponse from django.urls import reverse from django.utils import timezone from django.views import View from django.views.generic import FormView from django.views.generic import TemplateView -from jira import JIRAError -from django.shortcuts import redirect +from django.shortcuts import redirect, render +import json -from account.jira_util import get_jira from booking.forms import BookingForm, BookingEditForm -from booking.models import Booking -from dashboard.models import Resource - -def create_jira_ticket(user, booking): - jira = get_jira(user) - issue_dict = { - 'project': 'PHAROS', - 'summary': str(booking.resource) + ': Access Request', - 'description': booking.purpose, - 'issuetype': {'name': 'Task'}, - 'components': [{'name': 'POD Access Request'}], - 'assignee': {'name': booking.resource.owner.username} - } - issue = jira.create_issue(fields=issue_dict) - jira.add_attachment(issue, user.userprofile.pgp_public_key) - jira.add_attachment(issue, user.userprofile.ssh_public_key) - booking.jira_issue_id = issue.id - booking.save() +from resource_inventory.models import ResourceBundle +from resource_inventory.resource_manager import ResourceManager +from booking.models import Booking, Installer, Opsys +from booking.stats import StatisticsManager + + +def drop_filter(context): + installer_filter = {} + for os in Opsys.objects.all(): + installer_filter[os.id] = [] + for installer in os.sup_installers.all(): + installer_filter[os.id].append(installer.id) + + scenario_filter = {} + for installer in Installer.objects.all(): + scenario_filter[installer.id] = [] + for scenario in installer.sup_scenarios.all(): + scenario_filter[installer.id].append(scenario.id) + + context.update({'installer_filter': json.dumps(installer_filter), 'scenario_filter': json.dumps(scenario_filter)}) class BookingFormView(FormView): @@ -47,14 +47,16 @@ class BookingFormView(FormView): form_class = BookingForm def dispatch(self, request, *args, **kwargs): - self.resource = get_object_or_404(Resource, id=self.kwargs['resource_id']) + self.resource = get_object_or_404(ResourceBundle, id=self.kwargs['resource_id']) return super(BookingFormView, self).dispatch(request, *args, **kwargs) def get_context_data(self, **kwargs): - title = 'Booking: ' + self.resource.name + title = 'Booking: ' + str(self.resource.id) context = super(BookingFormView, self).get_context_data(**kwargs) context.update({'title': title, 'resource': self.resource}) - #raise PermissionDenied('check') + + drop_filter(context) + return context def get_success_url(self): @@ -75,24 +77,16 @@ class BookingFormView(FormView): booking = Booking(start=form.cleaned_data['start'], end=form.cleaned_data['end'], purpose=form.cleaned_data['purpose'], - opsys=form.cleaned_data['opsys'], installer=form.cleaned_data['installer'], scenario=form.cleaned_data['scenario'], - resource=self.resource, user=user) + resource=self.resource, + owner=user + ) try: booking.save() except ValueError as err: messages.add_message(self.request, messages.ERROR, err) return super(BookingFormView, self).form_invalid(form) - try: - if settings.CREATE_JIRA_TICKET: - create_jira_ticket(user, booking) - except JIRAError: - messages.add_message(self.request, messages.ERROR, 'Failed to create Jira Ticket. ' - 'Please check your Jira ' - 'permissions.') - booking.delete() - return super(BookingFormView, self).form_invalid(form) messages.add_message(self.request, messages.SUCCESS, 'Booking saved') return super(BookingFormView, self).form_valid(form) @@ -105,7 +99,7 @@ class BookingEditFormView(FormView): return True def dispatch(self, request, *args, **kwargs): - self.resource = get_object_or_404(Resource, id=self.kwargs['resource_id']) + self.resource = get_object_or_404(ResourceBundle, id=self.kwargs['resource_id']) self.original_booking = get_object_or_404(Booking, id=self.kwargs['booking_id']) return super(BookingEditFormView, self).dispatch(request, *args, **kwargs) @@ -113,6 +107,9 @@ class BookingEditFormView(FormView): title = 'Editing Booking on: ' + self.resource.name context = super(BookingEditFormView, self).get_context_data(**kwargs) context.update({'title': title, 'resource': self.resource, 'booking': self.original_booking}) + + drop_filter(context) + return context def get_form_kwargs(self): @@ -120,14 +117,6 @@ class BookingEditFormView(FormView): kwargs['purpose'] = self.original_booking.purpose kwargs['start'] = self.original_booking.start kwargs['end'] = self.original_booking.end - try: - kwargs['installer'] = self.original_booking.installer - except AttributeError: - pass - try: - kwargs['scenario'] = self.original_booking.scenario - except AttributeError: - pass return kwargs def get_success_url(self): @@ -145,7 +134,7 @@ class BookingEditFormView(FormView): 'You are not the owner of this booking.') return super(BookingEditFormView, self).form_invalid(form) - #Do Conflict Checks + # Do Conflict Checks if self.original_booking.end != form.cleaned_data['end']: if form.cleaned_data['end'] - self.original_booking.end > timezone.timedelta(days=7): messages.add_message(self.request, messages.ERROR, @@ -176,13 +165,12 @@ class BookingEditFormView(FormView): messages.add_message(self.request, messages.ERROR, err) return super(BookingEditFormView, self).form_invalid(form) - user = self.request.user return super(BookingEditFormView, self).form_valid(form) + class BookingView(TemplateView): template_name = "booking/booking_detail.html" - def get_context_data(self, **kwargs): booking = get_object_or_404(Booking, id=self.kwargs['booking_id']) title = 'Booking Details' @@ -190,6 +178,7 @@ class BookingView(TemplateView): context.update({'title': title, 'booking': booking}) return context + class BookingDeleteView(TemplateView): template_name = "booking/booking_delete.html" @@ -200,12 +189,14 @@ class BookingDeleteView(TemplateView): context.update({'title': title, 'booking': booking}) return context + def bookingDelete(request, booking_id): booking = get_object_or_404(Booking, id=booking_id) booking.delete() messages.add_message(request, messages.SUCCESS, 'Booking deleted') return redirect('../../../../') + class BookingListView(TemplateView): template_name = "booking/booking_list.html" @@ -219,8 +210,47 @@ class BookingListView(TemplateView): class ResourceBookingsJSON(View): def get(self, request, *args, **kwargs): - resource = get_object_or_404(Resource, id=self.kwargs['resource_id']) - bookings = resource.booking_set.get_queryset().values('id', 'start', 'end', 'purpose', - 'jira_issue_status', 'opsys__name', - 'installer__name', 'scenario__name') + resource = get_object_or_404(ResourceBundle, id=self.kwargs['resource_id']) + bookings = resource.booking_set.get_queryset().values( + 'id', + 'start', + 'end', + 'purpose', + 'jira_issue_status', + 'config_bundle__name' + ) return JsonResponse({'bookings': list(bookings)}) + + +def booking_detail_view(request, booking_id): + user = None + if request.user.is_authenticated: + user = request.user + else: + return render(request, "dashboard/login.html", {'title': 'Authentication Required'}) + + booking = get_object_or_404(Booking, id=booking_id) + return render(request, "booking/booking_detail.html", { + 'title': 'Booking Details', + 'booking': booking, + 'pdf': ResourceManager().makePDF(booking.resource), + 'user_id': user.id}) + + +def booking_stats_view(request): + return render( + request, + "booking/stats.html", + context={ + "data": StatisticsManager.getContinuousBookingTimeSeries(), + "title": "Booking Statistics" + } + ) + + +def booking_stats_json(request): + try: + span = int(request.GET.get("days", 14)) + except: + span = 14 + return JsonResponse(StatisticsManager.getContinuousBookingTimeSeries(span), safe=False) diff --git a/src/dashboard/__init__.py b/src/dashboard/__init__.py index b5914ce..b6fef6c 100644 --- a/src/dashboard/__init__.py +++ b/src/dashboard/__init__.py @@ -6,5 +6,3 @@ # which accompanies this distribution, and is available at # http://www.apache.org/licenses/LICENSE-2.0 ############################################################################## - - diff --git a/src/dashboard/admin.py b/src/dashboard/admin.py index 0bfdef8..43b5386 100644 --- a/src/dashboard/admin.py +++ b/src/dashboard/admin.py @@ -1,5 +1,6 @@ ############################################################################## # Copyright (c) 2016 Max Breitenfeldt and others. +# Copyright (c) 2018 Sawyer Bergeron, Parker Berberian, and others. # # All rights reserved. This program and the accompanying materials # are made available under the terms of the Apache License, Version 2.0 @@ -10,11 +11,6 @@ from django.contrib import admin -from dashboard.models import * admin.site.site_header = "Pharos Dashboard Administration" admin.site.site_title = "Pharos Dashboard" - -admin.site.register(Resource) -admin.site.register(Server) -admin.site.register(ResourceStatus) diff --git a/src/dashboard/context_processors.py b/src/dashboard/context_processors.py new file mode 100644 index 0000000..32c70b8 --- /dev/null +++ b/src/dashboard/context_processors.py @@ -0,0 +1,12 @@ +############################################################################## +# Copyright (c) 2018 Sawyer Bergeron, Parker Berberian, and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## +from django.conf import settings + +def debug(context): + return {'DEBUG': settings.DEBUG} diff --git a/src/dashboard/exceptions.py b/src/dashboard/exceptions.py new file mode 100644 index 0000000..bc3fcac --- /dev/null +++ b/src/dashboard/exceptions.py @@ -0,0 +1,46 @@ +############################################################################## +# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + + +class ResourceProvisioningException(Exception): + """ + Resources could not be provisioned + """ + pass + +class ModelValidationException(Exception): + """ + Validation before saving model returned issues + """ + pass + +class ResourceAvailabilityException(ResourceProvisioningException): + """ + Requested resources are not *currently* available + """ + pass + +class ResourceExistenceException(ResourceAvailabilityException): + """ + Requested resources do not exist or do not match any known resources + """ + pass + + +class NonUniqueHostnameException(Exception): + pass + +class InvalidHostnameException(Exception): + pass + +class InvalidVlanConfigurationException(Exception): + pass + +class NetworkExistsException(Exception): + pass diff --git a/src/dashboard/migrations/0001_initial.py b/src/dashboard/migrations/0001_initial.py deleted file mode 100644 index aaf3945..0000000 --- a/src/dashboard/migrations/0001_initial.py +++ /dev/null @@ -1,64 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10 on 2016-11-03 13:33 -from __future__ import unicode_literals - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('jenkins', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='Resource', - fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('name', models.CharField(max_length=100, unique=True)), - ('description', models.CharField(blank=True, max_length=300, null=True)), - ('url', models.CharField(blank=True, max_length=100, null=True)), - ('owner', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='user_lab_owner', to=settings.AUTH_USER_MODEL)), - ('slave', models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='jenkins.JenkinsSlave')), - ('vpn_users', models.ManyToManyField(blank=True, related_name='user_vpn_users', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'db_table': 'resource', - }, - ), - migrations.CreateModel( - name='ResourceStatus', - fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('timestamp', models.DateTimeField(auto_now_add=True)), - ('type', models.CharField(max_length=20)), - ('title', models.CharField(max_length=50)), - ('content', models.CharField(max_length=5000)), - ('resource', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dashboard.Resource')), - ], - options={ - 'db_table': 'resource_status', - }, - ), - migrations.CreateModel( - name='Server', - fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('name', models.CharField(blank=True, max_length=100)), - ('model', models.CharField(blank=True, max_length=100)), - ('cpu', models.CharField(blank=True, max_length=100)), - ('ram', models.CharField(blank=True, max_length=100)), - ('storage', models.CharField(blank=True, max_length=100)), - ('resource', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dashboard.Resource')), - ], - options={ - 'db_table': 'server', - }, - ), - ] diff --git a/src/dashboard/migrations/0002_auto_20170505_0815.py b/src/dashboard/migrations/0002_auto_20170505_0815.py deleted file mode 100644 index 4285b88..0000000 --- a/src/dashboard/migrations/0002_auto_20170505_0815.py +++ /dev/null @@ -1,42 +0,0 @@ -############################################################################## -# Copyright (c) 2016 Max Breitenfeldt and others. -# -# All rights reserved. This program and the accompanying materials -# are made available under the terms of the Apache License, Version 2.0 -# which accompanies this distribution, and is available at -# http://www.apache.org/licenses/LICENSE-2.0 -############################################################################## - - -# -*- coding: utf-8 -*- -# Generated by Django 1.10 on 2017-05-05 08:15 -from __future__ import unicode_literals - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('dashboard', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='resource', - name='dev_pod', - field=models.BooleanField(default=False), - ), - migrations.AlterField( - model_name='resource', - name='owner', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='user_lab_owner', to=settings.AUTH_USER_MODEL), - ), - migrations.AlterField( - model_name='resource', - name='slave', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='jenkins.JenkinsSlave'), - ), - ] diff --git a/src/dashboard/migrations/0003_resource_resource_lab.py b/src/dashboard/migrations/0003_resource_resource_lab.py deleted file mode 100644 index fff93fd..0000000 --- a/src/dashboard/migrations/0003_resource_resource_lab.py +++ /dev/null @@ -1,22 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10 on 2018-01-10 16:36 -from __future__ import unicode_literals - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('account', '0002_auto_20180110_1636'), - ('dashboard', '0002_auto_20170505_0815'), - ] - - operations = [ - migrations.AddField( - model_name='resource', - name='resource_lab', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='lab_resource_set', to='account.Lab'), - ), - ] diff --git a/src/dashboard/models.py b/src/dashboard/models.py index ff55232..f9bd07e 100644 --- a/src/dashboard/models.py +++ b/src/dashboard/models.py @@ -1,97 +1,9 @@ ############################################################################## # Copyright (c) 2016 Max Breitenfeldt and others. +# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others. # # All rights reserved. This program and the accompanying materials # are made available under the terms of the Apache License, Version 2.0 # which accompanies this distribution, and is available at # http://www.apache.org/licenses/LICENSE-2.0 ############################################################################## - - -from datetime import timedelta - -from django.contrib.auth.models import User -from django.db import models -from django.utils import timezone - -from jenkins.models import JenkinsSlave -from account.models import Lab - - -class Resource(models.Model): - id = models.AutoField(primary_key=True) - name = models.CharField(max_length=100, unique=True) - description = models.CharField(max_length=300, blank=True, null=True) - url = models.CharField(max_length=100, blank=True, null=True) - resource_lab = models.ForeignKey(Lab, related_name='lab_resource_set', null=True, blank=True) - owner = models.ForeignKey(User, related_name='user_lab_owner', null=True, blank=True) - vpn_users = models.ManyToManyField(User, related_name='user_vpn_users', blank=True) - slave = models.ForeignKey(JenkinsSlave, on_delete=models.DO_NOTHING, null=True, blank=True) - dev_pod = models.BooleanField(default=False) - - def get_booking_utilization(self, weeks): - """ - Return a dictionary containing the count of booked and free seconds for a resource in the - range [now,now + weeks] if weeks is positive, - or [now-weeks, now] if weeks is negative - """ - - length = timedelta(weeks=abs(weeks)) - now = timezone.now() - - start = now - end = now + length - if weeks < 0: - start = now - length - end = now - - bookings = self.booking_set.filter(start__lt=start + length, end__gt=start) - - booked_seconds = 0 - for booking in bookings: - booking_start = booking.start - booking_end = booking.end - if booking_start < start: - booking_start = start - if booking_end > end: - booking_end = start + length - total = booking_end - booking_start - booked_seconds += total.total_seconds() - - return {'booked_seconds': booked_seconds, - 'available_seconds': length.total_seconds() - booked_seconds} - - class Meta: - db_table = 'resource' - - def __str__(self): - return self.name - -class Server(models.Model): - id = models.AutoField(primary_key=True) - resource = models.ForeignKey(Resource, on_delete=models.CASCADE) - name = models.CharField(max_length=100, blank=True) - model = models.CharField(max_length=100, blank=True) - cpu = models.CharField(max_length=100, blank=True) - ram = models.CharField(max_length=100, blank=True) - storage = models.CharField(max_length=100, blank=True) - - class Meta: - db_table = 'server' - - def __str__(self): - return self.name - -class ResourceStatus(models.Model): - id = models.AutoField(primary_key=True) - resource = models.ForeignKey(Resource, on_delete=models.CASCADE) - timestamp = models.DateTimeField(auto_now_add=True) - type = models.CharField(max_length=20) - title = models.CharField(max_length=50) - content = models.CharField(max_length=5000) - - class Meta: - db_table = 'resource_status' - - def __str__(self): - return self.resource.name + ': ' + self.title + ' ' + str(self.timestamp) diff --git a/src/dashboard/populate_db_iol.py b/src/dashboard/populate_db_iol.py new file mode 100644 index 0000000..8c8b271 --- /dev/null +++ b/src/dashboard/populate_db_iol.py @@ -0,0 +1,346 @@ +############################################################################## +# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + + +from django.test import TestCase +from booking.models import Booking +from resource_inventory.models import * +from account.models import * +from api.serializers.booking_serializer import * +from datetime import timedelta +from django.utils import timezone +from django.contrib.auth.models import Permission, User +import json +import yaml + + +class Populator: + + def __init__(self): + self.host_profile_count = 0 + self.generic_host_count = 0 + self.host_profiles = [] + self.generic_bundle_count = 0 + self.booking_count = 0 + + + def make_host_profile(self, lab, data): + hostProfile = HostProfile.objects.create( + host_type=data['host']['type'], + name=data['host']['name'], + description=data['host']['description'] + ) + hostProfile.save() + + for iface_data in data['interfaces']: + + interfaceProfile = InterfaceProfile.objects.create( + speed=iface_data['speed'], + name=iface_data['name'], + host=hostProfile + ) + interfaceProfile.save() + + + for disk_data in data['disks']: + + diskProfile = DiskProfile.objects.create( + size=disk_data['size'], + media_type=disk_data['type'], + name=disk_data['name'], + host=hostProfile + ) + diskProfile.save() + + cpuProfile = CpuProfile.objects.create( + cores=data['cpu']['cores'], + architecture=data['cpu']['arch'], + cpus=data['cpu']['cpus'], + host=hostProfile + ) + cpuProfile.save() + ramProfile = RamProfile.objects.create( + amount=data['ram']['amount'], + channels=data['ram']['channels'], + host=hostProfile + ) + ramProfile.save() + hostProfile.labs.add(lab) + return hostProfile + + def make_users(self): + user_pberberian = User.objects.create(username="pberberian") + user_pberberian.save() + user_pberberian_prof = UserProfile.objects.create(user=user_pberberian) + user_pberberian_prof.save() + + user_sbergeron = User.objects.create(username="sbergeron") + user_sbergeron.save() + user_sbergeron_prof = UserProfile.objects.create(user=user_sbergeron) + user_sbergeron_prof.save() + return [user_sbergeron, user_pberberian,] + + + def make_labs(self): + unh_iol = User.objects.create(username="unh_iol") + unh_iol.save() + vlans = [] + reserved = [] + for i in range(1,4096): + vlans.append(1) + reserved.append(0) + # TODO: put reserved vlans here + iol = Lab.objects.create( + lab_user=unh_iol, + name="UNH_IOL", + vlan_manager=VlanManager.objects.create( + vlans = json.dumps(vlans), + reserved_vlans = json.dumps(reserved), + allow_overlapping = False, + block_size = 20, + ), + api_token = Lab.make_api_token(), + contact_email = "nfv-lab@iol.unh.edu", + location = "University of New Hampshire, Durham NH, 03824 USA" + ) + return [iol] + + + def make_configurations(self): + #scenarios + scen1 = Scenario.objects.create(name="os-nosdn-nofeature-noha") + scen2 = Scenario.objects.create(name="os-odl-kvm-ha") + scen3 = Scenario.objects.create(name="os-nosdn-nofeature-ha") + + #installers + fuel = Installer.objects.create(name="Fuel") + fuel.sup_scenarios.add(scen1) + fuel.sup_scenarios.add(scen3) + fuel.save() + joid = Installer.objects.create(name="Joid") + joid.sup_scenarios.add(scen1) + joid.sup_scenarios.add(scen2) + joid.save() + apex = Installer.objects.create(name="Apex") + apex.sup_scenarios.add(scen2) + apex.sup_scenarios.add(scen3) + apex.save() + daisy = Installer.objects.create(name="Daisy") + daisy.sup_scenarios.add(scen1) + daisy.sup_scenarios.add(scen2) + daisy.sup_scenarios.add(scen3) + daisy.save() + compass = Installer.objects.create(name="Compass") + compass.sup_scenarios.add(scen1) + compass.sup_scenarios.add(scen3) + compass.save() + + #operating systems + ubuntu = Opsys.objects.create(name="Ubuntu") + ubuntu.sup_installers.add(compass) + ubuntu.sup_installers.add(joid) + ubuntu.save() + centos = Opsys.objects.create(name="CentOs") + centos.sup_installers.add(apex) + centos.sup_installers.add(fuel) + centos.save() + suse = Opsys.objects.create(name="Suse") + suse.sup_installers.add(fuel) + suse.save() + + + #opnfv roles + compute = OPNFVRole.objects.create(name="Compute", description="Does the heavy lifting") + controller = OPNFVRole.objects.create(name="Controller", description="Controls everything") + jumphost = OPNFVRole.objects.create(name="Jumphost", description="Entry Point") + + lab = Lab.objects.first() + user = UserProfile.objects.first().user + image = Image.objects.create( + lab_id=23, + name="hpe centos", + from_lab=lab, + owner=user, + host_type=HostProfile.objects.get(name="hpe") + ) + image = Image.objects.create( + lab_id=25, + name="hpe ubuntu", + from_lab=lab, + owner=user, + host_type=HostProfile.objects.get(name="hpe") + ) + + image = Image.objects.create( + lab_id=26, + name="hpe suse", + from_lab=lab, + owner=user, + host_type=HostProfile.objects.get(name="hpe") + ) + image = Image.objects.create( + lab_id=27, + name="arm ubuntu", + from_lab=lab, + owner=user, + host_type=HostProfile.objects.get(name="arm") + ) + + def make_lab_hosts(self, hostcount, profile, lab, data, offset=1): + for i in range(hostcount): + name="Host_" + lab.name + "_" + profile.name + "_" + str(i + offset) + host = Host.objects.create( + name=name, + lab=lab, + profile=profile, + labid=data[i]['labid'] + ) + for iface_profile in profile.interfaceprofile.all(): + iface_data = data[i]['interfaces'][iface_profile.name] + Interface.objects.create( + mac_address=iface_data['mac'], + bus_address=iface_data['bus'], + name=iface_profile.name, + host=host + ) + + def make_profile_data(self): + """ + returns a dictionary of data from the yaml files + created by inspection scripts + """ + data = [] + for prof in ["hpe", "arm"]: # TODO + profile_dict = {} + host = { + "name": prof, + "type": 0, + "description": "some LaaS servers" + } + profile_dict['host'] = host + profile_dict['interfaces'] = [] + for interface in [{"name": "eno1", "speed": 1000}, {"name": "eno2", "speed": 10000}]: # TODO + iface_dict = {} + iface_dict["name"] = interface['name'] + iface_dict['speed'] = interface['speed'] + profile_dict['interfaces'].append(iface_dict) + + profile_dict['disks'] = [] + for disk in [{"size": 1000, "type": "ssd", "name": "sda"}]: # TODO + disk_dict = {} + disk_dict['size'] = disk['size'] + disk_dict['type'] = disk['type'] + disk_dict['name'] = disk['name'] + profile_dict['disks'].append(disk_dict) + + # cpu + cpu = {} + cpu['cores'] = 4 + cpu['arch'] = "x86" + cpu['cpus'] = 2 + profile_dict['cpu'] = cpu + + # ram + ram = {} + ram['amount'] = 256 + ram['channels'] = 4 + profile_dict['ram'] = ram + + data.append(profile_dict) + + return data + + def get_lab_data(self, lab): + data = {} + path = "/pharos_dashboard/data/" + lab.name + "/" + host_file = open(path + "hostlist.json") + host_structure = json.loads(host_file.read()) + host_file.close() + for profile in host_structure['profiles'].keys(): + data[profile] = {} + prof_path = path + profile + for host in host_structure['profiles'][profile]: + host_file = open(prof_path + "/" + host + ".yaml") + host_data = yaml.load(host_file.read()) + host_file.close() + data[profile][host] = host_data + return data + + def make_profiles_and_hosts(self, lab, lab_data): + for host_profile_name, host_data_dict in lab_data.items(): + if len(host_data_dict) < 1: + continue + host_profile = HostProfile.objects.create( + name=host_profile_name, + description="" + ) + host_profile.labs.add(lab) + example_host_data = list(host_data_dict.values())[0] + + cpu_data = example_host_data['cpu'] + CpuProfile.objects.create( + cores=cpu_data['cores'], + architecture=cpu_data['arch'], + cpus=cpu_data['cpus'], + host=host_profile + ) + + ram_data = example_host_data['memory'] + RamProfile.objects.create( + amount=int(ram_data[:-1]), + channels=1, + host=host_profile + ) + + disks_data = example_host_data['disk'] + for disk_data in disks_data: + size = 0 + try: + size=int(disk_data['size'].split('.')[0]) + except: + size=int(disk_data['size'].split('.')[0][:-1]) + DiskProfile.objects.create( + size=size, + media_type="SSD", + name=disk_data['name'], + host=host_profile + ) + + ifaces_data = example_host_data['interface'] + for iface_data in ifaces_data: + InterfaceProfile.objects.create( + speed=iface_data['speed'], + name=iface_data['name'], + host=host_profile + ) + + # all profiles created + for hostname, host_data in host_data_dict.items(): + host = Host.objects.create( + name=hostname, + labid=hostname, + profile=host_profile, + lab=lab + ) + for iface_data in host_data['interface']: + Interface.objects.create( + mac_address=iface_data['mac'], + bus_address=iface_data['busaddr'], + name=iface_data['name'], + host=host + ) + + def populate(self): + self.labs = self.make_labs() + # We should use the existing users, not creating our own + for lab in self.labs: + lab_data = self.get_lab_data(lab) + self.make_profiles_and_hosts(lab, lab_data) + + # We will add opnfv info and images as they are created and supported diff --git a/src/dashboard/tasks.py b/src/dashboard/tasks.py index fa2ee9d..827c7c5 100644 --- a/src/dashboard/tasks.py +++ b/src/dashboard/tasks.py @@ -1,5 +1,6 @@ ############################################################################## # Copyright (c) 2016 Max Breitenfeldt and others. +# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others. # # All rights reserved. This program and the accompanying materials # are made available under the terms of the Apache License, Version 2.0 @@ -7,32 +8,100 @@ # http://www.apache.org/licenses/LICENSE-2.0 ############################################################################## -from datetime import timedelta from celery import shared_task from django.utils import timezone -from django.conf import settings +from django.db.models import Q from booking.models import Booking +from notifier.manager import * +from notifier.models import * +from api.models import * +from resource_inventory.resource_manager import ResourceManager -from jenkins.models import JenkinsStatistic @shared_task -def database_cleanup(): - now = timezone.now() - JenkinsStatistic.objects.filter(timestamp__lt=now - timedelta(weeks=4)).delete() - -def booking_cleanup(): - expire_time = timedelta(days=int(settings.BOOKING_EXP_TIME)) - expire_number = int(settings.BOOKING_MAX_NUM) - expired_set = Booking.objects.filter(end__lte=timezone.now()) - expired_count = len(expired_set) - - for booking in expired_set: - if timezone.now() - booking.end > expire_time: - booking.delete() - expired_count = expired_count - 1 - - if expired_count > expire_number: - oldest = expired_set.order_by("end")[:expired_count-expire_number] - for booking in oldest: - booking.delete() +def conjure_aggregate_notifiers(): + NotifyPeriodic.task() + + +@shared_task +def booking_poll(): + def cleanup_hardware(qs): + for hostrelation in qs: + config = hostrelation.config + config.clear_delta() + config.set_power("off") + config.save() + hostrelation.status=JobStatus.NEW + hostrelation.save() + + def cleanup_network(qs): + for hostrelation in qs: + network = hostrelation.config + network.interfaces.clear() + host = hostrelation.host + network.clear_delta() + vlans = [] + for interface in host.interfaces.all(): + for vlan in interface.config: + if vlan.public: + try: + host.lab.vlan_manager.release_public_vlan(vlan.vlan_id) + except: # will fail if we already released in this loop + pass + else: + vlans.append(vlan.vlan_id) + + # release all vlans + if len(vlans) > 0: + host.lab.vlan_manager.release_vlans(vlans) + + interface.config.clear() + network.add_interface(interface) + network.save() + hostrelation.status=JobStatus.NEW + hostrelation.save() + + def cleanup_software(qs): + if qs.exists(): + relation = qs.first() + software = relation.config.opnfv + software.clear_delta() + software.save() + relation.status=JobStatus.NEW + relation.save() + + def cleanup_access(qs): + for relation in qs: + pass # TODO + + cleanup_set = Booking.objects.filter(end__lte=timezone.now()).filter(job__complete=False) + + for booking in cleanup_set: + if not booking.job.complete: + job = booking.job + cleanup_software(SoftwareRelation.objects.filter(job=job)) + cleanup_hardware(HostHardwareRelation.objects.filter(job=job)) + cleanup_network(HostNetworkRelation.objects.filter(job=job)) + cleanup_access(AccessRelation.objects.filter(job=job)) + job.complete = True + job.save() + + +@shared_task +def free_hosts(): + """ + gets all hosts from the database that need to be freed and frees them + """ + networks = ~Q(~Q(job__hostnetworkrelation__status=200)) + hardware = ~Q(~Q(job__hosthardwarerelation__status=200)) + + bookings = Booking.objects.filter( + networks, + hardware, + end__lt=timezone.now(), + job__complete=True, + resource__isnull=False + ) + for booking in bookings: + ResourceManager.getInstance().deleteResourceBundle(booking.resource) diff --git a/src/dashboard/templatetags/jenkins_filters.py b/src/dashboard/templatetags/jenkins_filters.py deleted file mode 100644 index e7e1425..0000000 --- a/src/dashboard/templatetags/jenkins_filters.py +++ /dev/null @@ -1,38 +0,0 @@ -############################################################################## -# Copyright (c) 2016 Max Breitenfeldt and others. -# -# All rights reserved. This program and the accompanying materials -# are made available under the terms of the Apache License, Version 2.0 -# which accompanies this distribution, and is available at -# http://www.apache.org/licenses/LICENSE-2.0 -############################################################################## - - -from django.template.defaultfilters import register - - -@register.filter -def jenkins_job_color(job_result): - if job_result == 'SUCCESS': - return '#5cb85c' - if job_result == 'FAILURE': - return '#d9534f' - if job_result == 'UNSTABLE': - return '#EDD62B' - return '#646F73' # job is still building - - -@register.filter -def jenkins_status_color(slave_status): - if slave_status == 'offline': - return '#d9534f' - if slave_status == 'online': - return '#5cb85c' - if slave_status == 'online / idle': - return '#5bc0de' - - -@register.filter -def jenkins_job_blink(job_result): - if job_result == '': # job is still building - return 'class=blink_me' diff --git a/src/dashboard/tests/test_models.py b/src/dashboard/tests/test_models.py deleted file mode 100644 index 3a3aeab..0000000 --- a/src/dashboard/tests/test_models.py +++ /dev/null @@ -1,69 +0,0 @@ -############################################################################## -# Copyright (c) 2016 Max Breitenfeldt and others. -# -# All rights reserved. This program and the accompanying materials -# are made available under the terms of the Apache License, Version 2.0 -# which accompanies this distribution, and is available at -# http://www.apache.org/licenses/LICENSE-2.0 -############################################################################## - - -from datetime import timedelta -from math import ceil, floor - -from django.test import TestCase -from django.utils import timezone - -from booking.models import * -from dashboard.models import Resource -from jenkins.models import JenkinsSlave - - -class ResourceModelTestCase(TestCase): - def setUp(self): - self.slave = JenkinsSlave.objects.create(name='test', url='test') - self.owner = User.objects.create(username='owner') - - self.res1 = Resource.objects.create(name='res1', slave=self.slave, description='x', - url='x', owner=self.owner) - - def test_booking_utilization(self): - utilization = self.res1.get_booking_utilization(1) - self.assertTrue(utilization['booked_seconds'] == 0) - self.assertTrue(utilization['available_seconds'] == timedelta(weeks=1).total_seconds()) - - start = timezone.now() + timedelta(days=1) - end = start + timedelta(days=1) - booking = Booking.objects.create(start=start, end=end, purpose='test', resource=self.res1, - user=self.owner) - - utilization = self.res1.get_booking_utilization(1) - booked_seconds = timedelta(days=1).total_seconds() - self.assertEqual(utilization['booked_seconds'], booked_seconds) - - utilization = self.res1.get_booking_utilization(-1) - self.assertEqual(utilization['booked_seconds'], 0) - - booking.delete() - start = timezone.now() - timedelta(days=1) - end = start + timedelta(days=2) - booking = Booking.objects.create(start=start, end=end, purpose='test', resource=self.res1, - user=self.owner) - booked_seconds = self.res1.get_booking_utilization(1)['booked_seconds'] - # use ceil because a fraction of the booked time has already passed now - booked_seconds = ceil(booked_seconds) - self.assertEqual(booked_seconds, timedelta(days=1).total_seconds()) - - booking.delete() - start = timezone.now() + timedelta(days=6) - end = start + timedelta(days=2) - booking = Booking.objects.create(start=start, end=end, purpose='test', resource=self.res1, - user=self.owner) - booked_seconds = self.res1.get_booking_utilization(1)['booked_seconds'] - booked_seconds = floor(booked_seconds) - self.assertEqual(booked_seconds, timedelta(days=1).total_seconds()) - - - - - diff --git a/src/dashboard/tests/test_views.py b/src/dashboard/tests/test_views.py deleted file mode 100644 index f5e17c2..0000000 --- a/src/dashboard/tests/test_views.py +++ /dev/null @@ -1,75 +0,0 @@ -############################################################################## -# Copyright (c) 2016 Max Breitenfeldt and others. -# -# All rights reserved. This program and the accompanying materials -# are made available under the terms of the Apache License, Version 2.0 -# which accompanies this distribution, and is available at -# http://www.apache.org/licenses/LICENSE-2.0 -############################################################################## - - -from django.test import TestCase -from django.urls import reverse - -from dashboard.models import Resource -from jenkins.models import JenkinsSlave - - -class DashboardViewTestCase(TestCase): - def setUp(self): - self.slave_active = JenkinsSlave.objects.create(name='slave_active', url='x', active=True) - self.slave_inactive = JenkinsSlave.objects.create(name='slave_inactive', url='x', - active=False) - self.res_active = Resource.objects.create(name='res_active', slave=self.slave_active, - description='x', url='x') - self.res_inactive = Resource.objects.create(name='res_inactive', slave=self.slave_inactive, - description='x', url='x') - - def test_booking_utilization_json(self): - url = reverse('dashboard:booking_utilization', kwargs={'resource_id': 0, 'weeks': 0}) - self.assertEqual(self.client.get(url).status_code, 404) - - url = reverse('dashboard:booking_utilization', kwargs={'resource_id': self.res_active.id, - 'weeks': 0}) - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - self.assertContains(response, 'data') - - def test_jenkins_utilization_json(self): - url = reverse('dashboard:jenkins_utilization', kwargs={'resource_id': 0, 'weeks': 0}) - self.assertEqual(self.client.get(url).status_code, 404) - - url = reverse('dashboard:jenkins_utilization', kwargs={'resource_id': self.res_active.id, - 'weeks': 0}) - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - self.assertContains(response, 'data') - - def test_jenkins_slaves_view(self): - url = reverse('dashboard:jenkins_slaves') - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - self.assertIn(self.slave_active, response.context['slaves']) - self.assertNotIn(self.slave_inactive, response.context['slaves']) - - def test_ci_pods_view(self): - url = reverse('dashboard:ci_pods') - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.context['ci_pods']), 0) - - self.slave_active.ci_slave = True - self.slave_inactive.ci_slave = True - self.slave_active.save() - self.slave_inactive.save() - - response = self.client.get(url) - self.assertIn(self.res_active, response.context['ci_pods']) - self.assertNotIn(self.res_inactive, response.context['ci_pods']) - - def test_dev_pods_view(self): - url = reverse('dashboard:dev_pods') - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.context['dev_pods']), 0) - diff --git a/src/dashboard/urls.py b/src/dashboard/urls.py index 609e5d6..0d7ee87 100644 --- a/src/dashboard/urls.py +++ b/src/dashboard/urls.py @@ -1,5 +1,6 @@ ############################################################################## # Copyright (c) 2016 Max Breitenfeldt and others. +# Copyright (c) 2018 Sawyer Bergeron, Parker Berberian, and others. # # All rights reserved. This program and the accompanying materials # are made available under the terms of the Apache License, Version 2.0 @@ -24,18 +25,12 @@ Including another URLconf 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) """ from django.conf.urls import url - from dashboard.views import * +app_name="dashboard" urlpatterns = [ - url(r'^ci_pods/$', CIPodsView.as_view(), name='ci_pods'), - url(r'^dev_pods/$', DevelopmentPodsView.as_view(), name='dev_pods'), - url(r'^jenkins_slaves/$', JenkinsSlavesView.as_view(), name='jenkins_slaves'), - url(r'^resource/all/$', LabOwnerView.as_view(), name='resources'), - url(r'^resource/(?P[0-9]+)/$', ResourceView.as_view(), name='resource'), - url(r'^resource/(?P[0-9]+)/booking_utilization/(?P-?\d+)/$', - BookingUtilizationJSON.as_view(), name='booking_utilization'), - url(r'^resource/(?P[0-9]+)/jenkins_utilization/(?P-?\d+)/$', - JenkinsUtilizationJSON.as_view(), name='jenkins_utilization'), - url(r'^$', DevelopmentPodsView.as_view(), name="index"), + url(r'^$', landing_view, name='index'), + url(r'^lab/$', lab_list_view, name='all_labs'), + url(r'^lab/(?P.+)/$', lab_detail_view, name='lab_detail'), + url(r'^hosts/$', host_profile_detail_view, name="hostprofile_detail") ] diff --git a/src/dashboard/views.py b/src/dashboard/views.py index 4bab036..2d1f8b2 100644 --- a/src/dashboard/views.py +++ b/src/dashboard/views.py @@ -1,5 +1,6 @@ ############################################################################## # Copyright (c) 2016 Max Breitenfeldt and others. +# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others. # # All rights reserved. This program and the accompanying materials # are made available under the terms of the Apache License, Version 2.0 @@ -8,139 +9,106 @@ ############################################################################## -from datetime import timedelta - -from django.http import JsonResponse from django.shortcuts import get_object_or_404 -from django.utils import timezone -from django.views import View from django.views.generic import TemplateView +from django.shortcuts import render +from django.http import HttpResponseRedirect from booking.models import Booking -from dashboard.models import Resource -from jenkins.models import JenkinsSlave +from account.models import Lab +from resource_inventory.models import * +from workflow.views import * +from workflow.workflow_manager import * -class JenkinsSlavesView(TemplateView): - template_name = "dashboard/jenkins_slaves.html" - def get_context_data(self, **kwargs): - slaves = JenkinsSlave.objects.filter(active=True) - context = super(JenkinsSlavesView, self).get_context_data(**kwargs) - context.update({'title': "Jenkins Slaves", 'slaves': slaves}) - return context +def lab_list_view(request): + labs = Lab.objects.all() + context = {"labs": labs} + return render(request, "dashboard/lab_list.html", context) -class CIPodsView(TemplateView): - template_name = "dashboard/ci_pods.html" - def get_context_data(self, **kwargs): - ci_pods = Resource.objects.filter(slave__ci_slave=True, slave__active=True) - context = super(CIPodsView, self).get_context_data(**kwargs) - context.update({'title': "CI Pods", 'ci_pods': ci_pods}) - return context +def lab_detail_view(request, lab_name): + user = None + if request.user.is_authenticated: + user = request.user + lab = get_object_or_404(Lab, name=lab_name) -class DevelopmentPodsView(TemplateView): - template_name = "dashboard/dev_pods.html" + images = Image.objects.filter(from_lab=lab).filter(public=True) + if user: + images = images | Image.objects.filter(from_lab=lab).filter(owner=user) - def get_context_data(self, **kwargs): - resources = Resource.objects.filter(dev_pod=True) - - bookings = Booking.objects.filter(start__lte=timezone.now()) - bookings = bookings.filter(end__gt=timezone.now()) - - dev_pods = [] - for resource in resources: - booking_utilization = resource.get_booking_utilization(weeks=4) - total = booking_utilization['booked_seconds'] + booking_utilization['available_seconds'] - try: - utilization_percentage = "%d%%" % (float(booking_utilization['booked_seconds']) / - total * 100) - except (ValueError, ZeroDivisionError): - return "" - - dev_pod = (resource, None, utilization_percentage) - for booking in bookings: - if booking.resource == resource: - dev_pod = (resource, booking, utilization_percentage) - dev_pods.append(dev_pod) - - context = super(DevelopmentPodsView, self).get_context_data(**kwargs) - context.update({'title': "Development Pods", 'dev_pods': dev_pods}) - return context + return render(request, "dashboard/lab_detail.html", + {'title': "Lab Overview", + 'lab': lab, + 'hostprofiles': lab.hostprofiles.all(), + 'images': images}) -class ResourceView(TemplateView): - template_name = "dashboard/resource.html" +def host_profile_detail_view(request): - def get_context_data(self, **kwargs): - resource = get_object_or_404(Resource, id=self.kwargs['resource_id']) - bookings = Booking.objects.filter(resource=resource, end__gt=timezone.now()) - context = super(ResourceView, self).get_context_data(**kwargs) - context.update({'title': str(resource), 'resource': resource, 'bookings': bookings}) - return context + return render(request, "dashboard/host_profile_detail.html", + {'title': "Host Types", + }) -class LabOwnerView(TemplateView): - template_name = "dashboard/resource_all.html" +def landing_view(request): + manager = None + manager_detected = False + if 'manager_session' in request.session: - def get_context_data(self, **kwargs): - resources = Resource.objects.filter(slave__dev_pod=True, slave__active=True) - pods = [] - for resource in resources: - utilization = resource.slave.get_utilization(timedelta(days=7)) - bookings = Booking.objects.filter(resource=resource, end__gt=timezone.now()) - pods.append((resource, utilization, bookings)) - context = super(LabOwnerView, self).get_context_data(**kwargs) - context.update({'title': "Overview", 'pods': pods}) - return context + try: + manager = ManagerTracker.managers[request.session['manager_session']] + + + except KeyError as e: + pass + + if manager is not None: + #no manager detected, don't display continue button + manager_detected = True + if request.method == 'GET': + return render(request, 'dashboard/landing.html', {'manager': manager_detected, 'title': "Welcome!"}) -class BookingUtilizationJSON(View): - def get(self, request, *args, **kwargs): - resource = get_object_or_404(Resource, id=kwargs['resource_id']) - utilization = resource.get_booking_utilization(int(kwargs['weeks'])) - utilization = [ - { - 'label': 'Booked', - 'data': utilization['booked_seconds'], - 'color': '#d9534f' - }, - { - 'label': 'Available', - 'data': utilization['available_seconds'], - 'color': '#5cb85c' - }, - ] - return JsonResponse({'data': utilization}) - - -class JenkinsUtilizationJSON(View): - def get(self, request, *args, **kwargs): - resource = get_object_or_404(Resource, id=kwargs['resource_id']) - weeks = int(kwargs['weeks']) + if request.method == 'POST': try: - utilization = resource.slave.get_utilization(timedelta(weeks=weeks)) - utilization = [ - { - 'label': 'Offline', - 'data': utilization['offline'], - 'color': '#d9534f' - }, - { - 'label': 'Online', - 'data': utilization['online'], - 'color': '#5cb85c' - }, - { - 'label': 'Idle', - 'data': utilization['idle'], - 'color': '#5bc0de' - }, - ] - jutilization = JsonResponse({'data': utilization}) - except AttributeError: - return JsonResponse({'data': ''}) - if jutilization: - return jutilization + create = request.POST['create'] + + if manager is not None: + del manager + + mgr_uuid = create_session(create, request=request,) + request.session['manager_session'] = mgr_uuid + return HttpResponseRedirect('/wf/') + + except KeyError as e: + pass + + +class LandingView(TemplateView): + template_name = "dashboard/landing.html" + + def get_context_data(self, **kwargs): + context = super(LandingView, self).get_context_data(**kwargs) + + hosts = [] + + for host_profile in HostProfile.objects.all(): + name = host_profile.name + description = host_profile.description + in_labs = host_profile.labs + + interfaces = host_profile.interfaceprofile + storage = host_profile.storageprofile + cpu = host_profile.cpuprofile + ram = host_profile.ramprofile + + host = (name, description, in_labs, interfaces, storage, cpu, ram) + hosts.append(host) + + context.update({'hosts': hosts}) + + return context diff --git a/src/jenkins/adapter.py b/src/jenkins/adapter.py deleted file mode 100644 index b48b868..0000000 --- a/src/jenkins/adapter.py +++ /dev/null @@ -1,137 +0,0 @@ -############################################################################## -# Copyright (c) 2016 Max Breitenfeldt and others. -# -# All rights reserved. This program and the accompanying materials -# are made available under the terms of the Apache License, Version 2.0 -# which accompanies this distribution, and is available at -# http://www.apache.org/licenses/LICENSE-2.0 -############################################################################## - - -import logging -import re -from django.conf import settings - -import requests -from django.core.cache import cache - -logger = logging.getLogger(__name__) - -# TODO: implement caching decorator, cache get_* functions -def get_json(url): - if cache.get(url) is None: - try: - response = requests.get(url) - json = response.json() - cache.set(url, json, 180) # cache result for 180 seconds - return json - except requests.exceptions.RequestException as e: - logger.exception(e) - except ValueError as e: - logger.exception(e) - else: - return cache.get(url) - - -def get_all_slaves(): - url = settings.ALL_SLAVES_URL - json = get_json(url) - if json is not None: - return json['computer'] # return list of dictionaries - return [] - - - - -def get_slave(slavename): - slaves = get_all_slaves() - for slave in slaves: - if slave['displayName'] == slavename: - return slave - return {} - - -def get_ci_slaves(): - url = settings.CI_SLAVES_URL - json = get_json(url) - if json is not None: - return json['nodes'] - return [] - - -def get_all_jobs(): - url = settings.ALL_JOBS_URL - json = get_json(url) - if json is not None: - return json['jobs'] # return list of dictionaries - return [] - - -def get_jenkins_job(slavename): - jobs = get_all_jobs() - max_time = 0 - last_job = None - for job in jobs: - if job['lastBuild'] is not None: - if job['lastBuild']['builtOn'] == slavename: - if job['lastBuild']['building'] is True: - return job # return active build - if job['lastBuild']['timestamp'] > max_time: - last_job = job - max_time = job['lastBuild']['timestamp'] - return last_job - - -def is_ci_slave(slavename): - ci_slaves = get_ci_slaves() - for ci_slave in ci_slaves: - if ci_slave['nodeName'] == slavename: - return True - return False - - -def is_dev_pod(slavename): - if is_ci_slave(slavename): - return False - if slavename.find('pod') != -1: - return True - return False - - -def parse_job(job): - result = parse_job_string(job['lastBuild']['fullDisplayName']) - result['building'] = job['lastBuild']['building'] - result['result'] = '' - if not job['lastBuild']['building']: - result['result'] = job['lastBuild']['result'] - result['url'] = job['url'] - return result - - -def parse_job_string(full_displayname): - job = {} - job['scenario'] = '' - job['installer'] = '' - job['branch'] = '' - tokens = re.split(r'[ -]', full_displayname) - for i in range(len(tokens)): - if tokens[i] == 'os': - job['scenario'] = '-'.join(tokens[i: i + 4]) - elif tokens[i] in ['fuel', 'joid', 'apex', 'compass']: - job['installer'] = tokens[i] - elif tokens[i] in ['master', 'arno', 'brahmaputra', 'colorado']: - job['branch'] = tokens[i] - tokens = full_displayname.split(' ') - job['name'] = tokens[0] - return job - -def get_slave_url(slave): - return settings.GET_SLAVE_URL + slave['displayName'] - - -def get_slave_status(slave): - if not slave['offline'] and slave['idle']: - return 'online / idle' - if not slave['offline']: - return 'online' - return 'offline' diff --git a/src/jenkins/migrations/0001_initial.py b/src/jenkins/migrations/0001_initial.py deleted file mode 100644 index b1c7889..0000000 --- a/src/jenkins/migrations/0001_initial.py +++ /dev/null @@ -1,53 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10 on 2016-11-03 13:33 -from __future__ import unicode_literals - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='JenkinsSlave', - fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('name', models.CharField(max_length=100, unique=True)), - ('status', models.CharField(default='offline', max_length=30)), - ('url', models.CharField(max_length=1024)), - ('ci_slave', models.BooleanField(default=False)), - ('dev_pod', models.BooleanField(default=False)), - ('building', models.BooleanField(default=False)), - ('last_job_name', models.CharField(default='', max_length=1024)), - ('last_job_url', models.CharField(default='', max_length=1024)), - ('last_job_scenario', models.CharField(default='', max_length=50)), - ('last_job_branch', models.CharField(default='', max_length=50)), - ('last_job_installer', models.CharField(default='', max_length=50)), - ('last_job_result', models.CharField(default='', max_length=30)), - ('active', models.BooleanField(default=False)), - ], - options={ - 'db_table': 'jenkins_slave', - }, - ), - migrations.CreateModel( - name='JenkinsStatistic', - fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('offline', models.BooleanField(default=False)), - ('idle', models.BooleanField(default=False)), - ('online', models.BooleanField(default=False)), - ('timestamp', models.DateTimeField(auto_now_add=True)), - ('slave', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='jenkins.JenkinsSlave')), - ], - options={ - 'db_table': 'jenkins_statistic', - }, - ), - ] diff --git a/src/jenkins/models.py b/src/jenkins/models.py deleted file mode 100644 index 8254ff3..0000000 --- a/src/jenkins/models.py +++ /dev/null @@ -1,62 +0,0 @@ -############################################################################## -# Copyright (c) 2016 Max Breitenfeldt and others. -# -# All rights reserved. This program and the accompanying materials -# are made available under the terms of the Apache License, Version 2.0 -# which accompanies this distribution, and is available at -# http://www.apache.org/licenses/LICENSE-2.0 -############################################################################## - - -from django.db import models -from django.utils import timezone - - -class JenkinsSlave(models.Model): - id = models.AutoField(primary_key=True) - name = models.CharField(max_length=100, unique=True) - status = models.CharField(max_length=30, default='offline') - url = models.CharField(max_length=1024) - ci_slave = models.BooleanField(default=False) - dev_pod = models.BooleanField(default=False) - - building = models.BooleanField(default=False) - - last_job_name = models.CharField(max_length=1024, default='') - last_job_url = models.CharField(max_length=1024, default='') - last_job_scenario = models.CharField(max_length=50, default='') - last_job_branch = models.CharField(max_length=50, default='') - last_job_installer = models.CharField(max_length=50, default='') - last_job_result = models.CharField(max_length=30, default='') - - active = models.BooleanField(default=False) - - def get_utilization(self, timedelta): - """ - Return a dictionary containing the count of idle, online and offline measurements in the time from - now-timedelta to now - """ - utilization = {'idle': 0, 'online': 0, 'offline': 0} - statistics = self.jenkinsstatistic_set.filter(timestamp__gte=timezone.now() - timedelta) - utilization['idle'] = statistics.filter(idle=True).count() - utilization['online'] = statistics.filter(online=True).count() - utilization['offline'] = statistics.filter(offline=True).count() - return utilization - - class Meta: - db_table = 'jenkins_slave' - - def __str__(self): - return self.name - - -class JenkinsStatistic(models.Model): - id = models.AutoField(primary_key=True) - slave = models.ForeignKey(JenkinsSlave, on_delete=models.CASCADE) - offline = models.BooleanField(default=False) - idle = models.BooleanField(default=False) - online = models.BooleanField(default=False) - timestamp = models.DateTimeField(auto_now_add=True) - - class Meta: - db_table = 'jenkins_statistic' diff --git a/src/jenkins/tasks.py b/src/jenkins/tasks.py deleted file mode 100644 index ea986c1..0000000 --- a/src/jenkins/tasks.py +++ /dev/null @@ -1,64 +0,0 @@ -############################################################################## -# Copyright (c) 2016 Max Breitenfeldt and others. -# -# All rights reserved. This program and the accompanying materials -# are made available under the terms of the Apache License, Version 2.0 -# which accompanies this distribution, and is available at -# http://www.apache.org/licenses/LICENSE-2.0 -############################################################################## - - -from celery import shared_task - -from dashboard.models import Resource -from jenkins.models import JenkinsSlave, JenkinsStatistic -from .adapter import * - - -@shared_task -def sync_jenkins(): - update_jenkins_slaves() - - -def update_jenkins_slaves(): - JenkinsSlave.objects.all().update(active=False) - - jenkins_slaves = get_all_slaves() - for slave in jenkins_slaves: - jenkins_slave, created = JenkinsSlave.objects.get_or_create(name=slave['displayName'], - url=get_slave_url(slave)) - jenkins_slave.active = True - jenkins_slave.ci_slave = is_ci_slave(slave['displayName']) - jenkins_slave.dev_pod = is_dev_pod(slave['displayName']) - jenkins_slave.status = get_slave_status(slave) - - # if this is a new slave and a pod, check if there is a resource for it, create one if not - if created and 'pod' in slave['displayName']: - # parse resource name from slave name - # naming example: orange-pod1, resource name: Orange POD 1 - tokens = slave['displayName'].split('-') - name = tokens[0].capitalize() + ' POD '# company name - name += tokens[1][3:] # remove 'pod' - resource, created = Resource.objects.get_or_create(name=name) - resource.slave = jenkins_slave - resource.save() - - last_job = get_jenkins_job(jenkins_slave.name) - if last_job is not None: - last_job = parse_job(last_job) - jenkins_slave.last_job_name = last_job['name'] - jenkins_slave.last_job_url = last_job['url'] - jenkins_slave.last_job_scenario = last_job['scenario'] - jenkins_slave.last_job_branch = last_job['branch'] - jenkins_slave.last_job_installer = last_job['installer'] - jenkins_slave.last_job_result = last_job['result'] - jenkins_slave.save() - - jenkins_statistic = JenkinsStatistic(slave=jenkins_slave) - if jenkins_slave.status == 'online' or jenkins_slave.status == 'building': - jenkins_statistic.online = True - if jenkins_slave.status == 'offline': - jenkins_statistic.offline = True - if jenkins_slave.status == 'online / idle': - jenkins_statistic.idle = True - jenkins_statistic.save() diff --git a/src/jenkins/tests.py b/src/jenkins/tests.py deleted file mode 100644 index 3723cd3..0000000 --- a/src/jenkins/tests.py +++ /dev/null @@ -1,129 +0,0 @@ -############################################################################## -# Copyright (c) 2016 Max Breitenfeldt and others. -# -# All rights reserved. This program and the accompanying materials -# are made available under the terms of the Apache License, Version 2.0 -# which accompanies this distribution, and is available at -# http://www.apache.org/licenses/LICENSE-2.0 -############################################################################## - - -from datetime import timedelta -from unittest import TestCase - -import jenkins.adapter as jenkins -from jenkins.models import * - - -# Tests that the data we get with the jenkinsadapter contains all the -# data we need. These test will fail if; -# - there is no internet connection -# - the opnfv jenkins url has changed -# - the jenkins api has changed -# - jenkins is not set up / there is no data -class JenkinsAdapterTestCase(TestCase): - def test_get_all_slaves(self): - slaves = jenkins.get_all_slaves() - self.assertTrue(len(slaves) > 0) - for slave in slaves: - self.assertTrue('displayName' in slave) - self.assertTrue('idle' in slave) - self.assertTrue('offline' in slave) - - def test_get_slave(self): - slaves = jenkins.get_all_slaves() - self.assertEqual(slaves[0], jenkins.get_slave(slaves[0]['displayName'])) - self.assertEqual({}, jenkins.get_slave('098f6bcd4621d373cade4e832627b4f6')) - - def test_get_ci_slaves(self): - slaves = jenkins.get_ci_slaves() - self.assertTrue(len(slaves) > 0) - for slave in slaves: - self.assertTrue('nodeName' in slave) - - def test_get_jenkins_job(self): - slaves = jenkins.get_ci_slaves() - job = None - for slave in slaves: - job = jenkins.get_jenkins_job(slave['nodeName']) - if job is not None: - break - # We need to test at least one job - self.assertNotEqual(job, None) - - def test_get_all_jobs(self): - jobs = jenkins.get_all_jobs() - lastBuild = False - self.assertTrue(len(jobs) > 0) - for job in jobs: - self.assertTrue('displayName' in job) - self.assertTrue('url' in job) - self.assertTrue('lastBuild' in job) - if job['lastBuild'] is not None: - lastBuild = True - self.assertTrue('building' in job['lastBuild']) - self.assertTrue('fullDisplayName' in job['lastBuild']) - self.assertTrue('result' in job['lastBuild']) - self.assertTrue('timestamp' in job['lastBuild']) - self.assertTrue('builtOn' in job['lastBuild']) - self.assertTrue(lastBuild) - - def test_parse_job(self): - job = { - "displayName": "apex-deploy-baremetal-os-nosdn-fdio-noha-colorado", - "url": "https://build.opnfv.org/ci/job/apex-deploy-baremetal-os-nosdn-fdio-noha-colorado/", - "lastBuild": { - "building": False, - "fullDisplayName": "apex-deploy-baremetal-os-nosdn-fdio-noha-colorado #37", - "result": "SUCCESS", - "timestamp": 1476283629917, - "builtOn": "lf-pod1" - } - } - - job = jenkins.parse_job(job) - self.assertEqual(job['scenario'], 'os-nosdn-fdio-noha') - self.assertEqual(job['installer'], 'apex') - self.assertEqual(job['branch'], 'colorado') - self.assertEqual(job['result'], 'SUCCESS') - self.assertEqual(job['building'], False) - self.assertEqual(job['url'], - "https://build.opnfv.org/ci/job/apex-deploy-baremetal-os-nosdn-fdio-noha-colorado/") - self.assertEqual(job['name'], - 'apex-deploy-baremetal-os-nosdn-fdio-noha-colorado') - - def test_get_slave_status(self): - slave = { - 'offline': True, - 'idle': False - } - self.assertEqual(jenkins.get_slave_status(slave), 'offline') - slave = { - 'offline': False, - 'idle': False - } - self.assertEqual(jenkins.get_slave_status(slave), 'online') - slave = { - 'offline': False, - 'idle': True - } - self.assertEqual(jenkins.get_slave_status(slave), 'online / idle') - - -class JenkinsModelTestCase(TestCase): - def test_get_utilization(self): - jenkins_slave = JenkinsSlave.objects.create(name='test', status='offline', url='') - utilization = jenkins_slave.get_utilization(timedelta(weeks=1)) - self.assertEqual(utilization['idle'], 0) - self.assertEqual(utilization['offline'], 0) - self.assertEqual(utilization['online'], 0) - - for i in range(10): - JenkinsStatistic.objects.create(slave=jenkins_slave, - offline=True, idle=True, - online=True) - - utilization = jenkins_slave.get_utilization(timedelta(weeks=1)) - self.assertEqual(utilization['idle'], 10) - self.assertEqual(utilization['offline'], 10) - self.assertEqual(utilization['online'], 10) diff --git a/src/manage.py b/src/manage.py old mode 100644 new mode 100755 diff --git a/src/notifier/__init__.py b/src/notifier/__init__.py index e69de29..d65b13a 100644 --- a/src/notifier/__init__.py +++ b/src/notifier/__init__.py @@ -0,0 +1,8 @@ +############################################################################## +# Copyright (c) 2018 Sawyer Bergeron and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## diff --git a/src/notifier/admin.py b/src/notifier/admin.py index cfbe778..d3e8be5 100644 --- a/src/notifier/admin.py +++ b/src/notifier/admin.py @@ -1,5 +1,5 @@ ############################################################################## -# Copyright (c) 2016 Max Breitenfeldt and others. +# Copyright (c) 2018 Sawyer Bergeron, Parker Berberian, and others. # # All rights reserved. This program and the accompanying materials # are made available under the terms of the Apache License, Version 2.0 @@ -12,3 +12,5 @@ from django.contrib import admin from notifier.models import * admin.site.register(Notifier) +admin.site.register(MetaBooking) +admin.site.register(LabMessage) diff --git a/src/notifier/apps.py b/src/notifier/apps.py index da5d3b0..52902da 100644 --- a/src/notifier/apps.py +++ b/src/notifier/apps.py @@ -1,5 +1,5 @@ ############################################################################## -# Copyright (c) 2016 Max Breitenfeldt and others. +# Copyright (c) 2018 Sawyer Bergeron, Parker Berberian, and others. # # All rights reserved. This program and the accompanying materials # are made available under the terms of the Apache License, Version 2.0 @@ -7,6 +7,7 @@ # http://www.apache.org/licenses/LICENSE-2.0 ############################################################################## + from django.apps import AppConfig diff --git a/src/notifier/dispatchers.py b/src/notifier/dispatchers.py index c35fe2b..1b66b37 100644 --- a/src/notifier/dispatchers.py +++ b/src/notifier/dispatchers.py @@ -1,5 +1,5 @@ ############################################################################## -# Copyright (c) 2016 Max Breitenfeldt and others. +# Copyright (c) 2018 Sawyer Bergeron, Parker Berberian, and others. # # All rights reserved. This program and the accompanying materials # are made available under the terms of the Apache License, Version 2.0 @@ -30,4 +30,4 @@ class DispatchHandler(): instance.sender,[instance.user.email_addr], fail_silently=False) def webnotification(instance): - instance.msg_sent='by web notification' \ No newline at end of file + instance.msg_sent='by web notification' diff --git a/src/notifier/manager.py b/src/notifier/manager.py new file mode 100644 index 0000000..a705d00 --- /dev/null +++ b/src/notifier/manager.py @@ -0,0 +1,98 @@ +############################################################################## +# Copyright (c) 2018 Sawyer Bergeron and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + +from booking.models import * +from notifier.models import Notifier, MetaBooking, LabMessage +from django.utils import timezone +from datetime import timedelta +from django.template import Template, Context +from account.models import UserProfile + +from django.db import models + +class NotifyPeriodic(object): + def task(): + bookings_new = Booking.objects.filter(metabooking__isnull=True) + bookings_old = Booking.objects.filter(end__lte=timezone.now() + timedelta(hours=24)).filter(metabooking__ended_notified=False) + + for booking in bookings_old: + metabooking = booking.metabooking + if booking.end <= timezone.now() + timedelta(hours=24): + if not metabooking.ending_notified: + Notify().notify(Notify.TOCLEAN, booking) + metabooking.ending_notified = True + metabooking.save() + if booking.end <= timezone.now(): + metabooking = booking.metabooking + if not metabooking.ended_notified: + Notify().notify(Notify.CLEANED, booking) + metabooking.ended_notified = True + metabooking.save() + + for booking in bookings_new: + metabooking = MetaBooking() + metabooking.booking = booking + metabooking.created_notified = True + metabooking.save() + + Notify().notify(Notify.CREATED, booking) + + +class Notify(object): + + CREATED = "created" + TOCLEAN = "toclean" + CLEANED = "cleaned" + + TITLES = {} + TITLES["created"] = "Your booking has been confirmed" + TITLES["toclean"] = "Your booking is ending soon" + TITLES["cleaned"] = "Your booking has ended" + + """ + Lab message is provided with the following context elements: + * if is for owner or for collaborator (if owner) + * recipient username (.username) + * recipient full name (.userprofile.full_name) + * booking it pertains to (booking) + * status message should convey (currently "created", "toclean" and "cleaned" as strings) + It should be a django template that can be rendered with these context elements + and should generally use all of them in one way or another. + It should be applicable to email, the web based general view, and should be scalable for + all device formats across those mediums. + """ + def notify(self, notifier_type, booking): + template = Template(LabMessage.objects.filter(lab=booking.lab).first().msg) + + context = {} + context["owner"] = booking.owner + context["notify_type"] = notifier_type + context["booking"] = booking + message = template.render(Context(context)) + notifier = Notifier() + notifier.title = self.TITLES[notifier_type] + notifier.content = message + notifier.user = booking.owner.userprofile + notifier.sender = str(booking.lab) + notifier.save() + notifier.send() + + + context["owner"] = False + + for user in booking.collaborators.all(): + context["collaborator"] = user + message = template.render(Context(context)) + notifier = Notifier() + notifier.title = self.TITLES[notifier_type] + notifier.content = message + notifier.user = UserProfile.objects.get(user=user) + notifier.sender = str(booking.lab) + notifier.save() + notifier.send() diff --git a/src/notifier/migrations/0001_initial.py b/src/notifier/migrations/0001_initial.py index cac4d04..e5d0009 100644 --- a/src/notifier/migrations/0001_initial.py +++ b/src/notifier/migrations/0001_initial.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10 on 2017-12-14 21:41 -from __future__ import unicode_literals +# Generated by Django 2.1 on 2018-09-14 14:48 from django.db import migrations, models import django.db.models.deletion @@ -12,10 +10,29 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('dashboard', '0002_auto_20170505_0815'), + ('account', '0001_initial'), + ('booking', '0001_initial'), ] operations = [ + migrations.CreateModel( + name='LabMessage', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('msg', models.TextField()), + ('lab', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='account.Lab')), + ], + ), + migrations.CreateModel( + name='MetaBooking', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('ending_notified', models.BooleanField(default=False)), + ('ended_notified', models.BooleanField(default=False)), + ('created_notified', models.BooleanField(default=False)), + ('booking', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='metabooking', to='booking.Booking')), + ], + ), migrations.CreateModel( name='Notifier', fields=[ diff --git a/src/notifier/models.py b/src/notifier/models.py index 9ebc6fc..ed0edeb 100644 --- a/src/notifier/models.py +++ b/src/notifier/models.py @@ -1,5 +1,5 @@ ############################################################################## -# Copyright (c) 2016 Max Breitenfeldt and others. +# Copyright (c) 2018 Sawyer Bergeron, Parker Berberian, and others. # # All rights reserved. This program and the accompanying materials # are made available under the terms of the Apache License, Version 2.0 @@ -8,14 +8,24 @@ ############################################################################## from django.db import models -from jira import JIRA, JIRAError -from dashboard.models import Resource from booking.models import Booking -from django.contrib.auth.models import User from account.models import UserProfile -from django.contrib import messages -from django.db.models.signals import pre_save from fernet_fields import EncryptedTextField +from account.models import Lab + + +class MetaBooking(models.Model): + id = models.AutoField(primary_key=True) + booking = models.OneToOneField(Booking, on_delete=models.CASCADE, related_name="metabooking") + ending_notified = models.BooleanField(default=False) + ended_notified = models.BooleanField(default=False) + created_notified = models.BooleanField(default=False) + + +class LabMessage(models.Model): + lab = models.ForeignKey(Lab, on_delete=models.CASCADE) + msg = models.TextField() # django template should be put here + class Notifier(models.Model): id = models.AutoField(primary_key=True) @@ -24,15 +34,18 @@ class Notifier(models.Model): user = models.ForeignKey(UserProfile, on_delete=models.CASCADE, null=True, blank=True) sender = models.CharField(max_length=240, default='unknown') message_type = models.CharField(max_length=240, default='email', choices=( - ('email','Email'), + ('email', 'Email'), ('webnotification', 'Web Notification'))) msg_sent = '' - import notifier.dispatchers - def __str__(self): return self.title + """ + Implement for next PR: send Notifier by media agreed to by user + """ + def send(self): + pass + def getEmail(self): return self.user.email_addr - diff --git a/src/jenkins/admin.py b/src/notifier/tests/test_dispatcher.py similarity index 58% rename from src/jenkins/admin.py rename to src/notifier/tests/test_dispatcher.py index c499670..07d8387 100644 --- a/src/jenkins/admin.py +++ b/src/notifier/tests/test_dispatcher.py @@ -1,5 +1,5 @@ ############################################################################## -# Copyright (c) 2016 Max Breitenfeldt and others. +# Copyright (c) 2018 Sawyer Bergeron and others. # # All rights reserved. This program and the accompanying materials # are made available under the terms of the Apache License, Version 2.0 @@ -8,10 +8,10 @@ ############################################################################## -from django.conf import settings -from django.contrib import admin +from django.test import TestCase +from notifier.models import * +from django.contrib.auth.models import User -from jenkins.models import JenkinsSlave - -if settings.DEBUG: - admin.site.register(JenkinsSlave) \ No newline at end of file +class DispatchTestCase(TestCase): + # This is a stub, it will be filled out as this feature is remade with saner practices. + pass diff --git a/src/notifier/tests/test_models.py b/src/notifier/tests/test_models.py new file mode 100644 index 0000000..10aec3e --- /dev/null +++ b/src/notifier/tests/test_models.py @@ -0,0 +1,29 @@ +############################################################################## +# Copyright (c) 2018 Sawyer Bergeron and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + + +from django.test import TestCase +from notifier.models import * +from django.contrib.auth.models import User + +class NotifierTestCase(TestCase): + + def test_valid_notifier_saves(self): + + sender = User.objects.create() + recipient = User.objects.create() + self.assertTrue( + Notifier.objects.create( + title='notification title', + content='notification body', + user=recipient, + sender=sender, + message_type='email' + ) + ) diff --git a/src/notifier/urls.py b/src/notifier/urls.py new file mode 100644 index 0000000..9bbc3bf --- /dev/null +++ b/src/notifier/urls.py @@ -0,0 +1,21 @@ +############################################################################## +# Copyright (c) 2018 Sawyer Bergeron, Parker Berberian, and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + + +from django.conf.urls import url + +from notifier.views import * + +app_name = "notifier" +urlpatterns = [ + + + url(r'^$', InboxView, name='messages'), + url(r'^notification/(?P[0-9]+)/$', NotificationView, name='notifier_single') +] diff --git a/src/notifier/views.py b/src/notifier/views.py new file mode 100644 index 0000000..026894a --- /dev/null +++ b/src/notifier/views.py @@ -0,0 +1,34 @@ +############################################################################## +# Copyright (c) 2018 Sawyer Bergeron and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + +from notifier.models import * +from django.shortcuts import render + +def InboxView(request): + if request.user.is_authenticated: + user = request.user + else: + return render(request, "dashboard/login.html", {'title': 'Authentication Required'}) + + return render(request, "notifier/inbox.html", {'notifier_messages': Notifier.objects.filter(user=user.userprofile)}) + + +def NotificationView(request, notification_id): + if notification_id == 0: + pass + if request.user.is_authenticated: + user = request.user + else: + return render(request, "dashboard/login.html", {'title': 'Authentication Required'}) + + notification = Notifier.objects.get(id=notification_id) + if not notification.user.user.username == user.username: + return render(request, "dashboard/login.html", {'title': 'Access Denied'}) + + return render(request, "notifier/notification.html", {'notification': notification}) diff --git a/src/pharos_dashboard/settings.py b/src/pharos_dashboard/settings.py index 8155fd4..7fccb32 100644 --- a/src/pharos_dashboard/settings.py +++ b/src/pharos_dashboard/settings.py @@ -1,3 +1,11 @@ +############################################################################## +# Copyright (c) 2018 Sawyer Bergeron and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## import os from datetime import timedelta @@ -8,15 +16,18 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # NOTE: os.environ only returns strings, so making a comparison to # 'True' here will convert it to the correct Boolean value. DEBUG = os.environ['DEBUG'] == 'True' +TESTING = os.environ['TEST'] == 'True' # Application definition INSTALLED_APPS = [ 'dashboard', + 'resource_inventory', 'booking', 'account', - 'jenkins', 'notifier', + 'workflow', + 'api', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -51,6 +62,7 @@ TEMPLATES = [ 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ + 'dashboard.context_processors.debug', 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', @@ -60,6 +72,10 @@ TEMPLATES = [ }, ] +TEMPLATE_CONTEXT_PROCESSORS = [ + 'dashboard.context_processors.debug', +] + WSGI_APPLICATION = 'pharos_dashboard.wsgi.application' # Password validation @@ -128,13 +144,12 @@ DATABASES = { } } - # Rest API Settings REST_FRAMEWORK = { 'DEFAULT_PERMISSION_CLASSES': [ 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly' ], - 'DEFAULT_FILTER_BACKENDS': ('rest_framework.filters.DjangoFilterBackend',), + 'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.FilterSet',), 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.TokenAuthentication', @@ -170,32 +185,20 @@ RABBITMQ_DEFAULT_PASS = os.environ['RABBITMQ_DEFAULT_PASS'] BROKER_URL = 'amqp://' + RABBITMQ_DEFAULT_USER + ':' + RABBITMQ_DEFAULT_PASS + '@rabbitmq:5672//' -BOOKING_EXP_TIME = os.environ['BOOKING_EXPIRE_TIME'] -BOOKING_MAX_NUM = os.environ['BOOKING_MAXIMUM_NUMBER'] - CELERYBEAT_SCHEDULE = { - 'sync-jenkins': { - 'task': 'jenkins.tasks.sync_jenkins', - 'schedule': timedelta(minutes=5) - }, - 'send-booking-notifications': { - 'task': 'notification.tasks.send_booking_notifications', - 'schedule': timedelta(minutes=5) + 'booking_poll': { + 'task': 'dashboard.tasks.booking_poll', + 'schedule': timedelta(minutes=1) }, - 'clean-database': { - 'task': 'dashboard.tasks.database_cleanup', - 'schedule': timedelta(hours=24) + 'free_hosts': { + 'task': 'dashboard.tasks.free_hosts', + 'schedule': timedelta(minutes=1) }, - 'booking_cleanup': { - 'task': 'dashboard.tasks.booking_cleanup', - 'schedule': timedelta(hours=24) + 'conjure_notifiers': { + 'task': 'dashboard.tasks.conjure_aggregate_notifiers', + 'schedule': timedelta(seconds=30) }, } -# Jenkins Settings -ALL_SLAVES_URL = os.environ['JENKINS_URL'] + '/computer/api/json?tree=computer[displayName,offline,idle]' -CI_SLAVES_URL = os.environ['JENKINS_URL'] + '/label/ci-pod/api/json?tree=nodes[nodeName,offline,idle]' -ALL_JOBS_URL = os.environ['JENKINS_URL'] + '/api/json?tree=jobs[displayName,url,lastBuild[fullDisplayName,building,builtOn,timestamp,result]' -GET_SLAVE_URL = os.environ['JENKINS_URL'] + '/computer/' # Notifier Settings EMAIL_HOST = os.environ['EMAIL_HOST'] @@ -204,3 +207,4 @@ EMAIL_HOST_USER = os.environ['EMAIL_HOST_USER'] EMAIL_HOST_PASSWORD = os.environ['EMAIL_HOST_PASSWORD'] EMAIL_USE_TLS=True DEFAULT_EMAIL_FROM = os.environ.get('DEFAULT_EMAIL_FROM', 'webmaster@localhost') +SESSION_ENGINE="django.contrib.sessions.backends.signed_cookies" diff --git a/src/pharos_dashboard/urls.py b/src/pharos_dashboard/urls.py index adcb5b8..8535bed 100644 --- a/src/pharos_dashboard/urls.py +++ b/src/pharos_dashboard/urls.py @@ -1,5 +1,6 @@ ############################################################################## # Copyright (c) 2016 Max Breitenfeldt and others. +# Copyright (c) 2018 Sawyer Bergeron, Parker Berberian, and others. # # All rights reserved. This program and the accompanying materials # are made available under the terms of the Apache License, Version 2.0 @@ -30,15 +31,17 @@ from django.contrib import admin urlpatterns = [ + + url(r'^wf/', include('workflow.urls', namespace='workflow')), url(r'^', include('dashboard.urls', namespace='dashboard')), url(r'^booking/', include('booking.urls', namespace='booking')), url(r'^accounts/', include('account.urls', namespace='account')), - + url(r'^resource/', include('resource_inventory.urls', namespace='resource')), url(r'^admin/', admin.site.urls), url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')), - - url(r'^api/', include('api.urls')) + url(r'^api/', include('api.urls')), + url(r'^messages/', include('notifier.urls', namespace='notifier')) ] if settings.DEBUG is True: - urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \ No newline at end of file + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/src/jenkins/migrations/__init__.py b/src/resource_inventory/__init__.py similarity index 85% rename from src/jenkins/migrations/__init__.py rename to src/resource_inventory/__init__.py index b5914ce..f903394 100644 --- a/src/jenkins/migrations/__init__.py +++ b/src/resource_inventory/__init__.py @@ -1,10 +1,8 @@ ############################################################################## -# Copyright (c) 2016 Max Breitenfeldt and others. +# Copyright (c) 2018 Sawyer Bergeron, Parker Berberian, and others. # # All rights reserved. This program and the accompanying materials # are made available under the terms of the Apache License, Version 2.0 # which accompanies this distribution, and is available at # http://www.apache.org/licenses/LICENSE-2.0 ############################################################################## - - diff --git a/src/resource_inventory/admin.py b/src/resource_inventory/admin.py new file mode 100644 index 0000000..222877a --- /dev/null +++ b/src/resource_inventory/admin.py @@ -0,0 +1,29 @@ +############################################################################## +# Copyright (c) 2016 Max Breitenfeldt and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + + +from django.contrib import admin + +from resource_inventory.models import * + +profiles = [HostProfile, InterfaceProfile, DiskProfile, CpuProfile, RamProfile] + +admin.site.register(profiles) + +generics = [GenericResourceBundle, GenericResource, GenericHost, GenericPod, GenericInterface] + +admin.site.register(generics) + +physical = [Host, Interface, Network, Vlan, ResourceBundle] + +admin.site.register(physical) + +config = [Scenario, Installer, Opsys, ConfigBundle, OPNFVConfig, OPNFVRole, Image, HostConfiguration] + +admin.site.register(config) diff --git a/src/jenkins/apps.py b/src/resource_inventory/apps.py similarity index 89% rename from src/jenkins/apps.py rename to src/resource_inventory/apps.py index 41faf60..79768a7 100644 --- a/src/jenkins/apps.py +++ b/src/resource_inventory/apps.py @@ -7,9 +7,8 @@ # http://www.apache.org/licenses/LICENSE-2.0 ############################################################################## - from django.apps import AppConfig -class JenkinsConfig(AppConfig): - name = 'jenkins' +class ResourcesConfig(AppConfig): + name = 'hwresource' diff --git a/src/resource_inventory/migrations/0001_initial.py b/src/resource_inventory/migrations/0001_initial.py new file mode 100644 index 0000000..d01e8e7 --- /dev/null +++ b/src/resource_inventory/migrations/0001_initial.py @@ -0,0 +1,328 @@ +# Generated by Django 2.1 on 2018-09-14 14:48 + +from django.conf import settings +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('account', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='ConfigBundle', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=200, unique=True)), + ('description', models.CharField(default='', max_length=1000)), + ], + ), + migrations.CreateModel( + name='CpuProfile', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('cores', models.IntegerField()), + ('architecture', models.CharField(choices=[('x86_64', 'x86_64'), ('aarch64', 'aarch64')], max_length=50)), + ('cpus', models.IntegerField()), + ('cflags', models.TextField(null=True)), + ], + ), + migrations.CreateModel( + name='DiskProfile', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('size', models.IntegerField()), + ('media_type', models.CharField(choices=[('SSD', 'SSD'), ('HDD', 'HDD')], max_length=50)), + ('name', models.CharField(max_length=50)), + ('rotation', models.IntegerField(default=0)), + ('interface', models.CharField(choices=[('sata', 'sata'), ('sas', 'sas'), ('ssd', 'ssd'), ('nvme', 'nvme'), ('scsi', 'scsi'), ('iscsi', 'iscsi')], default='sata', max_length=50)), + ], + ), + migrations.CreateModel( + name='GenericHost', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ], + ), + migrations.CreateModel( + name='GenericInterface', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('host', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='generic_interfaces', to='resource_inventory.GenericHost')), + ], + ), + migrations.CreateModel( + name='GenericResource', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200, validators=[django.core.validators.RegexValidator(message='Enter a valid hostname. Full domain name may be 1-253 characters, each hostname 1-63 characters (including suffixed dot), and valid characters for hostnames are A-Z, a-z, 0-9, hyphen (-), and underscore (_)', regex='(?=^.{1,253}$)(?=(^([A-Za-z0-9\\-\\_]{1,62}\\.)*[A-Za-z0-9\\-\\_]{1,63}$))')])), + ], + ), + migrations.CreateModel( + name='GenericResourceBundle', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=300, unique=True)), + ('xml', models.TextField()), + ('description', models.CharField(default='', max_length=1000)), + ('lab', models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='account.Lab')), + ('owner', models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Host', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('booked', models.BooleanField(default=False)), + ('name', models.CharField(max_length=200, unique=True)), + ('labid', models.CharField(default='default_id', max_length=200)), + ('working', models.BooleanField(default=True)), + ('vendor', models.CharField(default='unknown', max_length=100)), + ('model', models.CharField(default='unknown', max_length=150)), + ], + ), + migrations.CreateModel( + name='HostConfiguration', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('bundle', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='hostConfigurations', to='resource_inventory.ConfigBundle')), + ('host', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='configuration', to='resource_inventory.GenericHost')), + ], + ), + migrations.CreateModel( + name='HostProfile', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('host_type', models.PositiveSmallIntegerField()), + ('name', models.CharField(max_length=200, unique=True)), + ('description', models.TextField()), + ('labs', models.ManyToManyField(related_name='hostprofiles', to='account.Lab')), + ], + ), + migrations.CreateModel( + name='Image', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('lab_id', models.IntegerField()), + ('name', models.CharField(max_length=200)), + ('public', models.BooleanField(default=True)), + ('description', models.TextField()), + ('from_lab', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='account.Lab')), + ('host_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.HostProfile')), + ('owner', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Installer', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=200)), + ], + ), + migrations.CreateModel( + name='Interface', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('mac_address', models.CharField(max_length=17)), + ('bus_address', models.CharField(max_length=50)), + ('name', models.CharField(default='eth0', max_length=100)), + ], + ), + migrations.CreateModel( + name='InterfaceProfile', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('speed', models.IntegerField()), + ('name', models.CharField(max_length=100)), + ('nic_type', models.CharField(choices=[('onboard', 'onboard'), ('pcie', 'pcie')], default='onboard', max_length=50)), + ('host', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='interfaceprofile', to='resource_inventory.HostProfile')), + ], + ), + migrations.CreateModel( + name='Network', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('vlan_id', models.IntegerField()), + ('name', models.CharField(max_length=100)), + ], + ), + migrations.CreateModel( + name='OPNFVConfig', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('bundle', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='opnfv_config', to='resource_inventory.ConfigBundle')), + ('installer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.Installer')), + ], + ), + migrations.CreateModel( + name='OPNFVRole', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=200)), + ('description', models.TextField()), + ], + ), + migrations.CreateModel( + name='Opsys', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=100)), + ('sup_installers', models.ManyToManyField(blank=True, to='resource_inventory.Installer')), + ], + ), + migrations.CreateModel( + name='RamProfile', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('amount', models.IntegerField()), + ('channels', models.IntegerField()), + ('host', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='ramprofile', to='resource_inventory.HostProfile')), + ], + ), + migrations.CreateModel( + name='ResourceBundle', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('template', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='resource_inventory.GenericResourceBundle')), + ], + ), + migrations.CreateModel( + name='Scenario', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=300)), + ], + ), + migrations.CreateModel( + name='Vlan', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('vlan_id', models.IntegerField()), + ('tagged', models.BooleanField()), + ], + ), + migrations.CreateModel( + name='GenericPod', + fields=[ + ('genericresource_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='resource_inventory.GenericResource')), + ], + bases=('resource_inventory.genericresource',), + ), + migrations.AddField( + model_name='opnfvconfig', + name='scenario', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.Scenario'), + ), + migrations.AddField( + model_name='interface', + name='config', + field=models.ManyToManyField(to='resource_inventory.Vlan'), + ), + migrations.AddField( + model_name='interface', + name='host', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interfaces', to='resource_inventory.Host'), + ), + migrations.AddField( + model_name='installer', + name='sup_scenarios', + field=models.ManyToManyField(blank=True, to='resource_inventory.Scenario'), + ), + migrations.AddField( + model_name='hostconfiguration', + name='image', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='resource_inventory.Image'), + ), + migrations.AddField( + model_name='hostconfiguration', + name='opnfvRole', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='resource_inventory.OPNFVRole'), + ), + migrations.AddField( + model_name='host', + name='bundle', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='hosts', to='resource_inventory.ResourceBundle'), + ), + migrations.AddField( + model_name='host', + name='config', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='configuration', to='resource_inventory.HostConfiguration'), + ), + migrations.AddField( + model_name='host', + name='lab', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='account.Lab'), + ), + migrations.AddField( + model_name='host', + name='profile', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.HostProfile'), + ), + migrations.AddField( + model_name='host', + name='template', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='resource_inventory.GenericHost'), + ), + migrations.AddField( + model_name='genericresource', + name='bundle', + field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='generic_resources', to='resource_inventory.GenericResourceBundle'), + ), + migrations.AddField( + model_name='genericinterface', + name='profile', + field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='resource_inventory.InterfaceProfile'), + ), + migrations.AddField( + model_name='genericinterface', + name='vlans', + field=models.ManyToManyField(to='resource_inventory.Vlan'), + ), + migrations.AddField( + model_name='generichost', + name='profile', + field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='resource_inventory.HostProfile'), + ), + migrations.AddField( + model_name='generichost', + name='resource', + field=models.OneToOneField(on_delete=django.db.models.deletion.DO_NOTHING, related_name='generic_host', to='resource_inventory.GenericResource'), + ), + migrations.AddField( + model_name='diskprofile', + name='host', + field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='storageprofile', to='resource_inventory.HostProfile'), + ), + migrations.AddField( + model_name='cpuprofile', + name='host', + field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='cpuprofile', to='resource_inventory.HostProfile'), + ), + migrations.AddField( + model_name='configbundle', + name='bundle', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.GenericResourceBundle'), + ), + migrations.AddField( + model_name='configbundle', + name='owner', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='genericpod', + name='hosts', + field=models.ManyToManyField(to='resource_inventory.GenericHost'), + ), + migrations.AddField( + model_name='genericpod', + name='networks', + field=models.ManyToManyField(to='resource_inventory.Network'), + ), + ] diff --git a/src/resource_inventory/migrations/0002_auto_20180919_1459.py b/src/resource_inventory/migrations/0002_auto_20180919_1459.py new file mode 100644 index 0000000..80c9e6f --- /dev/null +++ b/src/resource_inventory/migrations/0002_auto_20180919_1459.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1 on 2018-09-19 14:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('resource_inventory', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='hostprofile', + name='host_type', + field=models.PositiveSmallIntegerField(default=0), + ), + ] diff --git a/src/resource_inventory/migrations/0003_vlan_public.py b/src/resource_inventory/migrations/0003_vlan_public.py new file mode 100644 index 0000000..07dc647 --- /dev/null +++ b/src/resource_inventory/migrations/0003_vlan_public.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1 on 2018-09-26 14:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('resource_inventory', '0002_auto_20180919_1459'), + ] + + operations = [ + migrations.AddField( + model_name='vlan', + name='public', + field=models.BooleanField(default=False), + ), + ] diff --git a/src/resource_inventory/migrations/__init__.py b/src/resource_inventory/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/resource_inventory/models.py b/src/resource_inventory/models.py new file mode 100644 index 0000000..b71748e --- /dev/null +++ b/src/resource_inventory/models.py @@ -0,0 +1,299 @@ +############################################################################## +# Copyright (c) 2018 Sawyer Bergeron, Parker Berberian, and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + +from django.contrib.auth.models import User +from django.db import models +from django.core.validators import RegexValidator + +import re + +from account.models import Lab + + +# profile of resources hosted by labs +class HostProfile(models.Model): + id = models.AutoField(primary_key=True) + host_type = models.PositiveSmallIntegerField(default=0) + name = models.CharField(max_length=200, unique=True) + description = models.TextField() + labs = models.ManyToManyField(Lab, related_name="hostprofiles") + + def validate(self): + validname = re.compile("^[A-Za-z0-9\-\_\.\/\, ]+$") + if not validname.match(self.name): + return "Invalid host profile name given. Name must only use A-Z, a-z, 0-9, hyphens, underscores, dots, commas, or spaces." + else: + return None + + def __str__(self): + return self.name + + +class InterfaceProfile(models.Model): + id = models.AutoField(primary_key=True) + speed = models.IntegerField() + name = models.CharField(max_length=100) + host = models.ForeignKey(HostProfile, on_delete=models.DO_NOTHING, related_name='interfaceprofile') + nic_type = models.CharField(max_length=50, choices=[ + ("onboard", "onboard"), + ("pcie", "pcie") + ], default="onboard") + + def __str__(self): + return self.name + " for " + str(self.host) + + +class DiskProfile(models.Model): + id = models.AutoField(primary_key=True) + size = models.IntegerField() + media_type = models.CharField(max_length=50, choices=[ + ("SSD", "SSD"), + ("HDD", "HDD") + ]) + name = models.CharField(max_length=50) + host = models.ForeignKey(HostProfile, on_delete=models.DO_NOTHING, related_name='storageprofile') + rotation = models.IntegerField(default=0) + interface = models.CharField(max_length=50, choices=[ + ("sata", "sata"), + ("sas", "sas"), + ("ssd", "ssd"), + ("nvme", "nvme"), + ("scsi", "scsi"), + ("iscsi", "iscsi"), + ], default="sata") + + def __str__(self): + return self.name + " for " + str(self.host) + + +class CpuProfile(models.Model): + id = models.AutoField(primary_key=True) + cores = models.IntegerField() + architecture = models.CharField(max_length=50, choices=[ + ("x86_64", "x86_64"), + ("aarch64", "aarch64") + ]) + cpus = models.IntegerField() + host = models.ForeignKey(HostProfile, on_delete=models.DO_NOTHING, related_name='cpuprofile') + cflags = models.TextField(null=True) + + def __str__(self): + return str(self.architecture) + " " + str(self.cpus) + "S" + str(self.cores) + " C for " + str(self.host) + + +class RamProfile(models.Model): + id = models.AutoField(primary_key=True) + amount = models.IntegerField() + channels = models.IntegerField() + host = models.ForeignKey(HostProfile, on_delete=models.DO_NOTHING, related_name='ramprofile') + + def __str__(self): + return str(self.amount) + "G for " + str(self.host) + + +##Networking -- located here due to import order requirements +class Network(models.Model): + id = models.AutoField(primary_key=True) + vlan_id = models.IntegerField() + name = models.CharField(max_length=100) + + def __str__(self): + return self.name + +class Vlan(models.Model): + id = models.AutoField(primary_key=True) + vlan_id = models.IntegerField() + tagged = models.BooleanField() + public = models.BooleanField(default=False) + + def __str__(self): + return str(self.vlan_id) + ("_T" if self.tagged else "") + + +# Generic resource templates +class GenericResourceBundle(models.Model): + id = models.AutoField(primary_key=True) + name = models.CharField(max_length=300, unique=True) + xml = models.TextField() + owner = models.ForeignKey(User, null=True, on_delete=models.DO_NOTHING) + lab = models.ForeignKey(Lab, null=True, on_delete=models.DO_NOTHING) + description = models.CharField(max_length=1000, default="") + + def getHosts(self): + return_hosts = [] + for genericResource in self.generic_resources.all(): + return_hosts.append(genericResource.getHost()) + + return return_hosts + + def __str__(self): + return self.name + + +class GenericResource(models.Model): + bundle = models.ForeignKey(GenericResourceBundle, related_name='generic_resources', on_delete=models.DO_NOTHING) + hostname_validchars = RegexValidator(regex='(?=^.{1,253}$)(?=(^([A-Za-z0-9\-\_]{1,62}\.)*[A-Za-z0-9\-\_]{1,63}$))', message="Enter a valid hostname. Full domain name may be 1-253 characters, each hostname 1-63 characters (including suffixed dot), and valid characters for hostnames are A-Z, a-z, 0-9, hyphen (-), and underscore (_)") + name = models.CharField(max_length=200, validators=[hostname_validchars]) + + def getHost(self): + return self.generic_host + + def __str__(self): + return self.name + + def validate(self): + validname = re.compile('(?=^.{1,253}$)(?=(^([A-Za-z0-9\-\_]{1,62}\.)*[A-Za-z0-9\-\_]{1,63}$))') + if not validname.match(self.name): + return "Enter a valid hostname. Full domain name may be 1-253 characters, each hostname 1-63 characters (including suffixed dot), and valid characters for hostnames are A-Z, a-z, 0-9, hyphen (-), and underscore (_)" + else: + return None + + +# Host template +class GenericHost(models.Model): + id = models.AutoField(primary_key=True) + profile = models.ForeignKey(HostProfile, on_delete=models.DO_NOTHING) + resource = models.OneToOneField(GenericResource, related_name='generic_host', on_delete=models.DO_NOTHING) + + def __str__(self): + return self.resource.name + + +# Physical, actual resources +class ResourceBundle(models.Model): + id = models.AutoField(primary_key=True) + template = models.ForeignKey(GenericResourceBundle, on_delete=models.DO_NOTHING) + + def __str__(self): + return "instance of " + str(self.template) + + +# Networking + + +class GenericInterface(models.Model): + id = models.AutoField(primary_key=True) + vlans = models.ManyToManyField(Vlan) + profile = models.ForeignKey(InterfaceProfile, on_delete=models.DO_NOTHING) + host = models.ForeignKey(GenericHost, on_delete=models.DO_NOTHING, related_name='generic_interfaces') + + def __str__(self): + return "type " + str(self.profile) + " on host " + str(self.host) + + +class Scenario(models.Model): + id = models.AutoField(primary_key=True) + name = models.CharField(max_length=300) + + def __str__(self): + return self.name + +class Installer(models.Model): + id = models.AutoField(primary_key=True) + name = models.CharField(max_length=200) + sup_scenarios = models.ManyToManyField(Scenario, blank=True) + + def __str__(self): + return self.name + +class Opsys(models.Model): + id = models.AutoField(primary_key=True) + name = models.CharField(max_length=100) + sup_installers = models.ManyToManyField(Installer, blank=True) + + def __str__(self): + return self.name + +class ConfigBundle(models.Model): + id = models.AutoField(primary_key=True) + owner = models.ForeignKey(User, on_delete=models.CASCADE) #consider setting to root user? + name = models.CharField(max_length=200, unique=True) + description = models.CharField(max_length=1000, default="") + bundle = models.ForeignKey(GenericResourceBundle, null=True, on_delete=models.CASCADE) + + def __str__(self): + return self.name + +class OPNFVConfig(models.Model): + id = models.AutoField(primary_key=True) + installer = models.ForeignKey(Installer, on_delete=models.CASCADE) + scenario = models.ForeignKey(Scenario, on_delete=models.CASCADE) + bundle = models.ForeignKey(ConfigBundle, related_name="opnfv_config", on_delete=models.CASCADE) + + def __str__(self): + return "OPNFV job with " + str(self.installer) + " and " + str(self.scenario) + +class OPNFVRole(models.Model): + id = models.AutoField(primary_key=True) + name = models.CharField(max_length=200) + description = models.TextField() + + def __str__(self): + return self.name + +class Image(models.Model): + """ + model for representing OS images / snapshots of hosts + """ + id = models.AutoField(primary_key=True) + lab_id = models.IntegerField() # ID the lab who holds this image knows + from_lab = models.ForeignKey(Lab, on_delete=models.CASCADE) + name = models.CharField(max_length=200) + owner = models.ForeignKey(User, null=True, on_delete=models.SET_NULL) + public = models.BooleanField(default=True) + host_type = models.ForeignKey(HostProfile, on_delete=models.CASCADE) #may need to change to models.SET() once images are transferrable between compatible host types + description = models.TextField() + + def __str__(self): + return self.name + +class HostConfiguration(models.Model): + """ + model to represent a complete configuration for a single + physical host + """ + id = models.AutoField(primary_key=True) + host = models.ForeignKey(GenericHost, related_name="configuration", on_delete=models.CASCADE) + image = models.ForeignKey(Image, on_delete=models.PROTECT) + bundle = models.ForeignKey(ConfigBundle, related_name="hostConfigurations", null=True, on_delete=models.CASCADE) + opnfvRole = models.ForeignKey(OPNFVRole, on_delete=models.PROTECT) #need protocol for phasing out a role if we are going to allow that to happen + + def __str__(self): + return "config with " + str(self.host) + " and image " + str(self.image) + + +# Concrete host, actual machine in a lab +class Host(models.Model): + id = models.AutoField(primary_key=True) + template = models.ForeignKey(GenericHost, on_delete=models.SET_NULL, null=True) + booked = models.BooleanField(default=False) + name = models.CharField(max_length=200, unique=True) + bundle = models.ForeignKey(ResourceBundle, related_name='hosts', on_delete=models.SET_NULL, null=True) + config = models.ForeignKey(HostConfiguration, null=True, related_name="configuration", on_delete=models.SET_NULL) + labid = models.CharField(max_length=200, default="default_id") + profile = models.ForeignKey(HostProfile, on_delete=models.CASCADE) + lab = models.ForeignKey(Lab, on_delete=models.CASCADE) + working = models.BooleanField(default=True) + vendor = models.CharField(max_length=100, default="unknown") + model = models.CharField(max_length=150, default="unknown") + + def __str__(self): + return self.name + + +class Interface(models.Model): + id = models.AutoField(primary_key=True) + mac_address = models.CharField(max_length=17) + bus_address = models.CharField(max_length=50) + name = models.CharField(max_length=100, default="eth0") + config = models.ManyToManyField(Vlan) + host = models.ForeignKey(Host, on_delete=models.CASCADE, related_name='interfaces') + + def __str__(self): + return self.mac_address + " on host " + str(self.host) diff --git a/src/resource_inventory/resource_manager.py b/src/resource_inventory/resource_manager.py new file mode 100644 index 0000000..cd70867 --- /dev/null +++ b/src/resource_inventory/resource_manager.py @@ -0,0 +1,197 @@ +############################################################################## +# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + + +from django.core.exceptions import * +from django.template.loader import render_to_string + +import booking +from dashboard.exceptions import * +from resource_inventory.models import * + +class ResourceManager: + + instance = None + + def __init__(self): + pass + + @staticmethod + def getInstance(): + if ResourceManager.instance is None: + ResourceManager.instance = ResourceManager() + return ResourceManager.instance + + #public interface + def deleteResourceBundle(self, resourceBundle): + for host in Host.objects.filter(bundle=resourceBundle): + self.releaseHost(host) + resourceBundle.delete() + + def convertResourceBundle(self, genericResourceBundle, lab=None, config=None): + """ + Takes in a GenericResourceBundle and 'converts' it into a ResourceBundle + """ + resource_bundle = ResourceBundle() + resource_bundle.template = genericResourceBundle + resource_bundle.save() + + hosts = genericResourceBundle.getHosts() + + #current supported case: user creating new booking + #currently unsupported: editing existing booking + + physical_hosts = [] + + for host in hosts: + host_config=None + if config: + host_config = HostConfiguration.objects.get(bundle=config, host=host) + try: + physical_host = self.acquireHost(host, genericResourceBundle.lab.name) + except ResourceAvailabilityException: + self.fail_acquire(physical_hosts) + raise ResourceAvailabilityException("Could not provision hosts, not enough available") + try: + physical_host.bundle = resource_bundle + physical_host.template = host + physical_host.config = host_config + physical_hosts.append(physical_host) + + self.configureNetworking(physical_host) + except: + self.fail_acquire(physical_hosts) + raise ResourceProvisioningException("Network configuration failed.") + try: + physical_host.save() + except: + self.fail_acquire(physical_hosts) + raise ModelValidationException("Saving hosts failed") + + return resource_bundle + + def configureNetworking(self, host): + generic_interfaces = list(host.template.generic_interfaces.all()) + for int_num, physical_interface in enumerate(host.interfaces.all()): + generic_interface = generic_interfaces[int_num] + physical_interface.config.clear() + for vlan in generic_interface.vlans.all(): + physical_interface.config.add(vlan) + + #private interface + def acquireHost(self, genericHost, labName): + host_full_set = Host.objects.filter(lab__name__exact=labName, profile=genericHost.profile) + if not host_full_set.first(): + raise ResourceExistenceException("No matching servers found") + host_set = host_full_set.filter(booked=False) + if not host_set.first(): + raise ResourceAvailabilityException("No unbooked hosts match requested hosts") + host = host_set.first() + host.booked = True + host.template = genericHost + host.save() + return host + + def releaseHost(self, host): + host.template = None + host.bundle = None + host.booked = False + host.save() + + def fail_acquire(self, hosts): + for host in hosts: + self.releaseHost(host) + + def makePDF(self, resource): + """ + fills the pod descriptor file template with info about the resource + """ + template = "dashboard/pdf.yaml" + info = {} + info['details'] = self.get_pdf_details(resource) + info['jumphost'] = self.get_pdf_jumphost(resource) + info['nodes'] = self.get_pdf_nodes(resource) + + return render_to_string(template, context=info) + + def get_pdf_details(self, resource): + details = {} + owner = "Anon" + email = "email@mail.com" + resource_lab = resource.template.lab + lab = resource_lab.name + location = resource_lab.location + pod_type = "development" + link = "https://wiki.opnfv.org/display/INF/Pharos+Laas" + + try: + # try to get more specific info that may fail, we dont care if it does + booking_owner = booking.models.Booking.objects.get(resource=resource).owner + owner = booking_owner.username + email = booking_owner.userprofile.email_addr + except Exception as e: + pass + + details['owner'] = owner + details['email'] = email + details['lab'] = lab + details['location'] = location + details['type'] = pod_type + details['link'] = link + + return details + + def get_pdf_jumphost(self, resource): + jumphost = Host.objects.get(bundle=resource, config__opnfvRole__name__iexact="jumphost") + return self.get_pdf_host(jumphost) + + def get_pdf_nodes(self, resource): + pdf_nodes = [] + nodes = Host.objects.filter(bundle=resource).exclude(config__opnfvRole__name__iexact="jumphost") + for node in nodes: + pdf_nodes.append(self.get_pdf_host(node)) + + return pdf_nodes + + + def get_pdf_host(self, host): + host_info = {} + host_info['name'] = host.template.resource.name + host_info['node'] = {} + host_info['node']['type'] = "baremetal" + host_info['node']['vendor'] = host.vendor + host_info['node']['model'] = host.model + host_info['node']['arch'] = host.profile.cpuprofile.first().architecture + host_info['node']['cpus'] = host.profile.cpuprofile.first().cpus + host_info['node']['cores'] = host.profile.cpuprofile.first().cores + cflags = host.profile.cpuprofile.first().cflags + if cflags and cflags.strip(): + host_info['node']['cpu_cflags'] = cflags + host_info['node']['memory'] = str(host.profile.ramprofile.first().amount) + "G" + host_info['disks'] = [] + for disk in host.profile.storageprofile.all(): + disk_info = {} + disk_info['name'] = disk.name + disk_info['capacity'] = str(disk.size) + "G" + disk_info['type'] = disk.media_type + disk_info['interface'] = disk.interface + disk_info['rotation'] = disk.rotation + host_info['disks'].append(disk_info) + + host_info['interfaces'] = [] + for interface in host.interfaces.all(): + iface_info = {} + iface_info['name'] = interface.name + iface_info['address'] = "unknown" + iface_info['mac_address'] = interface.mac_address + vlans = "|".join([str(vlan.vlan_id) for vlan in interface.config.all()]) + iface_info['vlans'] = vlans + host_info['interfaces'].append(iface_info) + + return host_info diff --git a/src/resource_inventory/tests/test_managers.py b/src/resource_inventory/tests/test_managers.py new file mode 100644 index 0000000..5a13b2e --- /dev/null +++ b/src/resource_inventory/tests/test_managers.py @@ -0,0 +1,236 @@ +############################################################################## +# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + +from django.test import TestCase +from django.contrib.auth.models import User + +from resource.inventory_manager import InventoryManager +from resource.resource_manager import ResourceManager +from resource.models import * + + +class InventoryManagerTestCase(TestCase): + + def test_singleton(self): + instance = InventoryManager.getInstance() + self.assertTrue(isinstance(instance, InventoryManager)) + self.assertTrue(instance is InventoryManager.getInstance()) + + def setUp(self): + # setup + # create lab and give it resources + user = User.objects.create(username="username") + self.lab = Lab.objects.create( + lab_user=user, + name='test lab', + contact_email='someone@email.com', + contact_phone='dont call me' + ) + + # create hostProfile + hostProfile = HostProfile.objects.create( + host_type=0, + name='Test profile', + description='a test profile' + ) + interfaceProfile = InterfaceProfile.objects.create( + speed=1000, + name='eno3', + host=hostProfile + ) + diskProfile = DiskProfile.objects.create( + size=1000, + media_type="SSD", + name='/dev/sda', + host=hostProfile + ) + cpuProfile = CpuProfile.objects.create( + cores=96, + architecture="x86_64", + cpus=2, + host=hostProfile + ) + ramProfile = RamProfile.objects.create( + amount=256, + channels=4, + host=hostProfile + ) + + #create GenericResourceBundle + genericBundle = GenericResourceBundle.objects.create() + + self.gHost1 = GenericHost.objects.create( + bundle=genericBundle, + name='generic host 1', + profile=hostProfile + ) + self.gHost2 = GenericHost.objects.create( + bundle=genericBundle, + name='generic host 2', + profile=hostProfile + ) + + #actual resource bundle + bundle = ResourceBundle.objects.create(template=genericBundle) + + self.host1 = Host.objects.create( + template=self.gHost1, + booked=True, + name='host1', + bundle=bundle, + profile=hostProfile, + lab=self.lab + ) + + self.host2 = Host.objects.create( + template=self.gHost2, + booked=True, + name='host2', + bundle=bundle, + profile=hostProfile, + lab=self.lab + ) + + vlan1 = Vlan.objects.create(vlan_id=300, tagged=False) + vlan2 = Vlan.objects.create(vlan_id=300, tagged=False) + + iface1 = Interface.objects.create( + mac_address='00:11:22:33:44:55', + bus_address='some bus address', + switch_name='switch1', + port_name='port10', + config=vlan1, + host=self.host1 + ) + iface2 = Interface.objects.create( + mac_address='00:11:22:33:44:56', + bus_address='some bus address', + switch_name='switch1', + port_name='port12', + config=vlan2, + host=self.host2 + ) + + def test_acquire_host(self): + host = InventoryManager.getInstance().acquireHost(self.gHost1, self.lab.name) + self.assertNotEquals(host, None) + self.assertTrue(host.booked) + self.assertEqual(host.template, self.gHost1) + + def test_release_host(self): + host = InventoryManager.getInstance().acquireHost(self.gHost1, self.lab.name) + self.assertTrue(host.booked) + InventoryManager.getInstance().releaseHost(host) + self.assertFalse(host.booked) + + +class ResourceManagerTestCase(TestCase): + def test_singleton(self): + instance = ResourceManager.getInstance() + self.assertTrue(isinstance(instance, ResourceManager)) + self.assertTrue(instance is ResourceManager.getInstance()) + + def setUp(self): + # setup + # create lab and give it resources + user = User.objects.create(username="username") + self.lab = Lab.objects.create( + lab_user=user, + name='test lab', + contact_email='someone@email.com', + contact_phone='dont call me' + ) + + # create hostProfile + hostProfile = HostProfile.objects.create( + host_type=0, + name='Test profile', + description='a test profile' + ) + interfaceProfile = InterfaceProfile.objects.create( + speed=1000, + name='eno3', + host=hostProfile + ) + diskProfile = DiskProfile.objects.create( + size=1000, + media_type="SSD", + name='/dev/sda', + host=hostProfile + ) + cpuProfile = CpuProfile.objects.create( + cores=96, + architecture="x86_64", + cpus=2, + host=hostProfile + ) + ramProfile = RamProfile.objects.create( + amount=256, + channels=4, + host=hostProfile + ) + + #create GenericResourceBundle + genericBundle = GenericResourceBundle.objects.create() + + self.gHost1 = GenericHost.objects.create( + bundle=genericBundle, + name='generic host 1', + profile=hostProfile + ) + self.gHost2 = GenericHost.objects.create( + bundle=genericBundle, + name='generic host 2', + profile=hostProfile + ) + + #actual resource bundle + bundle = ResourceBundle.objects.create(template=genericBundle) + + self.host1 = Host.objects.create( + template=self.gHost1, + booked=True, + name='host1', + bundle=bundle, + profile=hostProfile, + lab=self.lab + ) + + self.host2 = Host.objects.create( + template=self.gHost2, + booked=True, + name='host2', + bundle=bundle, + profile=hostProfile, + lab=self.lab + ) + + vlan1 = Vlan.objects.create(vlan_id=300, tagged=False) + vlan2 = Vlan.objects.create(vlan_id=300, tagged=False) + + iface1 = Interface.objects.create( + mac_address='00:11:22:33:44:55', + bus_address='some bus address', + switch_name='switch1', + port_name='port10', + config=vlan1, + host=self.host1 + ) + iface2 = Interface.objects.create( + mac_address='00:11:22:33:44:56', + bus_address='some bus address', + switch_name='switch1', + port_name='port12', + config=vlan2, + host=self.host2 + ) + + def test_convert_bundle(self): + bundle = ResourceManager.getInstance().convertResoureBundle(self.genericBundle, self.lab.name) + # verify bundle configuration diff --git a/src/resource_inventory/tests/test_models.py b/src/resource_inventory/tests/test_models.py new file mode 100644 index 0000000..4ddedf2 --- /dev/null +++ b/src/resource_inventory/tests/test_models.py @@ -0,0 +1,162 @@ +############################################################################## +# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## +from django.test import TestCase +from django.contrib.auth.models import User +from account.models import Lab +from resource_inventory.models import * + + +class ConfigUtil(): + count=0 + + @staticmethod + def makeScenario(): + return Scenario.objects.create(name="testScenario") + + @staticmethod + def makeInstaller(): + inst = Installer.objects.create( + name = "testInstaller" + ) + inst.sup_scenarios = [ConfigUtil.makeScenario()] + return inst + + @staticmethod + def makeOpsys(): + os = Opsys.objects.create( + name = "test Operating System" + ) + os.sup_installers = [ConfigUtil.makeInstaller()] + return os + + @staticmethod + def makeConfigBundle(): + user = User.objects.create(username="test_user" + str(ConfigUtil.count)) + ConfigUtil.count += 1 + return ConfigBundle.objects.create( + owner = user + ) + + @staticmethod + def makeOPNFVConfig(): + installer = ConfigUtil.makeInstaller() + scenario = ConfigUtil.makeScenario() + bundle = ConfigUtil.makeConfigBundle() + return OPNFVConfig.objects.create( + installer=installer, + scenario=scenario, + bundle=bundle + ) + + @staticmethod + def makeOPNFVRole(): + return OPNFVRole.objects.create( + name="Test role", + description="This is a test role" + ) + + @staticmethod + def makeImage(): + owner = User.objects.create(username="another test user") + lab_user = User.objects.create(username="labUserForTests") + lab = Lab.objects.create( + lab_user=lab_user, + name="this is lab for testing", + contact_email="email@mail.com", + contact_phone="123-4567" + ) + + return Image.objects.create( + lab_id=0, + from_lab=lab, + name="an image for testing", + owner=owner + ) + + + @staticmethod + def makeGenericHost(): + profile = HostProfile.objects.create( + host_type=0, + name="test lab for config bundle", + description="this is a test profile" + ) + user = User.objects.create(username="test sample user 12") + bundle = GenericResourceBundle.objects.create( + name="Generic bundle for config tests", + xml="", + owner=user, + description="" + ) + + resource = GenericResource.objects.create( + bundle=bundle, + name="a test generic resource" + ) + + return GenericHost.objects.create( + profile=profile, + resource=resource + ) + + @staticmethod + def makeHostConfiguration(): + host = ConfigUtil.makeGenericHost() + image = ConfigUtil.makeImage() + bundle = ConfigUtil.makeConfigBundle() + opnfvRole = ConfigUtil.makeOPNFVRole() + return HostConfiguration.objects.create( + host=host, + image=image, + bundle=bundle, + opnfvRole=opnfvRole + ) + + +class ScenarioTestCase(TestCase): + + def test_save(self): + self.assertTrue(ConfigUtil.makeScenario()) + +class InstallerTestCase(TestCase): + + def test_save(self): + self.assertTrue(ConfigUtil.makeInstaller()) + +class OperatingSystemTestCase(TestCase): + + def test_save(self): + self.assertTrue(ConfigUtil.makeOpsys()) + +class ConfigBundleTestCase(TestCase): + + def test_save(self): + self.assertTrue(ConfigUtil.makeConfigBundle()) + +class OPNFVConfigTestCase(TestCase): + + def test_save(self): + self.assertTrue(ConfigUtil.makeOPNFVConfig()) + +class OPNFVRoleTestCase(TestCase): + + def test_save(self): + self.assertTrue(ConfigUtil.makeOPNFVRole()) + + +class HostConfigurationTestCase(TestCase): + + def test_save(self): + self.assertTrue(ConfigUtil.makeHostConfiguration()) + + +class ImageTestCase(TestCase): + + def test_save(self): + self.assertTrue(ConfigUtil.makeImage()) diff --git a/src/resource_inventory/urls.py b/src/resource_inventory/urls.py new file mode 100644 index 0000000..4e159ba --- /dev/null +++ b/src/resource_inventory/urls.py @@ -0,0 +1,34 @@ +############################################################################## +# Copyright (c) 2016 Max Breitenfeldt and others. +# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + + +"""pharos_dashboard URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/1.10/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.conf.urls import url, include + 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) +""" +from django.conf.urls import url +from resource_inventory.views import HostView + + +app_name = "resource" +urlpatterns = [ + url(r'^hosts$', HostView.as_view(), name='hosts') +] diff --git a/src/resource_inventory/views.py b/src/resource_inventory/views.py new file mode 100644 index 0000000..7e73006 --- /dev/null +++ b/src/resource_inventory/views.py @@ -0,0 +1,24 @@ +############################################################################## +# Copyright (c) 2018 Sawyer Bergeron, Parker Berberian, and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + + +from django.shortcuts import render +from django.views import View +from django.views.generic import TemplateView + +from resource_inventory.models import Host + +class HostView(TemplateView): + template_name = "resource/hosts.html" + + def get_context_data(self, **kwargs): + context = super(HostView, self).get_context_data(**kwargs) + hosts = Host.objects.filter(working=True) + context.update({'hosts':hosts, 'title':"Hardware Resources"}) + return context diff --git a/src/static/css/graph_common.css b/src/static/css/graph_common.css new file mode 100644 index 0000000..7f90a66 --- /dev/null +++ b/src/static/css/graph_common.css @@ -0,0 +1,162 @@ +div.mxRubberband { + position: absolute; + overflow: hidden; + border-style: solid; + border-width: 1px; + border-color: #0000FF; + background: #0077FF; +} +.mxCellEditor { + background: url(data:image/gif;base64,R0lGODlhMAAwAIAAAP///wAAACH5BAEAAAAALAAAAAAwADAAAAIxhI+py+0Po5y02ouz3rz7D4biSJbmiabqyrbuC8fyTNf2jef6zvf+DwwKh8Si8egpAAA7); + _background: url('/static/img/mxgraph/transparent.gif'); + border-color: transparent; + border-style: solid; + display: inline-block; + position: absolute; + overflow: visible; + word-wrap: normal; + border-width: 0; + min-width: 1px; + resize: none; + padding: 0px; + margin: 0px; +} +.mxPlainTextEditor * { + padding: 0px; + margin: 0px; +} +div.mxWindow { + -webkit-box-shadow: 3px 3px 12px #C0C0C0; + -moz-box-shadow: 3px 3px 12px #C0C0C0; + box-shadow: 3px 3px 12px #C0C0C0; + background: url('../img/mxgraph/window.gif'); + border:1px solid #c3c3c3; + position: absolute; + overflow: hidden; + z-index: 3; +} +table.mxWindow { + border-collapse: collapse; + table-layout: fixed; + font-family: Arial; + font-size: 8pt; +} +td.mxWindowTitle { + background: url('/static/img/mxgraph/window-title.gif') repeat-x; + text-overflow: ellipsis; + white-space: nowrap; + text-align: center; + font-weight: bold; + overflow: hidden; + height: 13px; + padding: 2px; + padding-top: 4px; + padding-bottom: 6px; + color: black; +} +td.mxWindowPane { + vertical-align: top; + padding: 0px; +} +div.mxWindowPane { + overflow: hidden; + position: relative; +} +td.mxWindowPane td { + font-family: Arial; + font-size: 8pt; +} +td.mxWindowPane input, td.mxWindowPane select, td.mxWindowPane textarea, td.mxWindowPane radio { + border-color: #8C8C8C; + border-style: solid; + border-width: 1px; + font-family: Arial; + font-size: 8pt; + padding: 1px; +} +td.mxWindowPane button { + background: url('/static/img/mxgraph/button.gif') repeat-x; + font-family: Arial; + font-size: 8pt; + padding: 2px; + float: left; +} +img.mxToolbarItem { + margin-right: 6px; + margin-bottom: 6px; + border-width: 1px; +} +select.mxToolbarCombo { + vertical-align: top; + border-style: inset; + border-width: 2px; +} +div.mxToolbarComboContainer { + padding: 2px; +} +img.mxToolbarMode { + margin: 2px; + margin-right: 4px; + margin-bottom: 4px; + border-width: 0px; +} +img.mxToolbarModeSelected { + margin: 0px; + margin-right: 2px; + margin-bottom: 2px; + border-width: 2px; + border-style: inset; +} +div.mxTooltip { + -webkit-box-shadow: 3px 3px 12px #C0C0C0; + -moz-box-shadow: 3px 3px 12px #C0C0C0; + box-shadow: 3px 3px 12px #C0C0C0; + background: #FFFFCC; + border-style: solid; + border-width: 1px; + border-color: black; + font-family: Arial; + font-size: 8pt; + position: absolute; + cursor: default; + padding: 4px; + color: black; +} +div.mxPopupMenu { + -webkit-box-shadow: 3px 3px 12px #C0C0C0; + -moz-box-shadow: 3px 3px 12px #C0C0C0; + box-shadow: 3px 3px 12px #C0C0C0; + background: url('/static/img/mxgraph/window.gif'); + position: absolute; + border-style: solid; + border-width: 1px; + border-color: black; +} +table.mxPopupMenu { + border-collapse: collapse; + margin-top: 1px; + margin-bottom: 1px; +} +tr.mxPopupMenuItem { + color: black; + cursor: pointer; +} +tr.mxPopupMenuItemHover { + background-color: #000066; + color: #FFFFFF; + cursor: pointer; +} +td.mxPopupMenuItem { + padding: 2px 30px 2px 10px; + white-space: nowrap; + font-family: Arial; + font-size: 8pt; +} +td.mxPopupMenuIcon { + background-color: #D0D0D0; + padding: 2px 4px 2px 4px; +} +.mxDisabled { + opacity: 0.2 !important; + cursor:default !important; +} diff --git a/src/static/css/theme.css b/src/static/css/theme.css index bd15637..fb350e2 100644 --- a/src/static/css/theme.css +++ b/src/static/css/theme.css @@ -10,4 +10,10 @@ .modal p { word-wrap: break-word; +} + +.create_drop { + display: none; + width: 100%; + } \ No newline at end of file diff --git a/src/static/img/mxgraph/add.png b/src/static/img/mxgraph/add.png new file mode 100644 index 0000000000000000000000000000000000000000..bf5f8edb1973c0db89872e1aff48a991ebc0d9d6 GIT binary patch literal 1564 zcmV+%2IKjOP)WdKHUATc!{PH%P~GB7YQATcmHFgQ9gIUp-AF)%RoqGHAX000McNliru z)CUI$7#)RzwFm$J010qNS#tmY3lRVS3lRZ-WM7d0000DMK}|sb0I`n?{9y$E00neO zL_t(|+I?1CY!p=#{^oCg+FA+}Tp*(Sw^XfGc_~s8B}zyn;SDfgO!P(L6XDS}!wZS{ zVB(8U24iBP@j-=%)PzJ62!c=qyIJfO+HSjace_6`vokw$uiu@{V%5rQPj}|d%y-WD z&UepQ4gY^=_z)wgVn0j_CDLX_gwTRUP=bq_XkZlQaRGOD281-ZJjim#XLwByaPrml z&kyX%^lQCcE8&MB+`xm=Y9e2m#f|AZ_+uiU4{P!?PT>sSs?manJNN`C_5YP8cD?cX zlY2EC3D~}aeB)0P>=~52G8(-01Hm^Y`j_VlbqPH{~;~(G-zB@5| z=-|H1!^XGs*++LqQC_N?MLnqC;?*e>#x2~PbTD@>!2GnphV4l>%@!Sr$XTcf=so6I zFBR;-7_P7)m9`VJ&H3I{L&t_P{jj|zE|<^2&~`dY~sE83%Fml zA*F<%6Uh=ANdaq9_wLq9wWBzPkBORBPV}e_O`JUZ)LtzJ4E$QTKo~7r4%N$yt{PNP z_RFk63ALaO!%9TpZ4*jHfrQpgLi(*ENaHy!tx&P+IE|5!%;10s6UbGmzE%w_mPo0l zNn-*qbrZVI_zeT5WulV^A%WW_lrbiuv%X!c)?f#A6FZ|AGEv6YK~37Yv9V0w*N;x39x^wm>I7-&+YhKG%oeu$c~~ z7#fB$UA>s03>#F6US5IxJCpEyF9H>$$PR31QUi61q}GMmW2~PQi-V`9?4x);`vvR4 zYrz|<^({Pkj8`2dAIvD}%{o)mw}c~HIG%%|TZ|l7mKD*X&Xt8kthTDqLmiTtrbRc^ zR%h0%bdUsEtV7ydLy~DoDHUnW&_~H#fJe?{R9*tcylV`*?Z7nSwMYgU1t4K6Jy30` zR&YC5ZKg@ps_4}=Fkz!q_ifVS!VB1Jgo%WfL`v&Kf`Dd9kBb;a&XPuwc!DxsR1%lz zgloz{qX*H68_u`8gvpIPHc=^z_+bDi2(V>$8gDs+&=_%-*MW0CT|@SOgNA6pVmzMi z;_b;n_%hA9Fe}M5S8-3Pu4T-;D29`~a@4BIVD$FXop;x*??ICeS{0^r#g3>_Hl1S? z6Fhert;V*%X#Fsav%h5FtDUU+l6tVTc99-g26DKkR%lHfKVmeOpU#JBztMp*H~v;8 zRxC_So`kqDD)Ax6}L9!!CWL4qreNc}bc2efcst6c z&3v8eek4Ih=ODw@{T>+!gUFa~zKMaaCAy zyGZC8MbhLV_l1Z7l>?6(sj`~D}WG10VWb0pA* zJvfR!^9UYUy#^N3mrTj0l!US9Op+gVSa@?aa(t8MfT<%cC@20JzZK7?Y z^~T#@z2n9@zZa`zhPI$MfB4Y$wc&KNkgWW*|>nsKyF47}{mmcIbUf6#rG4DLw) O0000R|Gs_u z_T$Ho@7}%p|NlP&7%2W^0n<7l5@ZGgitAS1881_0U#MvDLd literal 0 HcmV?d00001 diff --git a/src/static/img/mxgraph/camera.png b/src/static/img/mxgraph/camera.png new file mode 100644 index 0000000000000000000000000000000000000000..aecc94d6691889ed23459ae390f40097cd276e89 GIT binary patch literal 887 zcmV--1Bm>IP)WdKHUATc>0L}hv)GB7YRATTjHG&4FhIUp-AF)%O>gyEO~000McNliru z)CUI$919gX#a#dZ010qNS#tmY3h)2`3h)6!tTdPa000DMK}|sb0I`n?{9y$E00PfR zL_t(|+J#e1PZLoTJ)O?bNlOcDfhr#cS}2f6LriL1G@4-8bOZhY_yd@jnBYnluG|{` zf-8tiNu;=PK`fz$2x3HNLg@##oziwXo$1VUyi>*n#^_7VyiDFb=iPhmyEa^3HvE4X z*9c2Xue=_Q|H<&MXR=rnoeqb_y4@VpYBiy0s;;U{g3_wj>&ofr$(bM&-=|XBpNOjv z+N|I2d9%72k2jkVHaC;VPY8Oh|{GoP<#eS7=uQ#$xb z0qTSXpFc5&E)G$6iiB#&FgUDgR4%5@)F@nLHFbokc zmrA=pffB0KA6QtJ$JEpqWceH;Bcq5$A7X55#`KY)4^ObOvMi~+yLVQuXti5ZxgxrN!;JMjDa$P*iMT_;Ciih$9ymg#0EO~cS(voY{`c_b2B2!-ab zxcC^^?1fo7kwGOgCj)&9{KT!BI?4-=+*0IGqkMyjml2%QS<{ z=L`9(e%r0|y>vs1jH{7p_vl6r!QccO4kyKH*xgMU`}?22(@&kCvS(+g0Jhe7-&sk&w@4>4AM{9UUDU?d>H`N~JL?zfSJkB%g1J_58xF)!T1mI^H_ARiRov=b`RvKCOcr#Osung*LqXxMhR=?6W0r rJ}jGa_`qtL>5qzikNi;Ln<76uT|0TCb>1}52_e)iM3A8yPJyt+jx#L{D5 UsmJ!p)D;WU9;@U9FfdpH0IG}?*Z=?k literal 0 HcmV?d00001 diff --git a/src/static/img/mxgraph/close.png b/src/static/img/mxgraph/close.png new file mode 100644 index 0000000000000000000000000000000000000000..4de4396d4a09677774f79289de2a09511baeea22 GIT binary patch literal 1910 zcmV-+2Z{KJP)z^Q>-8KIssI20AY({UO#lFGm;eBCjsO7iaR2~=Yybd* zy8r;x$N&H_>;M26yq<`~KL7v*>`6pHRCwBAWWWy?p_swJ!GY=X=g*8ke*9qk`ST}Z zdU`qw7Z(@fv}x0Rf#f+jIR5kU^8P2LK7aoF=BZPs zUOIK^)MsF*`~o`QH`D=t(JcoEAauk84FygG1qFf7&``O#bLZLs4PE;m4ZL{q;_rh8 z5B{QyfBg7y1u$e3few}u6cpqKnFTeI3E3im07CaENCB6OjEqoBOpMCLjT`;`{{8y^ zWZ_n$O7C)YSBAZ*T9f@87@wgQ~r-ckkXvV`F1EUVpg2H5Y-eYuxnRM9M39EBU%&nZ#x+Xt8W|aVwYIj#h+{D^u`kKV z$v=Qu3ogdS#`gK?)2F{dW*t0uupVfZDo|VqWEaTS009KEm>Z}`3Ydy*?%%(^5TtnW z(;G*fzk0D zSSo?}K!do)4&2r=HI`6@Kg;DK;Y5@D8S6f$jA!R2#R=y z-@kw3EZXkgz5D;mmoF#|yK?2qKWvtQ08AWYDKNn@LoH?k2q3V5?BoZ76wef+J_SucZ|q}*4Eat0ppwvn;u{u2G>@=;`K8pCnvlB0SF)lXugsH z7X0Rlii*j=;_ekFPSCRwF!6l>MgdBk`uh5!#Cc9m&JXm$1Xy;1vlOsk$^_bB1{9YD zI&JkwlS1w(;^Z^)!@LCEWfM7lr1G>gQO-(Jludi=U<2%PtU4%`gn2LZ7@5Q~G#ZGZrRXD5)S*xA{=fSSvIdGkEBd`SvUNJ#h& zEi#`0(?mH?%mZWxEIR=N5WD~ZWo}@L!d6H~C^;o1WhN}%Kn9R%DX3Tl1uIBDuwk$s zD3%Dsw#WqtKmai#w>W@Fo)wt5IG#Lt!pzOh{RtR^ngIa;EI|DX!1DGl0}&uLHkM`0 znlZR5`~$4;!Ko9NO&EZQ5}bNLd|;LO3|NQX zDk&*B3FO`b6<$F66o}sdF(~nY(#>Ch0AfUH2(Un1!w0oo5@@*$Fu}2#z zG+4x;SOQq;hynAf5U?N-FflO^1=e1m<_rU{W%&_U5xfEBgts6ML*w!V5Wj@tchKm7 z#Vt550|XEwmS!y*Qm}(kyC5{X2|-gN4>0wE+fTqMj{(U315A|Pffy8wAD|Y29Pu8S zm|(&C3%QvK5I~Grnzu|)pR++TIyW=~c%hErLTWoPKn(*K{0(XeEWv#R;_pxkeqBrOP0*Db$8yndX$XyynEc#&m wB;+m&vSk?cJ3s(2G7t}lRr{Y9MF0T?00rpcocj25u>b%707*qoM6N<$g7i zGH1?|IZKxwySMh(wYA6YZQZ(e>)vBq_ntd;?A)<)_l{k=ckbM|YxnM5yZ8Ja2t2>{ z{QdLi??K@E|M&m@e+Pp9qhK@y24e{5fN}sRFEDTfG01Wl1T0`+X5yCPU^p1i;w-?% z)9^!q$)ST^UO?l31=E2J0U0iV6&wmcJrXB~_>jRZ~q>szcq>Q=J;n zFpbop7PL$&wWtkk(@t%Ujg_WMC8w*y9OPwkW)mkLKawp5-Z6PmZH>3vy|m1xAH7!1uCo}D_DgptI8@?quQ#onuB)H zidp0oJFJlnwxGpYvW2Z^wbpE9JKC*1+u4B*>&Omvq073mi{0q9?(F7hP}7>(AKFP6 zk)u_kOcbN!XutqZHEIr80}|cnIg%ZLGK`TUg|R%#SUDUIqikd6P#={ijm$BvyqlR| zytC79dJ|i7KVdSrxORDWGQ0lz_~OpiZ{KZveB#Qb7jO3`m_PHr|GT<7@26L9&0Kr- z<mPT1*RLio?%%X~ PuMXwY3s2V1FJtq6qc*w6 literal 0 HcmV?d00001 diff --git a/src/static/img/mxgraph/copy.png b/src/static/img/mxgraph/copy.png new file mode 100644 index 0000000000000000000000000000000000000000..a987d4397177414f386a8a819d7ccb7c4091a5d9 GIT binary patch literal 728 zcmV;}0w?{6P)WdKcYATc!{L3L*!GB7YTATcpIGch_bG$1Q5F)%RM{uz1z000McNliru z)CUI$94NP4v@ZYv010qNS#tmY3h)2`3h)6!tTdPa000DMK}|sb0I`n?{9y$E00J;c zL_t(|+I><*)FHsQHeVNL`^kK}IO{y4L5-@8i`2R>%mo%v?wn+>qWM$S)K z_F<@@2y9Yngms0b@8yAs$HVvO>KDO13ZECm1a0|N96C{0%Zt$qPf^J4xL)c!v*G#d z?Qy#L#w6m(*dt$2K@vX6A9#P~73wm?(SzI2p3g$7h~&u0E_Ro#HNo_oySJFcb#QT@ z@2`r3m+s-k?1DQNa#`mmZp=7eY(G3g1oL!!nKD`@N!D5zLa1F+INjgtNS-|@qwiP; zy7uqHsk3`&_!>WV64s06szDGi3j(jmweczE-&L2Y=#Qp%j{VQPR5V(f`0000< KMNUMnLSTZ1qdVFF literal 0 HcmV?d00001 diff --git a/src/static/img/mxgraph/cut.png b/src/static/img/mxgraph/cut.png new file mode 100644 index 0000000000000000000000000000000000000000..52bf94431fc3ad71a2ea021857e292b9755f8a06 GIT binary patch literal 781 zcmV+o1M>WdP)WdKHUATc%|O<`#uGB7YRATcpIF*Z6eH6SZ6F)%Qnf5sXB000McNliru z)CUI$94{$3A&dY3010qNS#tmY3h)2`3h)6!tTdPa000DMK}|sb0I`n?{9y$E00L!6 zL_t(|+GAkA4Hy}S0L<6~7cXA)Kut|e=IGI*=YZT_K>Uvw!{A`~vZep;+`02FEG%>a zkShWo1SV4$dGT*A$5PNHKu%)2C1GK@4_wc1|Fr4#d2K3}8gU z3_pMV{PpqUM+P=lR!*p9R>_<*Vj|JoH5jBLxR5PonK6CZ4-^UFVf z{=Srwl2&{6>=^?a8!M-0aEz>8($qsY@4k>YbMY;skahH{&v%q^f1hjH3Dp0Y<@4vy z7ed3r&4q-7gn`=>jf z92~Sh@7%$t|NBUZ|;X{`WN?Mo)1*JTY`SzPt>eu@> zH-W<6fcQ8NKZ7RkPaqm=UikN$<@)>AKlSh3|Eh9+&+*59F97}f_uFHj)ETG&AjAH` z5)d}P0S#CyAeMqU`4|vifoeu1TO0;3LJbiFVoqony@zW4kE{>?eQNj$0mvl#00000 LNkvXXu0mjf%B^d! literal 0 HcmV?d00001 diff --git a/src/static/img/mxgraph/delete2.png b/src/static/img/mxgraph/delete2.png new file mode 100644 index 0000000000000000000000000000000000000000..be78c614b59c6f469e163c059229b3fbcf297189 GIT binary patch literal 914 zcmV;D18w|?P)WdKBJATux^Q)O@fYOAIhl9J+|sKf}hUd$*e5Wx%|N}}F+D(JNydgw){ z-g=B81PfD(kp5ViMOGSUWun%mX1eL!AGdq&?)`P{-INVJ*v|QV-_H5YIY8Ry1JE!4 z+`kLd(N70~t><0?=ld95+y+{50Fgr0bS{1YxBSfqb@h5HA7E^lLi-4rM(!hUbfPp* z;b%&@uS(?IRyIcAVnT*xL!lt|r1#h!X4?c*G?pLe09q-!?HN$Zykb|`p#lLh8A_&r zPa6M2xduYchQPEx(4)@&1!NgpYj>jE?L5~49CzhsKdCuY=7JPKSXS}=b8x;BSR#sU zV55(EAmrAy4tH@rLoAMhBGKvfg_`Qxs+B)GOO2hii!?ozs!$Y@Y<`cakT@drZ*v+jilhe` zQnO_5r~_-E`_bX{{bkw4;MQU=TTzk%nRbRskKirz$HV}~kRO<*SE?!S9B3}8ExK3P zSZue1q-5!d1T;m4g?iT$8fj_Bpjykf3!c1X^@vO53slC8;emzJqK3m*4iY(TjxiL8 zqIX~}G5tGCtEhqNBtoJ##?iFb?%4%gAaXUqY00&+n>&iKsLz;5UX-Hff3+CA6jmR0 zj!L~Vev0clI8~x(8b+oS^Z`Foo9MJHX?#!%N2+88!z4X^IVOF-rt^^%z;_22&-)gR zwC;Aafi`q%PC<7->!lH_5qXgyGupP26N05nx3Rp(F>SaX2$Q`(lY_w&LA(rHq2&?j z(4>Eqr`Q+58^XMCZ5f1;5R6M9R1)l|_-tyFDodTLk=UGi96@s1 o)U+it_QjQYOQwL_1~(G_0qrJ3Y23kQ+W-In07*qoM6N<$f=RE9(*OVf literal 0 HcmV?d00001 diff --git a/src/static/img/mxgraph/dot.gif b/src/static/img/mxgraph/dot.gif new file mode 100644 index 0000000000000000000000000000000000000000..08b9947649822e6166a33d18028b389973493a75 GIT binary patch literal 517 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 L$MO{}jttfSRj*FG literal 0 HcmV?d00001 diff --git a/src/static/img/mxgraph/group.png b/src/static/img/mxgraph/group.png new file mode 100644 index 0000000000000000000000000000000000000000..585ad790df421dcd257bd1dda836f619956fd3ab GIT binary patch literal 899 zcmV-}1AP36P)WdKHUATc!{PH%P~GB7YQATcmHFgQ9gIUp-AF)%RoqGHAX000McNliru z)CUI$8z*R^KY0KE010qNS#tmY3h)2`3h)6!tTdPa000DMK}|sb0I`n?{9y$E00P@d zL_t(|+D%hUPZLoTJ@ZAUv{cGhLs+2EB8rBv&_oPzLE=gg5~D8MxgeTIOo)GgjZ2Lt zE+`mxBr4H02?mX*Ary@oq!3yHp#@5@)0wuk^W}YQ2Y8c{d2{p5opbL!kHgXR6wOdE z)L7rpSLg8tO~c}L(@HEISEi6a2os3VsRF@vDBJ=06>fFN!O;B+{XTaUhZSWR2`vew zn8kKR!>{FyTsB%5!Ap$O=`_J$G{GTXZ|<2G?!F^0Yhi?@XTZl>P)K^w)SAJXl7!nY zNc)o2tJj%2e&a8(@&rr3Gc=#|otk>weOLaN_<(d_7v7x?Ob)z7|EZfuw*A1|(h~M{ z3%u9n@|JMX-ZYl5hYgm5hk~JdUDtf`yR%Sr4Te>&NHa9#Os%vxEen=uqF6M+`>SQN z;|7IK5@fOVn6J+(yYXe3r|)2i(9?DqwqQdPd4$7ZwC!Cf#T&cUF#dfNXz^SEvKTP81JsnHHDG z?vxT3rj4R*m4+jU5+tb#qU0*q zf`oC`V3}CS10?|DA}MG3qPC??Z7E9d&rTfE(I{-&MrWoMRp+Y?T=H;i3`Lui{i~(E zpP|atFw??B^VmdaF|wA^Yy)bc0JT^=lx&u_MVbQglx>6hW=0LnBTnIULQcdaB5Hkp zq#y`SgaF4ef3a?lI?~xndx+dgwn^Gp*nW=;xd2#X1#uNS*W)%GA;=G-!6ieaKk8)C zJoQPThO6{8Vg`N^c@C`pPlOr5wsRLmLI5Xll}yq{bMA&qZiGe2b9xHvnJ^Yewo>}@ z9N`;*m0=uSrg9R0#bL}Ot%Mq9c$J!&>=i=FxlBhBINvhC$4o+yDsi1PjF2JhvrFU< Z{sTHZE)<@5Hy{82002ovPDHLkV1nnJh=~9I literal 0 HcmV?d00001 diff --git a/src/static/img/mxgraph/handle-connect.png b/src/static/img/mxgraph/handle-connect.png new file mode 100644 index 0000000000000000000000000000000000000000..513ab2b41b53049dbaa4216d48a6d3e1faf79e1c GIT binary patch literal 1300 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 literal 0 HcmV?d00001 diff --git a/src/static/img/mxgraph/icons48/mail_new.png b/src/static/img/mxgraph/icons48/mail_new.png new file mode 100644 index 0000000000000000000000000000000000000000..16c66628067daaa015199a3b8966cb1134225eb0 GIT binary patch literal 3944 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 literal 0 HcmV?d00001 diff --git a/src/static/img/mxgraph/loading.gif b/src/static/img/mxgraph/loading.gif new file mode 100644 index 0000000000000000000000000000000000000000..118f4b0dff870db00602981cf0be59223eecba37 GIT binary patch literal 7517 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 literal 0 HcmV?d00001 diff --git a/src/static/img/mxgraph/maximize.gif b/src/static/img/mxgraph/maximize.gif new file mode 100644 index 0000000000000000000000000000000000000000..e27cf3e19f622e66991b11bf09f20bf15495ff78 GIT binary patch literal 843 zcmZ?wbhEHbWV4q+~q qfC~=}wed3>_*4Wua_$n=589F;sC>LrTCyldLuj#62P-oZgEauQf)%F# literal 0 HcmV?d00001 diff --git a/src/static/img/mxgraph/minimize.gif b/src/static/img/mxgraph/minimize.gif new file mode 100644 index 0000000000000000000000000000000000000000..1e95e7cae4027b7a102220aed8629fa2cf0c7bc8 GIT binary patch literal 64 zcmZ?wbhEHb1}5>Ae(}?}yobAUf;iu8nsR)) NM#79r0ZB0iYXA(c5v~9L literal 0 HcmV?d00001 diff --git a/src/static/img/mxgraph/navigate_minus.png b/src/static/img/mxgraph/navigate_minus.png new file mode 100644 index 0000000000000000000000000000000000000000..71edaf9993e8d2504eed39d17aba1c4be7df5c60 GIT binary patch literal 485 zcmVWdKBJAT}UMb!;FqFfcSAF*Q0cFgi0fAS*C2Ffi8GJazy800(qQO+^RP z2L}iq23ft^VgLXD32;bRa{vGe@Bjb`@Bu=sG?)MY00d`2O+f$vv5yPs7*S3pCt7uTDZ;*bPa zHj2TLViE+q=eWl7LfpTcC_bWmEp&jneZ~GmIfu05ANcNZ^ zH0@(e&10sM%&ZAz&R?FnH>egBv`rildl=97PN)-)#HA9@T5PEyYy1awwbN((^+)>Q bzP}Xjq;{CKZX$qo00000NkvXXu0mjfvk|`C literal 0 HcmV?d00001 diff --git a/src/static/img/mxgraph/navigate_plus.png b/src/static/img/mxgraph/navigate_plus.png new file mode 100644 index 0000000000000000000000000000000000000000..b5b7e8706fab2e693b717d42d92fb686924b7221 GIT binary patch literal 709 zcmV;$0y_PPP)WdKBJAT}UMb!;FqFfcSAF*Q0dGdeXeAS*C2Ffdu(WC#EN00(qQO+^RP z2L}iq2QIqn^Z)<=32;bRa{vGe@Bjb`@Bu=sG?)MY00d`2O+f$vv5yPxy!6??L@Z4O7HgeV`kpGnPr$Bx1Kpmi21WaghBL7wVgp>o=|-<_l7swi@*@FULY z-)!o2`qVII@R^-q2U(#p%z==s^I~$3Sxlwy;H`sxizDhXC<>S7wSuNNBNl|rFknoh zvh1RMrwVsfp@@KwkJ5T;Nz;PF4?@aM&sxa;aFO)N0SuD=eM%w6k!m@2cHgkPSwZWV4q+~r s2m^+OR(5H-5{bZvM><*L<8pRvSbVgDPcw|A^PWdKcSATcu_MrC3kGB7YQATcvKF)}(bF(4~2F)%O}y-DBz000McNliru z)CUI$9u3ZK0_p$&010qNS#tmY3h)2`3h)6!tTdPa000DMK}|sb0I`n?{9y$E00L)8 zL_t(|+J#e1YZE~jerB@UHrchBG*XCdsY*}~K@h(vD1IP#tS3<@_$w-uf_Sik9_q=H zrDu=Yp7hX*2a!roqHUnyN43Tpo2J?9?tJu}*(4e%IPmQ3`|iy1KJPQL44s3S$JEr+ z(R@DtObDUo=jU(F&CPwFbc3Lq>vYG4;PJ65$PPYWj1|kOFZH`Pmd^Aa=C12vW95z9 z`~FEIC%?(Cp;uln|GIyD=EqCgwoOourO6LH?Ao6{!Sg^d-b`Y(EtWqp5|+ z^$yAzhYd)rLsL#FQ%qs~XXW$6#P|)``z}!sMNI`Nv6dF6Js&r3jddJROU&ftodXn} zBYd(1T1sv=OW7vSTA@}GD3$6+r5seN>)6~}!@$6R6-mR8WD)`uvZPOyv|waQ4c~V# zG}KLva)e<&f*^~Yo;@u=+_Y~AWSkh-RzL)%_ew=#X(>P#xnHZTlZr1G9ve_h0c0AhCqK@d~b?{Hm*F4i`pXp0ba7;6Z05XLLvX7zZt z?urK{o4AWljPeq|**OudijIZYx+UZ9F=glCx51X5?A||2(Fr$6Jfr!^E6- zuNmu1$eQ_H!WqH{VPDfAhTVs5ZY~j?w-%8-Fz1saxcL7tBz5ys?Eo~AI2dn*Kg<9C N002ovPDHLkV1lu0RB8YK literal 0 HcmV?d00001 diff --git a/src/static/img/mxgraph/plus.png b/src/static/img/mxgraph/plus.png new file mode 100644 index 0000000000000000000000000000000000000000..24a84bb492bfe79e313da4079162ff42d2d7c372 GIT binary patch literal 236 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLlHK)@j7&Flsg^73?X45_#^rQcJCL4b#)z2m=qWdKBPATc!{O<`#uGB7YRATcmHG%z|eI3O!9F)%Ro=a_T=000McNliru z)CUI$7ZQ@Cv{nEB010qNS#tmY3lRVS3lRZ-WM7d0000DMK}|sb0I`n?{9y$E00=Ef zL_t(|+ND=(XdKrSKC{nR?P{f!y^>YSt{-Y1N@JDU_(2q~iy(FUBg7A=}LJCA$5^PTUUdq<*29H&qJ`$VNudfBq{$$+Aqb zSd3a*Tg4a%1V{ohK6o@ulNJ{jDVNLPO7Jj#eheFf)gBwb+}w0L5(!C&hPH3tF0S!8 zJK?cdERqFE5cvb6rfJk{HUYS3;gy51?4tlk8{O#c-V!@{^tXSM<>(Ln{$Q7F>n(UZ zVB3;p7`AZe_Xog&52-L2n1yR~U2o=!#i_|u>U^eBDU4#Ozrp4o8vqVJ<8Y&R(&zKX z1A$=B=L`7Xe*0GgCr@R+bxjC4^x(wL1`uBl>@3jO15_0Rugg)fiq((!ddc97y8e4dxXPyd>0i8 zWx8|c7DXZvQI_oRzh(>8b=|DP`FHW>OW5M-RIC7iGiBShM9byNS4mYZ@r-4ePA=TZ zY}--$=FJ8e2qSua-Nfh6_*ZZ5lQcLuC;%}SRjn}q-PP5)B9v5B zE1`TT6!M9e5Urih=Rtc@GEI%5Q6E8Gun0{}r6`f;LOQZ2!#chD?n_juWTmTDuXS9$ zoStkp^PqT}#6UR^U2iY|?NIY+QzLT5dF&ctLWh zBMD6i&SRpp@ihj>lMK@Fca1^YX z{ei%-LakPtx^m^p5@+0F!vkQWk(AjH%fb_orkN{9Jz;Iyw24lhJc+!j0s!~@QmG`? zg|J)MC<72=*y{8Y5-GB!EVBq*Q7uf3~@cX?w{KH6o2Ps}ONye1XG5V7p(AleJOrD42mly>2pgzp zC)XUs{Ct+)fB(<)#v5;nmC&mO?h70_i&dkh+$;09BN~NqGS&+;QGpVX|Tnpj(RvI6_ z_v!ff^||HcDl;(Sf>_&%%_1}4REiTpI2;i95k z9VRAbP?pc(o&^RMv9}mrD3_HqCcX;02U8vbV47-?sp2Z&6mS}`s!8v?_a}Pio!ipAcHFE#nym`m3w`@uJve`vZc8XG^W5@c1h<5MZEqVZJfK$RwIbsIj^?VP2 zvu!g_QBm5rZyy@d?!LCR_Jf#nY;JCD#_fs^E=C`JoJMz28{BHWi44OM4a*alwNC%Aawn!g5Wb^XJdg z>#zR|`@c5P{n!y3Z^<+vaOdDMWnk=-MarcM;hAX9ocp_IM7&w8tXl<)>ej8)?{vL( z5K%_5+1YPod75_Z`qrUZElgNQD4kA=`uAEZGr%pH9kM>yA^Si@ETJ`DM;hkAP!nL7 zZvI-i>#hU(x(tkr{3V^qjJ%S|-9Ly8NdgXPufF<tAshSSiJ|-v#_X>I#KbrFV68^#8K>pIQ54r{L9G+JjvvfhV@WdKBPATcx`PH%P~GB7YQATl#LH90ymG$1Q5F)%Q>U7vUW000McNliru z)CUI$7ZQ@Cv{nEB010qNS#tmY3lRVS3lRZ-WM7d0000DMK}|sb0I`n?{9y$E00);z zL_t(|+MQQhY*X14-Pd;F#MpT_FOtbSv6Co}kVy&@`Up^IM$@XTG##mbv=ZO+r&Yy& z)eqFKqH6kqDg>gPf+(8OiYc0si5fbYq=-Ps147;p66fta9NV#P*E+r@b=sr@9BJp? zdwkB`Yp?w{Cx+}l4E{NL_N+56FRx;Id%GqckNdN-vZ@OU3w=&!TH&2LfBxm|+qXZ) zvUNyI-e5TN%hjt_bC#Evo42;M{Ap=v{(^#nDzDe;!+U9Vc6MrRZmyA+moIj^ZD(^u zwg2+`d?2Tze-5!rbq6tG$%P5UUt*3q`uyS90LC+WM zYtvIxgHW|f+^-FQn~(#+l9G~zP-ye}4R5y?;qBcX?-))3!uK?{OX878QQN1V^nisK z=nB0F0FKy`Cr`#34>z3f=D$^*>CII%Cwsh$4eOmOc)SIfoqxYGzO=OT40=|q2!83X z?J>6=n4kZ!v!mUN842a#SR&q$Skia$J3^IHTq^YqO?BAM1WJEZ2KE9FyL|cb-KmMu z(S?OIrIj?`BHVc%!oJap)g+nMJ2L0R3~|H%Vg0i+2A?eDG-p+r~i`i zzE0bB-#zoyva<3Y9X)#VeE=yzNiX;H^)1UjO=?es=g*%{K|gpm-R1uHgP;7u2qVU( zkfTAn_ADqUS&F(l?EQ&UsF zx^?ST7gTIXQkg0M7cX9PxLmHU$6~Qlkdv5dMk~(f{QQCO!B2l9fyoh>85@w7BmFWz z`CRSKq`_>jj5d0YTFe z98|juq7Ml4DNzOto21xNBoYbBM}PcCy1Kh=U%7JS=lE$H5^-SRuTP#l>BC_o(P)%n zJb(V&Oi4+JojZ5V7#kZi78e%{00Ib)5Ty2)bJ`G?Q~L>kf)J`saKG(S8FLJZE5j{k zzV)8Dwzl@Y)2B}l-M@eTx7ar6z@{u3x7lnvl;LSWp&ciq6crVjjg5^aem7E6Q*5@{i+L}Y=z~I2QvE%_{*`d)o91is)2pS+D zdWUyoc6L@#PVY6v$%eVlTB|^XLLpEdku;}E(q{|Blj@T1nBS0wjTTvrtk|H7y`q-s zq3#2qTNzZ1!D0f(vQnzQ$wmO>Mi4ZljZ(tluo4>PItl?~xIFUBrT0-eag=aW=C)^5 z+S?PCiX4d3p*>1ZPgh2%oZn=GBN+&vD=RC4iuoUd8U%&LDLO~s2i6T866$nX#+%IZ)j*x z3K{Fij~}b-W@1uw(rG3LXl`y!&|G#r)xU8BgFyw5h8a2<#YIQGb1j{-)+T76a{kC# zW|>u0RjJLBlan$qFrZk$B~*dY;aXo`SE0~GagR`YC^SH$b#-+LEWtc`_Drp>sHoUU zWIa?45CAFG$;IYEA>!%jX+bZL?(S}ur0}}Nb?6O>S_&?%%4mIkz0yS>FJ8P*MyZdC zaxW{1Y*{%#6y^>9g$9WEtrL;n&|qn4sfzc=$cS`ycFOSZuxxB>D1{6k!80U`EsgS< zy7JMc$tn|^`k1t23%Y{Vd0^UGTU&pKBrL)rgY*y`6;Ldsx;d#Z;FMI!#c%#jhS1|x z>WT!!-o%_fww1GT3Q@9N>!;To{|{R zF~58Ft_lmirLaTNOA3MOW`xQx!OQ>-qh92}2Ikh-ej!5;o@(QC(Fu7A;yZZoVB@i4 z$BrOt&!GM0A3uIvC8Zv^L*iIf;b4wvb_kB&pw|Q&GyVPj-KeOg)zwuVzta$UJObfK z$UEaE+l=OzgrPfUDAo>9vvsa|3egoI_E{}uMTvvHoKaR8DHDMqQ)A_0#e>rcQt zdrwbKANoYd%IFEm3&C pzAk{|wGN9Fw>7u9S6p9b{2Q6)WdKBPATcx`PH%P~GB7YQATl#LH90ymG$1Q5F)%Q>U7vUW000McNliru z)CUI$9uh-m4<7&k010qNS#tmY3lRVS3lRZ-WM7d0000DMK}|sb0I`n?{9y$E00P)a zL_t(|+I5r9PZLoP$LG%E65 zW~6SfT9y1zsl1{n-Ll*5Y`Tog?H~fcaXUu>U%yzEyxwI@OCZ9Y0)D@rZ#Exd+ibn( z7>yo^C_*V=)1t%}mpyaqxmn4EB7S)LA7g#(;5nM_X|ryW=;riG(a2yA1 zEp5GR0{Y)3GVO@ zA(q&JU_Pk9Vh&ki>M)YYBy4YQgTY{s5k--njQouX#^Z6w=krjj)#Sz0ntD1qJ;E^f zfmkdSphO}O2nK_I9s-}w2eDWTP#*{(vOuX+0;AC=Z|(O1FnBZ{=jZ2#2L}g9Yilb| zXrQ&_cP5jOw?{kEYPG`Z>gp#9>WLImp(AZ+xuv)D!H8lm%Xq0M5 z*vZP`;-a4#4(CMg#U?^eVVMfh4VjSkhK2@PcXzjaXlQ5vTW?%gSXjcM90rg7eNd{6VjhS>~ba=7+6H7SiPt~EiCT7Ouqnw WrB)%^q(A8Z0000WdKBPATcx`PH%P~GB7YQATl#LGch_eG$1Q5F)%QMP#j1A000McNliru z)CUI$9u%qssww~g010qNS#tmY3lRVS3lRZ-WM7d0000DMK}|sb0I`n?{9y$E00P%Z zL_t(|+KrQINK;`L$Dgw^r<>cHSIn8D8B^Erl4YqxL`4N%#E4;9$Q2!bH` z&s+Pon6juo_Th*o->XWhS3W@&Uw%IzyJ4npZ9qP z_$L1Lt>i_e>#dXLNe<_g!}_7GJbgj;PuLPO`S!iL)0Vs##2LkWd@IcWe_0HA4S zq0Amo)Y0)YIx(lWp`HL-+K!o+V|Ctyc+=GK0F?^dN4$G$q;7+Qd#Uw!`J*58n=m#W3ZV z2O8z6VgU0zo#0%S1=R9U#6+3JxfG;GSE@|}J=M`+)A#n%_oo8gV{QneK?o6J6zOO% zNrjdY&hday7g3V2T8*m8`YmB&_ZJ}#k=StikXPSjwlgZ?lZQxO2yx2BjVdBn4$w7d zafEv>PNU9%eB%ou1<6NpkXeLx$Gsp6Q6oCo&`_Sfr)76xNrl!m`f`ANe0B)GcOkZw zisDUDg~Y`ED02y-6wHbctuW^!;CsZqy7SiR7rbqdA}V3CFv8y)`#%P2wR$q~KLJ0H VDsF_*qTT=i002ovPDHLkV1lt@lLr6* literal 0 HcmV?d00001 diff --git a/src/static/img/mxgraph/resize.gif b/src/static/img/mxgraph/resize.gif new file mode 100644 index 0000000000000000000000000000000000000000..ff558dba03b55e1cf337c540d94722b5f72b7119 GIT binary patch literal 74 zcmZ?wbhEHbP!E>{@(Bh{^STKLRWtPIuwJ9HO^ literal 0 HcmV?d00001 diff --git a/src/static/img/mxgraph/separator.gif b/src/static/img/mxgraph/separator.gif new file mode 100644 index 0000000000000000000000000000000000000000..5c1b89566807692c4e28c65b4ee33b4f42f4c9ba GIT binary patch literal 146 zcmZ?wbhEHbWMmLzIKseCRaxKC*ioEQQeIY5ThmgIT~twCTUXsum{pXQT~b}ul$Tjh zQCe41*)Vg)yvmBY`kGb-Qh?%57O-+15DBuAft5euf&=?PhJHf@r;RE-!b}X-00+?_ AJ^%m! literal 0 HcmV?d00001 diff --git a/src/static/img/mxgraph/sidebar_bg.gif b/src/static/img/mxgraph/sidebar_bg.gif new file mode 100644 index 0000000000000000000000000000000000000000..67e824486baac86e93cdf52bed49561254853520 GIT binary patch literal 80 zcmZ?wbhEHbG-qIDn8?5&sOQ)w@` excS(@HT+JOJtA*gmOSXPY4G-&-dV)TU=08sqZhdV literal 0 HcmV?d00001 diff --git a/src/static/img/mxgraph/spacer.gif b/src/static/img/mxgraph/spacer.gif new file mode 100644 index 0000000000000000000000000000000000000000..35d42e808f0a8017b8d52a06be2f8fec0b466a66 GIT binary patch literal 43 scmZ?wbhEHbWMp7uXkcLY|NlP&1B2pE7Dgb&paUX6G7L;iE{qJ;0LZEa`2YX_ literal 0 HcmV?d00001 diff --git a/src/static/img/mxgraph/submenu.gif b/src/static/img/mxgraph/submenu.gif new file mode 100644 index 0000000000000000000000000000000000000000..ffe76176db93a40bbfcb96e60789886f68de0e49 GIT binary patch literal 56 zcmZ?wbhEHboxuA zlgc^UO&TvMW^6WYI4_^JL8EA&TK;aW(nH#1N6cET=vEvPid>?Uz0I`gl0y0>22`N< rlLaiG10q3oGO$V{EDVYKtZ+mq;);q(`xJv24>Ooq-qh5$GFSru1MD;X literal 0 HcmV?d00001 diff --git a/src/static/img/mxgraph/transparent.gif b/src/static/img/mxgraph/transparent.gif new file mode 100644 index 0000000000000000000000000000000000000000..76040f2b09d80470ad0536404ead54c3dba51d8e GIT binary patch literal 90 zcmZ?wbhEHbG+;1bXkcLY|NlP&1B2pE7Dgb&paUX6G7L;jF-siv0egDVL!_|Id(Z?5>3=Gx)k9{Vl literal 0 HcmV?d00001 diff --git a/src/static/img/mxgraph/undo.png b/src/static/img/mxgraph/undo.png new file mode 100644 index 0000000000000000000000000000000000000000..4ba0ffb168106c1a41ca49691efd2c1811a9e6ee GIT binary patch literal 879 zcmV-#1CacQP)WdKBPATcx`PH%P~GB7YQATl#LGch_eG$1Q5F)%QMP#j1A000McNliru z)CUI$9wdz2nuY)X010qNS#tmY3lRVS3lRZ-WM7d0000DMK}|sb0I`n?{9y$E00PHJ zL_t(|+Le=ENKJ1d?+N)W@aV+Nt`-$TWXoEjZJIL{k!{f*S&Y08&?X8{`}wrzx(^0bAI=H z&p8zQeNh{}6Mwq=u?}=+L1OhvYIfFf1(EAkT`RMMW{q55b35J!5l%Fd{nq)^Xw9Yq z#hY7~j~%S9E7BI06e_6V(%c}GyFV7s-wdvvNwf+h!mk0&*h=;Jbbm){WyZ7j05e|z zoFM>)1)$T_AgHs;xG)<}d^XLAK?t;3+ftNd7xSR;Jml3zQZ=E@J1y0kP745`0c@rz z)@kZ9Q8IOsw*1iUjGYEqpg*Guod*#=sne5dO?!(;>v)p`)OoS?vz6NimH<>q09~EE zF2C{WD{xPQQ8`!8+hUfdT|cKZ)RjfeuoVNC$x0yP*rEe{#gWElmJhmEoQii$GH>V_gn%pp@e+C4-za0OR-v6d)yqZg^M(NADTX z@T*jRj*oLQVYl6l`un%c6ssGTOL-n;zhz9yD}Hjgx>#aFCn4t(H=X zh^Pl>&GrEfmt<@M0S4vyhI|BIO8H2tFdH zB&;FvM7QPKzqBI^MNqCrxnAz>u=jnnrpqf7md!#AN4BlZb8jDY|E002ovPDHLk FV1m*$dWir4 literal 0 HcmV?d00001 diff --git a/src/static/img/mxgraph/view_1_1.png b/src/static/img/mxgraph/view_1_1.png new file mode 100644 index 0000000000000000000000000000000000000000..88657a1edadc3f23ce4c3b5b43bfdd90837d720c GIT binary patch literal 849 zcmV-X1FrmuP)WdKBJATc%|L3L*!GB7YTATcvKH8(mnFd!>1F)%REhL}eH000McNliru z)CUI$9w>!t)~x^l010qNS#tmY3h)2`3h)6!tTdPa000DMK}|sb0I`n?{9y$E00OB= zL_t(|+Le-BNK;`L$DgxL(OgS&et%t9w3Nl@m-(6aBU)3ZgWpaLoby+7q%-bCemcG7Oln4lcSTQp_t$NnC@=;8K*+oZf>aGIlXl(^SjKJAq6R0TyU%NW+R0-(^j+N+ix-!)6jMarg zq0{Fv96&e%u)vCNy{Q>^G{V~eh+#-I9;CDyO*U$i5!EJx&^qH=Z{c)p0+-qzA^I4O*&ty*F(lOh-T0pP~Fjyd2M4 z0|A!Cq0zV>TU@dYU$OOV%gHwEfsz6+Wa{%P{7$3AE_`2;z$bUIajNo=){#|Y39$#c zpuJ-SMvwSb3^_Wj5f0@;d{$oWvhnhXAxqCk)c9eGkguoJ0_^@&rfuUNmlK=IOU_@s z)PwmtB%RvIAsQMq$*rlcf9a+dz5SM6+UazTVE)~|8vHgv@{z;Q{Wvo5KL&DOBcPA2 bStR%iR@yLCHUExD00000NkvXXu0mjfO$=_a literal 0 HcmV?d00001 diff --git a/src/static/img/mxgraph/view_1_132.png b/src/static/img/mxgraph/view_1_132.png new file mode 100644 index 0000000000000000000000000000000000000000..e9a1b72993cdd6cf8b40843d59ebf6b275f68b0a GIT binary patch literal 2199 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 Z{sY;b4iG3(D2)IB002ovPDHLkV1icv71aO$ literal 0 HcmV?d00001 diff --git a/src/static/img/mxgraph/view_next.png b/src/static/img/mxgraph/view_next.png new file mode 100644 index 0000000000000000000000000000000000000000..b4094f04e642e083b07efe53c7657df534534ea5 GIT binary patch literal 918 zcmV;H18Mw;P)WdKBJATc%|L3L*!GB7YTATcvKH8(mnFd!>1F)%REhL}eH000McNliru z)CUI$9w>!t)~x^l010qNS#tmY3h)2`3h)6!tTdPa000DMK}|sb0I`n?{9y$E00Qnw zL_t(|+Le<_XcJ)&$7eU22Ae*Dh*nw?i;5T@CSqtVm#k|ty@u97g%YhOo3+Ayt@y~<>n)6kCoa>O=U&)m6n^F;BQ4Ocf; zxMu>Rau$m4TY0w}SZfA|VG>1Gl&zJk?0uiYW;7V@BNw0!Q^NFQoTOw9tyt*>SvP?p z08<$Q95xHc#w0ZC-Zs+&u~VnQ>11z^SDx<;;%#yc4q_<8K|%2nNe0jv9WsPKN;9E^ zV*zginx?^@W|$_0KpF@=BqDOt$wWLR6T7HqfW=-~mVlnm0Wlf4($)>A1S~*y!#Woi z82D!V@}*vr7Wjt6CKcs`$WVt>l#GTzcdy?avltLACsOZ_oml+HqP-Ue5PNEgD>AzC6lKb4-0f1Z~Jx@{J=#L58CVmz7XXq*tf^iQ9^*5uHKpE^#iKB``6 zyxEKTFR(C3i)SZHDMZ{0OBPgHIj&Sz6jqWF=D{P!ox4`qcI>Q90u0CDP1Eb{nwN>Q zKMe?AzDflwxdGFcCCPF+l1xp7RaK)q{umwO{f8qj3!RJRuYmQ-plp$2@%B_~sW+0l& z*>FG0yEobHE7!LWenKL(`}TKW{1~#3`I|z{=#pLaXU?<=qu-?-Z|7(%7Ngr}|5t$- s5u`l#0;vrmL;oWHxxdhn^JTh;-{QAwnjz0-1^@s607*qoM6N<$g7O26Pyhe` literal 0 HcmV?d00001 diff --git a/src/static/img/mxgraph/view_previous.png b/src/static/img/mxgraph/view_previous.png new file mode 100644 index 0000000000000000000000000000000000000000..b385b44c34c5abf1e094ba31abb46135674a3fce GIT binary patch literal 912 zcmV;B18@9^P)WdKBJATc%|L3L*!GB7YTATcvKH8(mnFd!>1F)%REhL}eH000McNliru z)CUI$9w>!t)~x^l010qNS#tmY3h)2`3h)6!tTdPa000DMK}|sb0I`n?{9y$E00QVq zL_t(|+HI3fOcOyA$KP(d?ebL$B?f^Ym4Kl|L?VKLh$2QwOq6Jpgs3s#iG-*ZO$-M; z5f2{V>dBLe9|t2uLNSUT2WZ8F7FrM?5rLL&X}fg0+jhpcq$CP`$!~Wv^WQh~-hYMx z4F9^1d-L+uy$B_pTxM>uxFLjm^D}Q9)6Peuwy`;s$OwfLq4BugTAR)GF8@eFSN*wG zV@_Emqd!o~^W{em8lr&<0pHq`7X!7ilnH8U4)++V>bje1ERcZ%q8x{CSO$se7+qO? zR{DywWWsKD;FN&GcP3zkh;pOAY5_4uU{fIwRuT{=1QIbB7*-1f^=BKi4TeV4nh<7Z z0xe`_mkJ{8kBNaoDTJbG5&|+Nq>KV-`DRA1(-osO4_Vt;3M&T#{#B8}FF^sG0HmvB zL=q5^32-b6N;m{kG|J(yTA^lF3BCo_h<{xGOAZf_zFx@HvEY{yzz_x`(8J2a&|+8= zX>#S=PPCkAuJFwNl*YSz=wJp%=K?W0BgCjnAau&JNF5G)?C!(9FMBfdCs3Z2$_f2s zd)xB&iY)4`Gt%d|+4MzT9vw90kY@!2M2=Hx8|bEvM%_rE7OGLt|7YU|13WSDzJ|(c zm5d3SU^Pa-stG_I7iDHb94)zek+J5F!QRrs(lbF{`S7A--yjg&wv;6-@EosUAYG3g z)WO!9*Fc8awnQQ>O*@ugav7l74A!pW%|oY+{S|t!Z0|(#GzuRNt!|oiOa2N;BFy^{ z@22ZFk=<*ch>{%iwzj!`^bgEdnfP+3Gede&$-ZUF^_H(=BR2d$sKrO9Q-b@CouS13 z+0fTn;PFt-e^YEXPu3OtZubbE#zq&DJs|%cO>6)ZA%(~+vcAJDwJYZ2%4#?f7n4Gy mHb+Wzaa79@WAa;6b$O{gd!R^Wb0000YeRy3=G!B)-7ShX(a P^}Yg~^Ch|ciVW5Oz=%=n literal 0 HcmV?d00001 diff --git a/src/static/img/mxgraph/warning.png b/src/static/img/mxgraph/warning.png new file mode 100644 index 0000000000000000000000000000000000000000..2f7878964ce8fcad001eaf06e3a60c751b781437 GIT binary patch literal 425 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Ea{HEjtmUzPnffIy#(?lOI#yL zg7ec#$`gxH85~pclTsBta}(23gHjVyDhp4h+AuIMT6?-UhE&{2`t$$4J+mrhyQQQZvOZ9GPi((1usJ;i`U=(|6(827jAr7AH&ALBH+jT?k=-;RE&T@EHsr`XJ3l_ZRadVU3uuDXt!61fV zv#dyijEqbIPf~I*tB0ZkL*fpHz(U!uZVQWd%&`X>75M}lH1r(4S#RP_VC;IwH9^6F zVdH~MvBAMj2Tywoo|!gf55G!$LoB0-z)?m9MwW8MsuPD7@wWsfy9fhSGk{co-o5l` zX&b-%g~N}m1hNnP1S&IQo&8oJCUz|+FEitsbqs|JOdXu@Cmu5iggeY(6L9ePA+l{t zO!|+1f7vfI@mpL4dQ|y@WKT~IkN0%0Kl{txGEdCAkdl$Rm+u<4KmvpO^52Wr-Yf=% OHG`+CpUXO@geCy=#hxkv literal 0 HcmV?d00001 diff --git a/src/static/img/mxgraph/window-title.gif b/src/static/img/mxgraph/window-title.gif new file mode 100644 index 0000000000000000000000000000000000000000..231def8bb5a44af3708c65e26d9d4196c00e8c94 GIT binary patch literal 275 zcmZ?wbhEHb6lV};IKsei{rdG&r%pY5`0(7ha|aF_IC${j`Sa%=J$m%y$&-s0FW$a= z`^AeF@87?_fB*i^pFeNjym{@~wa1Si_w@8!zI^$?g9mr--hKA$*_ktE7;pf^pDbV* z9S{k!lYw=Df}+sTjA>6at#V%PGdO*wB=`R3Mel3gzn-?Bz(Yr>|HP7@jtr~$7cz{U z)<&(5Sd~z1k+c7S!3-z8+2=EglF#PtzW=(b#&7-Yr(JvM&cA;y-_Y39+|t0%-qG3B z-7e7AKVjmeKH;g;rq7r;m2=MAdGi;{VOz|+blLJ1E5ui?S-WoiYQ{~Qw`|?ENoeP; M-Fx=#RAjIQ0Glw5tpET3 literal 0 HcmV?d00001 diff --git a/src/static/img/mxgraph/window.gif b/src/static/img/mxgraph/window.gif new file mode 100644 index 0000000000000000000000000000000000000000..6631c4f5f20e1090fbbbfce38c873e26f6259c0c GIT binary patch literal 75 zcmZ?wbhEHblwuHJXkcJ?^XAQm4<8hNvM_*v4u}BBFfb|g^shXWdKBJATc%|L3L*!GB7YTATcvKH8(mnFd!>1F)%REhL}eH000McNliru z)CUI$9xQ?e60`sS010qNS#tmY3h)2`3h)6!tTdPa000DMK}|sb0I`n?{9y$E00Oc} zL_t(|+Le(@OcPNQhR<}S4_hb|Nhu)3YEY??xR8fX#GnyPAR%?3#>59=f{6>`$`H41 zTp4$|a77GJ2{EXPrmX>yCP}(x>w9`&IQwkkVwF^WN^iRIz-kbmY_nvbJ zK!roJbl_$G-K3bgF3l^{b5SNnyJrU{zs)?b+U(!al0rn-LZr*-q--|Z8{N^yuKKpK z8ihVasElTXVt;M5T8MN+=-`(X6nfKJ1xP-A7`e5xy7si`NEMLag67^v(4^;pl&J`9 z$L-RlhK76SJBTQ@3}BM0*Gdl=A((Li&C;MGWuT-|V9>gOkWf%sf5udxR-4ef3n4ZK zP>@%!kL5<8=2;``pk!zfq3J~p2%$6#bPhpEXCUMoJG7uIECK%WD^c*dq|I$+(mnQm zIFVjo#+tQ10z6)CJP{3lEs8jea#Es;C3GTiVKHcV9Hi$aQuiM|(I{v8%Dci7Wv`dl zr{WoiZVVxEr~ zAUBk(RuP8d5*{`dpXYgD9{1CWKJR#^=Uu+OTw4XTm7p!t7+TqY(Y`25hSnjHeF?dt zo$Qce2#xp=AD`ftp8H|yc*L*OtK~+hGk|jUo(iAw%BfMi#fBahwo$e{xg6lYkV)Q` zd~{Ou1{v|zyzKT{%gRm5X68S%k5_n k$NvOmg%1HuZp|V5PkvP&tL&dn*8l(j07*qoM6N<$g1W4BhX4Qo literal 0 HcmV?d00001 diff --git a/src/static/img/mxgraph/zoom_in32.png b/src/static/img/mxgraph/zoom_in32.png new file mode 100644 index 0000000000000000000000000000000000000000..438ff0ff02b2011d8cb3c34b86a28570ea850fb8 GIT binary patch literal 2184 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 literal 0 HcmV?d00001 diff --git a/src/static/js/mxClient.min.js b/src/static/js/mxClient.min.js new file mode 100644 index 0000000..150a0ac --- /dev/null +++ b/src/static/js/mxClient.min.js @@ -0,0 +1,1808 @@ +var mxClient={VERSION:"3.9.6",IS_IE:0<=navigator.userAgent.indexOf("MSIE"),IS_IE6:0<=navigator.userAgent.indexOf("MSIE 6"),IS_IE11:!!navigator.userAgent.match(/Trident\/7\./),IS_EDGE:!!navigator.userAgent.match(/Edge\//),IS_QUIRKS:0<=navigator.userAgent.indexOf("MSIE")&&(null==document.documentMode||5==document.documentMode),IS_EM:"spellcheck"in document.createElement("textarea")&&8==document.documentMode,VML_PREFIX:"v",OFFICE_PREFIX:"o",IS_NS:0<=navigator.userAgent.indexOf("Mozilla/")&&0>navigator.userAgent.indexOf("MSIE")&& +0>navigator.userAgent.indexOf("Edge/"),IS_OP:0<=navigator.userAgent.indexOf("Opera/")||0<=navigator.userAgent.indexOf("OPR/"),IS_OT:0<=navigator.userAgent.indexOf("Presto/")&&0>navigator.userAgent.indexOf("Presto/2.4.")&&0>navigator.userAgent.indexOf("Presto/2.3.")&&0>navigator.userAgent.indexOf("Presto/2.2.")&&0>navigator.userAgent.indexOf("Presto/2.1.")&&0>navigator.userAgent.indexOf("Presto/2.0.")&&0>navigator.userAgent.indexOf("Presto/1."),IS_SF:0<=navigator.userAgent.indexOf("AppleWebKit/")&& +0>navigator.userAgent.indexOf("Chrome/")&&0>navigator.userAgent.indexOf("Edge/"),IS_IOS:navigator.userAgent.match(/(iPad|iPhone|iPod)/g)?!0:!1,IS_GC:0<=navigator.userAgent.indexOf("Chrome/")&&0>navigator.userAgent.indexOf("Edge/"),IS_CHROMEAPP:null!=window.chrome&&null!=chrome.app&&null!=chrome.app.runtime,IS_FF:0<=navigator.userAgent.indexOf("Firefox/"),IS_MT:0<=navigator.userAgent.indexOf("Firefox/")&&0>navigator.userAgent.indexOf("Firefox/1.")&&0>navigator.userAgent.indexOf("Firefox/2.")||0<=navigator.userAgent.indexOf("Iceweasel/")&& +0>navigator.userAgent.indexOf("Iceweasel/1.")&&0>navigator.userAgent.indexOf("Iceweasel/2.")||0<=navigator.userAgent.indexOf("SeaMonkey/")&&0>navigator.userAgent.indexOf("SeaMonkey/1.")||0<=navigator.userAgent.indexOf("Iceape/")&&0>navigator.userAgent.indexOf("Iceape/1."),IS_SVG:0<=navigator.userAgent.indexOf("Firefox/")||0<=navigator.userAgent.indexOf("Iceweasel/")||0<=navigator.userAgent.indexOf("Seamonkey/")||0<=navigator.userAgent.indexOf("Iceape/")||0<=navigator.userAgent.indexOf("Galeon/")|| +0<=navigator.userAgent.indexOf("Epiphany/")||0<=navigator.userAgent.indexOf("AppleWebKit/")||0<=navigator.userAgent.indexOf("Gecko/")||0<=navigator.userAgent.indexOf("Opera/")||null!=document.documentMode&&9<=document.documentMode,NO_FO:!document.createElementNS||"[object SVGForeignObjectElement]"!=document.createElementNS("http://www.w3.org/2000/svg","foreignObject")||0<=navigator.userAgent.indexOf("Opera/"),IS_VML:"MICROSOFT INTERNET EXPLORER"==navigator.appName.toUpperCase(),IS_WIN:0document.location.href.indexOf("http://")&&0>document.location.href.indexOf("https://"),defaultBundles:[],isBrowserSupported:function(){return mxClient.IS_VML||mxClient.IS_SVG},link:function(a,b,c){c=c||document;if(mxClient.IS_IE6)c.write('');else{var d= +c.createElement("link");d.setAttribute("rel",a);d.setAttribute("href",b);d.setAttribute("charset","UTF-8");d.setAttribute("type","text/css");c.getElementsByTagName("head")[0].appendChild(d)}},loadResources:function(a,b){function c(){0==--d&&a()}for(var d=mxClient.defaultBundles.length,e=0;e\x3c/script>')},dispose:function(){for(var a=0;ad&&g?(d++,window.setTimeout(e,f)):null!=c&&c()},f=30;e()},cascadeOpacity:function(a,b,c){for(var d=a.model.getChildCount(b),e=0;edocument.documentMode)?function(a){return null!=a?a.currentStyle:null}:function(a){return null!=a?window.getComputedStyle(a, +""):null}}(),parseCssNumber:function(a){"thin"==a?a="2":"medium"==a?a="4":"thick"==a&&(a="6");a=parseFloat(a);isNaN(a)&&(a=0);return a},setPrefixedStyle:function(){var a=null;mxClient.IS_OT?a="O":mxClient.IS_SF||mxClient.IS_GC?a="Webkit":mxClient.IS_MT?a="Moz":mxClient.IS_IE&&9<=document.documentMode&&10>document.documentMode&&(a="ms");return function(b,c,d){b[c]=d;null!=a&&0document.documentMode))switch(b.nodeType){case 1:var d=a.createElement(b.nodeName);if(b.attributes&&0\n");null!=e;)d.push(mxUtils.getPrettyXml(e,b,c+b)),e=e.nextSibling;d.push(c+"\n")}else d.push("/>\n")}return d.join("")},removeWhitespace:function(a,b){for(var c=b?a.previousSibling:a.nextSibling;null!=c&&c.nodeType==mxConstants.NODETYPE_TEXT;){var d= +b?c.previousSibling:c.nextSibling,e=mxUtils.getTextContent(c);0==mxUtils.trim(e).length&&c.parentNode.removeChild(c);c=d}},htmlEntities:function(a,b){a=String(a||"");a=a.replace(/&/g,"&");a=a.replace(/"/g,""");a=a.replace(/\'/g,"'");a=a.replace(//g,">");if(null==b||b)a=a.replace(/\n/g," ");return a},isVml:function(a){return null!=a&&"urn:schemas-microsoft-com:vml"==a.tagUrn},getXml:function(a,b){var c="";null!=window.XMLSerializer?c=(new XMLSerializer).serializeToString(a): +null!=a.xml&&(c=a.xml.replace(/\r\n\t[\t]*/g,"").replace(/>\r\n/g,">").replace(/\r\n/g,"\n"));return c=c.replace(/\n/g,b||" ")},extractTextWithWhitespace:function(a){function b(a){if(1!=a.length||"BR"!=a[0].nodeName&&"\n"!=a[0].innerHTML)for(var e=0;e"==g.innerHTML.toLowerCase()?d.push("\n"):(3===g.nodeType||4===g.nodeType?0"):(b.push(">"),b.push(a.innerHTML),b.push(""));return b.join("")}return""}:function(a){return null!=a?(new XMLSerializer).serializeToString(a):""}}(),write:function(a,b){var c=a.ownerDocument.createTextNode(b);null!=a&&a.appendChild(c);return c},writeln:function(a,b){var c=a.ownerDocument.createTextNode(b);null!=a&&(a.appendChild(c),a.appendChild(document.createElement("br")));return c},br:function(a,b){b=b||1;for(var c= +null,d=0;dk&&(a.style.left=Math.max(e,k-c)+"px");b=parseInt(a.offsetTop);c=parseInt(a.offsetHeight);f=d+Math.max(f.clientHeight||0,g.clientHeight);b+c>f&&(a.style.top=Math.max(d,f-c)+"px")},load:function(a){a=new mxXmlRequest(a,null,"GET",!1);a.send();return a},get:function(a,b,c,d,e,f){a=new mxXmlRequest(a,null,"GET");null!=d&&a.setBinary(d);a.send(b,c,e,f);return a}, +getAll:function(a,b,c){for(var d=a.length,e=[],f=0,g=function(){0==f&&null!=c&&c();f++},k=0;kf||299mxUtils.indexOf(b,e))&&(d[e]=c||"object"!=typeof a[e]?a[e]:mxUtils.clone(a[e]))}return d}, +equalPoints:function(a,b){if(null==a&&null!=b||null!=a&&null==b||null!=a&&null!=b&&a.length!=b.length)return!1;if(null!=a&&null!=b)for(var c=0;c [Function]\n";else if("object"==typeof a[c])var d=mxUtils.getFunctionName(a[c].constructor),b=b+(c+" => ["+d+"]\n");else b+= +c+" = "+a[c]+"\n"}catch(e){b+=c+"="+e.message}return b},toRadians:function(a){return Math.PI*a/180},toDegree:function(a){return 180*a/Math.PI},arcToCurves:function(a,b,c,d,e,f,g,k,l){k-=a;l-=b;if(0===c||0===d)return q;c=Math.abs(c);d=Math.abs(d);var m=-k/2,n=-l/2,p=Math.cos(e*Math.PI/180),q=Math.sin(e*Math.PI/180);e=p*m+q*n;var m=-1*q*m+p*n,n=e*e,r=m*m,t=c*c,u=d*d,x=n/t+r/u;1e&&(e+=2*Math.PI);g=2*e/Math.PI;g=Math.ceil(0>g?-1*g:g);e/=g;m=8/3*Math.sin(e/4)*Math.sin(e/4)/Math.sin(e/2);n=p*c;p*=d;c*=q;d*=q;for(var y=Math.cos(f),A=Math.sin(f),r=-m*(n*A+d*y),t=-m*(c*A-p*y),q=[],z=0;zc&&(a=3,-135>=c&&(a=2));if(0<=d.indexOf(mxConstants.DIRECTION_NORTH))switch(a){case 0:b|=mxConstants.DIRECTION_MASK_NORTH;break;case 1:b|=mxConstants.DIRECTION_MASK_EAST;break;case 2:b|=mxConstants.DIRECTION_MASK_SOUTH; +break;case 3:b|=mxConstants.DIRECTION_MASK_WEST}if(0<=d.indexOf(mxConstants.DIRECTION_WEST))switch(a){case 0:b|=mxConstants.DIRECTION_MASK_WEST;break;case 1:b|=mxConstants.DIRECTION_MASK_NORTH;break;case 2:b|=mxConstants.DIRECTION_MASK_EAST;break;case 3:b|=mxConstants.DIRECTION_MASK_SOUTH}if(0<=d.indexOf(mxConstants.DIRECTION_SOUTH))switch(a){case 0:b|=mxConstants.DIRECTION_MASK_SOUTH;break;case 1:b|=mxConstants.DIRECTION_MASK_WEST;break;case 2:b|=mxConstants.DIRECTION_MASK_NORTH;break;case 3:b|= +mxConstants.DIRECTION_MASK_EAST}if(0<=d.indexOf(mxConstants.DIRECTION_EAST))switch(a){case 0:b|=mxConstants.DIRECTION_MASK_EAST;break;case 1:b|=mxConstants.DIRECTION_MASK_SOUTH;break;case 2:b|=mxConstants.DIRECTION_MASK_WEST;break;case 3:b|=mxConstants.DIRECTION_MASK_NORTH}return b},reversePortConstraints:function(a){var b;b=(a&mxConstants.DIRECTION_MASK_WEST)<<3;b|=(a&mxConstants.DIRECTION_MASK_NORTH)<<1;b|=(a&mxConstants.DIRECTION_MASK_SOUTH)>>1;return b|=(a&mxConstants.DIRECTION_MASK_EAST)>>3}, +findNearestSegment:function(a,b,c){var d=-1;if(0f.distSq)&&(d=f)}}return null!=d?d.p:null},rectangleIntersectsSegment:function(a,b,c){var d=a.y,e=a.x,f=d+a.height,g=e+a.width;a=b.x;var k=c.x;b.x>c.x&&(a=c.x,k=b.x);k>g&&(k=g);ak)return!1;var e=b.y,g=c.y,l=c.x-b.x;1E-7g&&(b=g,g=e,e=b);g>f&&(g=f);eg?!1:!0},contains:function(a,b,c){return a.x<= +b&&a.x+a.width>=b&&a.y<=c&&a.y+a.height>=c},intersects:function(a,b){var c=a.width,d=a.height,e=b.width,f=b.height;if(0>=e||0>=f||0>=c||0>=d)return!1;var g=a.x,k=a.y,l=b.x,m=b.y,e=e+l,f=f+m,c=c+g,d=d+k;return(eg)&&(fk)&&(cl)&&(dm)},intersectsHotspot:function(a,b,c,d,e,f){d=null!=d?d:1;e=null!=e?e:0;f=null!=f?f:0;if(0a.toLowerCase().indexOf("0x"))},isInteger:function(a){return String(parseInt(a))===String(a)},mod:function(a,b){return(a%b+b)%b},intersection:function(a,b,c,d,e,f,g,k){var l=(k-f)*(c-a)-(g-e)*(d-b);g=((g-e)*(b-f)-(k-f)*(a-e))/l;e=((c-a)*(b-f)-(d-b)*(a-e))/l;return 0<=g&&1>=g&&0<=e&&1>=e?new mxPoint(a+g*(c-a),b+g*(d-b)):null},ptSegDistSq:function(a,b,c,d,e,f){c-=a;d-=b;e-=a;f-=b;0>=e*c+f*d?c=0:(e=c-e,f=d- +f,a=e*c+f*d,c=0>=a?0:a*a/(c*c+d*d));e=e*e+f*f-c;0>e&&(e=0);return e},ptLineDist:function(a,b,c,d,e,f){return Math.abs((d-b)*e-(c-a)*f+c*b-d*a)/Math.sqrt((d-b)*(d-b)+(c-a)*(c-a))},relativeCcw:function(a,b,c,d,e,f){c-=a;d-=b;e-=a;f-=b;a=e*d-f*c;0==a&&(a=e*c+f*d,0a&&(a=0)));return 0>a?-1:0document.documentMode)?a.style.filter=100<=b?"":"alpha(opacity="+b+")":a.style.opacity=b/100},createImage:function(a){var b;mxClient.IS_IE6&&"CSS1Compat"!=document.compatMode?(b=document.createElement(mxClient.VML_PREFIX+":image"),b.setAttribute("src",a),b.style.borderStyle="none"):(b=document.createElement("img"), +b.setAttribute("src",a),b.setAttribute("border","0"));return b},sortCells:function(a,b){b=null!=b?b:!0;var c=new mxDictionary;a.sort(function(a,e){var d=c.get(a);null==d&&(d=mxCellPath.create(a).split(mxCellPath.PATH_SEPARATOR),c.put(a,d));var g=c.get(e);null==g&&(g=mxCellPath.create(e).split(mxCellPath.PATH_SEPARATOR),c.put(e,g));d=mxCellPath.compare(d,g);return 0==d?0:0a.indexOf("="))?a:""},getStylenames:function(a){var b= +[];if(null!=a){a=a.split(";");for(var c=0;ca[c].indexOf("=")&&b.push(a[c])}return b},indexOfStylename:function(a,b){if(null!=a&&null!=b)for(var c=a.split(";"),d=0,e=0;emxUtils.indexOfStylename(a,b)&&(null==a?a="":0e?";":a.substring(e)):0>e||e==a.length-1?"":a.substring(e+1)}else{var f=a.indexOf(";"+b+"=");0>f?d&&(d=";"==a.charAt(a.length-1)?"":";",a=a+d+b+"="+c+";"):(e=a.indexOf(";",f+1),a=d?a.substring(0,f+1)+b+"="+c+(0>e?";":a.substring(e)):a.substring(0,f)+(0>e?";":a.substring(e)))}return a},setCellStyleFlags:function(a,b,c,d,e){if(null!=b&&0e)e=";"==a.charAt(a.length-1)?"":";",a=d||null==d?a+e+b+"="+c:a+e+b+"=0";else{var f=a.indexOf(";",e),g;g=0>f?a.substring(e+b.length+1):a.substring(e+b.length+1,f);g=null==d?parseInt(g)^c:d?parseInt(g)|c:parseInt(g)&~c;a=a.substring(0,e)+b+"="+g+(0<=f?a.substring(f):"")}}return a},getAlignmentAsPoint:function(a,b){var c=0,d=0;a==mxConstants.ALIGN_CENTER? +c=-.5:a==mxConstants.ALIGN_RIGHT&&(c=-1);b==mxConstants.ALIGN_MIDDLE?d=-.5:b==mxConstants.ALIGN_BOTTOM&&(d=-1);return new mxPoint(c,d)},getSizeForString:function(a,b,c,d){b=null!=b?b:mxConstants.DEFAULT_FONTSIZE;c=null!=c?c:mxConstants.DEFAULT_FONTFAMILY;var e=document.createElement("div");e.style.fontFamily=c;e.style.fontSize=Math.round(b)+"px";e.style.lineHeight=Math.round(b*mxConstants.LINE_HEIGHT)+"px";e.style.position="absolute";e.style.visibility="hidden";e.style.display=mxClient.IS_QUIRKS? +"inline":"inline-block";e.style.zoom="1";null!=d?(e.style.width=d+"px",e.style.whiteSpace="normal"):e.style.whiteSpace="nowrap";e.innerHTML=a;document.body.appendChild(e);a=new mxRectangle(0,0,e.offsetWidth,e.offsetHeight);document.body.removeChild(e);return a},getViewXml:function(a,b,c,d,e){d=null!=d?d:0;e=null!=e?e:0;b=null!=b?b:1;null==c&&(c=[a.getModel().getRoot()]);var f=a.getView(),g=null,k=f.isEventsEnabled();f.setEventsEnabled(!1);var l=f.drawPane,m=f.overlayPane;a.dialect==mxConstants.DIALECT_SVG? +(f.drawPane=document.createElementNS(mxConstants.NS_SVG,"g"),f.canvas.appendChild(f.drawPane),f.overlayPane=document.createElementNS(mxConstants.NS_SVG,"g")):(f.drawPane=f.drawPane.cloneNode(!1),f.canvas.appendChild(f.drawPane),f.overlayPane=f.overlayPane.cloneNode(!1));f.canvas.appendChild(f.overlayPane);var n=f.getTranslate();f.translate=new mxPoint(d,e);b=new mxTemporaryCellStates(a.getView(),b,c);try{g=(new mxCodec).encode(a.getView())}finally{b.destroy(),f.translate=n,f.canvas.removeChild(f.drawPane), +f.canvas.removeChild(f.overlayPane),f.drawPane=l,f.overlayPane=m,f.setEventsEnabled(k)}return g},getScaleForPageCount:function(a,b,c,d){if(1>a)return 1;c=null!=c?c:mxConstants.PAGE_FORMAT_A4_PORTRAIT;d=null!=d?d:0;var e=c.width-2*d;c=c.height-2*d;d=b.getGraphBounds().clone();b=b.getView().getScale();d.width/=b;d.height/=b;b=d.width;var f=Math.sqrt(a);d=Math.sqrt(b/d.height/(e/c));c=f*d;d=f/d;if(1>c&&d>a){var g=d/a;d=a;c/=g}1>d&&c>a&&(g=c/a,c=a,d/=g);g=Math.ceil(c)*Math.ceil(d);for(f=0;g>a;){var g= +Math.floor(c)/c,k=Math.floor(d)/d;1==g&&(g=Math.floor(c-1)/c);1==k&&(k=Math.floor(d-1)/d);g=g>k?g:k;c*=g;d*=g;g=Math.ceil(c)*Math.ceil(d);f++;if(10";g=document.getElementsByTagName("base");for(c=0;c";for(c=0;c'+('
')+ +a.container.innerHTML;b.writeln(d+"
");b.close()}else{b.writeln("");g=document.getElementsByTagName("base");for(c=0;c');b.close();c=b.createElement("div"); +c.position="absolute";c.overflow="hidden";c.style.width=e+"px";c.style.height=f+"px";e=b.createElement("div");e.style.position="absolute";e.style.left=k+"px";e.style.top=l+"px";f=a.container.firstChild;for(d=null;null!=f;)g=f.cloneNode(!0),f==a.view.drawPane.ownerSVGElement?(c.appendChild(g),d=g):e.appendChild(g),f=f.nextSibling;b.body.appendChild(c);null!=e.firstChild&&b.body.appendChild(e);null!=d&&(d.style.minWidth="",d.style.minHeight="",d.firstChild.setAttribute("transform","translate("+k+","+ +l+")"))}mxUtils.removeCursors(b.body);return b},printScreen:function(a){var b=window.open();a.getGraphBounds();mxUtils.show(a,b.document);a=function(){b.focus();b.print();b.close()};mxClient.IS_GC?b.setTimeout(a,500):a()},popup:function(a,b){if(b){var c=document.createElement("div");c.style.overflow="scroll";c.style.width="636px";c.style.height="460px";var d=document.createElement("pre");d.innerHTML=mxUtils.htmlEntities(a,!1).replace(/\n/g,"
").replace(/ /g," ");c.appendChild(d);c=new mxWindow("Popup Window", +c,document.body.clientWidth/2-320,Math.max(document.body.clientHeight||0,document.documentElement.clientHeight)/2-240,640,480,!1,!0);c.setClosable(!0);c.setVisible(!0)}else mxClient.IS_NS?(c=window.open(),c.document.writeln("
"+mxUtils.htmlEntities(a)+"").replace(/ /g," "),c.document.body.appendChild(d))},alert:function(a){alert(a)},prompt:function(a,b){return prompt(a,
+null!=b?b:"")},confirm:function(a){return confirm(a)},error:function(a,b,c,d){var e=document.createElement("div");e.style.padding="20px";var f=document.createElement("img");f.setAttribute("src",d||mxUtils.errorImage);f.setAttribute("valign","bottom");f.style.verticalAlign="middle";e.appendChild(f);e.appendChild(document.createTextNode(" "));e.appendChild(document.createTextNode(" "));e.appendChild(document.createTextNode(" "));mxUtils.write(e,a);a=document.body.clientWidth;d=document.body.clientHeight||
+document.documentElement.clientHeight;var g=new mxWindow(mxResources.get(mxUtils.errorResource)||mxUtils.errorResource,e,(a-b)/2,d/4,b,null,!1,!0);c&&(mxUtils.br(e),b=document.createElement("p"),c=document.createElement("button"),mxClient.IS_IE?c.style.cssText="float:right":c.setAttribute("style","float:right"),mxEvent.addListener(c,"click",function(a){g.destroy()}),mxUtils.write(c,mxResources.get(mxUtils.closeResource)||mxUtils.closeResource),b.appendChild(c),e.appendChild(b),mxUtils.br(e),g.setClosable(!0));
+g.setVisible(!0);return g},makeDraggable:function(a,b,c,d,e,f,g,k,l,m){a=new mxDragSource(a,c);a.dragOffset=new mxPoint(null!=e?e:0,null!=f?f:mxConstants.TOOLTIP_VERTICAL_OFFSET);a.autoscroll=g;a.setGuidesEnabled(!1);null!=l&&(a.highlightDropTargets=l);null!=m&&(a.getDropTarget=m);a.getGraphForEvent=function(a){return"function"==typeof b?b(a):b};null!=d&&(a.createDragElement=function(){return d.cloneNode(!0)},k&&(a.createPreviewElement=function(a){var b=d.cloneNode(!0),c=parseInt(b.style.width),e=
+parseInt(b.style.height);b.style.width=Math.round(c*a.view.scale)+"px";b.style.height=Math.round(e*a.view.scale)+"px";return b}));return a}},mxConstants={DEFAULT_HOTSPOT:.3,MIN_HOTSPOT_SIZE:8,MAX_HOTSPOT_SIZE:0,RENDERING_HINT_EXACT:"exact",RENDERING_HINT_FASTER:"faster",RENDERING_HINT_FASTEST:"fastest",DIALECT_SVG:"svg",DIALECT_VML:"vml",DIALECT_MIXEDHTML:"mixedHtml",DIALECT_PREFERHTML:"preferHtml",DIALECT_STRICTHTML:"strictHtml",NS_SVG:"http://www.w3.org/2000/svg",NS_XHTML:"http://www.w3.org/1999/xhtml",
+NS_XLINK:"http://www.w3.org/1999/xlink",SHADOWCOLOR:"gray",VML_SHADOWCOLOR:"gray",SHADOW_OFFSET_X:2,SHADOW_OFFSET_Y:3,SHADOW_OPACITY:1,NODETYPE_ELEMENT:1,NODETYPE_ATTRIBUTE:2,NODETYPE_TEXT:3,NODETYPE_CDATA:4,NODETYPE_ENTITY_REFERENCE:5,NODETYPE_ENTITY:6,NODETYPE_PROCESSING_INSTRUCTION:7,NODETYPE_COMMENT:8,NODETYPE_DOCUMENT:9,NODETYPE_DOCUMENTTYPE:10,NODETYPE_DOCUMENT_FRAGMENT:11,NODETYPE_NOTATION:12,TOOLTIP_VERTICAL_OFFSET:16,DEFAULT_VALID_COLOR:"#00FF00",DEFAULT_INVALID_COLOR:"#FF0000",OUTLINE_HIGHLIGHT_COLOR:"#00FF00",
+OUTLINE_HIGHLIGHT_STROKEWIDTH:5,HIGHLIGHT_STROKEWIDTH:3,HIGHLIGHT_SIZE:2,HIGHLIGHT_OPACITY:100,CURSOR_MOVABLE_VERTEX:"move",CURSOR_MOVABLE_EDGE:"move",CURSOR_LABEL_HANDLE:"default",CURSOR_TERMINAL_HANDLE:"pointer",CURSOR_BEND_HANDLE:"crosshair",CURSOR_VIRTUAL_BEND_HANDLE:"crosshair",CURSOR_CONNECT:"pointer",HIGHLIGHT_COLOR:"#00FF00",CONNECT_TARGET_COLOR:"#0000FF",INVALID_CONNECT_TARGET_COLOR:"#FF0000",DROP_TARGET_COLOR:"#0000FF",VALID_COLOR:"#00FF00",INVALID_COLOR:"#FF0000",EDGE_SELECTION_COLOR:"#00FF00",
+VERTEX_SELECTION_COLOR:"#00FF00",VERTEX_SELECTION_STROKEWIDTH:1,EDGE_SELECTION_STROKEWIDTH:1,VERTEX_SELECTION_DASHED:!0,EDGE_SELECTION_DASHED:!0,GUIDE_COLOR:"#FF0000",GUIDE_STROKEWIDTH:1,OUTLINE_COLOR:"#0099FF",OUTLINE_STROKEWIDTH:mxClient.IS_IE?2:3,HANDLE_SIZE:6,LABEL_HANDLE_SIZE:4,HANDLE_FILLCOLOR:"#00FF00",HANDLE_STROKECOLOR:"black",LABEL_HANDLE_FILLCOLOR:"yellow",CONNECT_HANDLE_FILLCOLOR:"#0000FF",LOCKED_HANDLE_FILLCOLOR:"#FF0000",OUTLINE_HANDLE_FILLCOLOR:"#00FFFF",OUTLINE_HANDLE_STROKECOLOR:"#0033FF",
+DEFAULT_FONTFAMILY:"Arial,Helvetica",DEFAULT_FONTSIZE:11,DEFAULT_TEXT_DIRECTION:"",LINE_HEIGHT:1.2,WORD_WRAP:"normal",ABSOLUTE_LINE_HEIGHT:!1,DEFAULT_FONTSTYLE:0,DEFAULT_STARTSIZE:40,DEFAULT_MARKERSIZE:6,DEFAULT_IMAGESIZE:24,ENTITY_SEGMENT:30,RECTANGLE_ROUNDING_FACTOR:.15,LINE_ARCSIZE:20,ARROW_SPACING:0,ARROW_WIDTH:30,ARROW_SIZE:30,PAGE_FORMAT_A4_PORTRAIT:new mxRectangle(0,0,827,1169),PAGE_FORMAT_A4_LANDSCAPE:new mxRectangle(0,0,1169,827),PAGE_FORMAT_LETTER_PORTRAIT:new mxRectangle(0,0,850,1100),
+PAGE_FORMAT_LETTER_LANDSCAPE:new mxRectangle(0,0,1100,850),NONE:"none",STYLE_PERIMETER:"perimeter",STYLE_SOURCE_PORT:"sourcePort",STYLE_TARGET_PORT:"targetPort",STYLE_PORT_CONSTRAINT:"portConstraint",STYLE_PORT_CONSTRAINT_ROTATION:"portConstraintRotation",STYLE_SOURCE_PORT_CONSTRAINT:"sourcePortConstraint",STYLE_TARGET_PORT_CONSTRAINT:"targetPortConstraint",STYLE_OPACITY:"opacity",STYLE_FILL_OPACITY:"fillOpacity",STYLE_STROKE_OPACITY:"strokeOpacity",STYLE_TEXT_OPACITY:"textOpacity",STYLE_TEXT_DIRECTION:"textDirection",
+STYLE_OVERFLOW:"overflow",STYLE_ORTHOGONAL:"orthogonal",STYLE_EXIT_X:"exitX",STYLE_EXIT_Y:"exitY",STYLE_EXIT_PERIMETER:"exitPerimeter",STYLE_ENTRY_X:"entryX",STYLE_ENTRY_Y:"entryY",STYLE_ENTRY_PERIMETER:"entryPerimeter",STYLE_WHITE_SPACE:"whiteSpace",STYLE_ROTATION:"rotation",STYLE_FILLCOLOR:"fillColor",STYLE_POINTER_EVENTS:"pointerEvents",STYLE_SWIMLANE_FILLCOLOR:"swimlaneFillColor",STYLE_MARGIN:"margin",STYLE_GRADIENTCOLOR:"gradientColor",STYLE_GRADIENT_DIRECTION:"gradientDirection",STYLE_STROKECOLOR:"strokeColor",
+STYLE_SEPARATORCOLOR:"separatorColor",STYLE_STROKEWIDTH:"strokeWidth",STYLE_ALIGN:"align",STYLE_VERTICAL_ALIGN:"verticalAlign",STYLE_LABEL_WIDTH:"labelWidth",STYLE_LABEL_POSITION:"labelPosition",STYLE_VERTICAL_LABEL_POSITION:"verticalLabelPosition",STYLE_IMAGE_ASPECT:"imageAspect",STYLE_IMAGE_ALIGN:"imageAlign",STYLE_IMAGE_VERTICAL_ALIGN:"imageVerticalAlign",STYLE_GLASS:"glass",STYLE_IMAGE:"image",STYLE_IMAGE_WIDTH:"imageWidth",STYLE_IMAGE_HEIGHT:"imageHeight",STYLE_IMAGE_BACKGROUND:"imageBackground",
+STYLE_IMAGE_BORDER:"imageBorder",STYLE_FLIPH:"flipH",STYLE_FLIPV:"flipV",STYLE_NOLABEL:"noLabel",STYLE_NOEDGESTYLE:"noEdgeStyle",STYLE_LABEL_BACKGROUNDCOLOR:"labelBackgroundColor",STYLE_LABEL_BORDERCOLOR:"labelBorderColor",STYLE_LABEL_PADDING:"labelPadding",STYLE_INDICATOR_SHAPE:"indicatorShape",STYLE_INDICATOR_IMAGE:"indicatorImage",STYLE_INDICATOR_COLOR:"indicatorColor",STYLE_INDICATOR_STROKECOLOR:"indicatorStrokeColor",STYLE_INDICATOR_GRADIENTCOLOR:"indicatorGradientColor",STYLE_INDICATOR_SPACING:"indicatorSpacing",
+STYLE_INDICATOR_WIDTH:"indicatorWidth",STYLE_INDICATOR_HEIGHT:"indicatorHeight",STYLE_INDICATOR_DIRECTION:"indicatorDirection",STYLE_SHADOW:"shadow",STYLE_SEGMENT:"segment",STYLE_ENDARROW:"endArrow",STYLE_STARTARROW:"startArrow",STYLE_ENDSIZE:"endSize",STYLE_STARTSIZE:"startSize",STYLE_SWIMLANE_LINE:"swimlaneLine",STYLE_ENDFILL:"endFill",STYLE_STARTFILL:"startFill",STYLE_DASHED:"dashed",STYLE_DASH_PATTERN:"dashPattern",STYLE_FIX_DASH:"fixDash",STYLE_ROUNDED:"rounded",STYLE_CURVED:"curved",STYLE_ARCSIZE:"arcSize",
+STYLE_ABSOLUTE_ARCSIZE:"absoluteArcSize",STYLE_SOURCE_PERIMETER_SPACING:"sourcePerimeterSpacing",STYLE_TARGET_PERIMETER_SPACING:"targetPerimeterSpacing",STYLE_PERIMETER_SPACING:"perimeterSpacing",STYLE_SPACING:"spacing",STYLE_SPACING_TOP:"spacingTop",STYLE_SPACING_LEFT:"spacingLeft",STYLE_SPACING_BOTTOM:"spacingBottom",STYLE_SPACING_RIGHT:"spacingRight",STYLE_HORIZONTAL:"horizontal",STYLE_DIRECTION:"direction",STYLE_ELBOW:"elbow",STYLE_FONTCOLOR:"fontColor",STYLE_FONTFAMILY:"fontFamily",STYLE_FONTSIZE:"fontSize",
+STYLE_FONTSTYLE:"fontStyle",STYLE_ASPECT:"aspect",STYLE_AUTOSIZE:"autosize",STYLE_FOLDABLE:"foldable",STYLE_EDITABLE:"editable",STYLE_BENDABLE:"bendable",STYLE_MOVABLE:"movable",STYLE_RESIZABLE:"resizable",STYLE_RESIZE_WIDTH:"resizeWidth",STYLE_RESIZE_HEIGHT:"resizeHeight",STYLE_ROTATABLE:"rotatable",STYLE_CLONEABLE:"cloneable",STYLE_DELETABLE:"deletable",STYLE_SHAPE:"shape",STYLE_EDGE:"edgeStyle",STYLE_JETTY_SIZE:"jettySize",STYLE_SOURCE_JETTY_SIZE:"sourceJettySize",STYLE_TARGET_JETTY_SIZE:"targetJettySize",
+STYLE_LOOP:"loopStyle",STYLE_ORTHOGONAL_LOOP:"orthogonalLoop",STYLE_ROUTING_CENTER_X:"routingCenterX",STYLE_ROUTING_CENTER_Y:"routingCenterY",FONT_BOLD:1,FONT_ITALIC:2,FONT_UNDERLINE:4,SHAPE_RECTANGLE:"rectangle",SHAPE_ELLIPSE:"ellipse",SHAPE_DOUBLE_ELLIPSE:"doubleEllipse",SHAPE_RHOMBUS:"rhombus",SHAPE_LINE:"line",SHAPE_IMAGE:"image",SHAPE_ARROW:"arrow",SHAPE_ARROW_CONNECTOR:"arrowConnector",SHAPE_LABEL:"label",SHAPE_CYLINDER:"cylinder",SHAPE_SWIMLANE:"swimlane",SHAPE_CONNECTOR:"connector",SHAPE_ACTOR:"actor",
+SHAPE_CLOUD:"cloud",SHAPE_TRIANGLE:"triangle",SHAPE_HEXAGON:"hexagon",ARROW_CLASSIC:"classic",ARROW_CLASSIC_THIN:"classicThin",ARROW_BLOCK:"block",ARROW_BLOCK_THIN:"blockThin",ARROW_OPEN:"open",ARROW_OPEN_THIN:"openThin",ARROW_OVAL:"oval",ARROW_DIAMOND:"diamond",ARROW_DIAMOND_THIN:"diamondThin",ALIGN_LEFT:"left",ALIGN_CENTER:"center",ALIGN_RIGHT:"right",ALIGN_TOP:"top",ALIGN_MIDDLE:"middle",ALIGN_BOTTOM:"bottom",DIRECTION_NORTH:"north",DIRECTION_SOUTH:"south",DIRECTION_EAST:"east",DIRECTION_WEST:"west",
+TEXT_DIRECTION_DEFAULT:"",TEXT_DIRECTION_AUTO:"auto",TEXT_DIRECTION_LTR:"ltr",TEXT_DIRECTION_RTL:"rtl",DIRECTION_MASK_NONE:0,DIRECTION_MASK_WEST:1,DIRECTION_MASK_NORTH:2,DIRECTION_MASK_SOUTH:4,DIRECTION_MASK_EAST:8,DIRECTION_MASK_ALL:15,ELBOW_VERTICAL:"vertical",ELBOW_HORIZONTAL:"horizontal",EDGESTYLE_ELBOW:"elbowEdgeStyle",EDGESTYLE_ENTITY_RELATION:"entityRelationEdgeStyle",EDGESTYLE_LOOP:"loopEdgeStyle",EDGESTYLE_SIDETOSIDE:"sideToSideEdgeStyle",EDGESTYLE_TOPTOBOTTOM:"topToBottomEdgeStyle",EDGESTYLE_ORTHOGONAL:"orthogonalEdgeStyle",
+EDGESTYLE_SEGMENT:"segmentEdgeStyle",PERIMETER_ELLIPSE:"ellipsePerimeter",PERIMETER_RECTANGLE:"rectanglePerimeter",PERIMETER_RHOMBUS:"rhombusPerimeter",PERIMETER_HEXAGON:"hexagonPerimeter",PERIMETER_TRIANGLE:"trianglePerimeter"};function mxEventObject(a){this.name=a;this.properties=[];for(var b=1;bnavigator.userAgent.indexOf("Presto/2.5")&&(this.contentWrapper.style.overflow=a?"auto":"hidden")};
+mxWindow.prototype.activate=function(){if(mxWindow.activeWindow!=this){var a=mxUtils.getCurrentStyle(this.getElement()),a=null!=a?a.zIndex:3;if(mxWindow.activeWindow){var b=mxWindow.activeWindow.getElement();null!=b&&null!=b.style&&(b.style.zIndex=a)}b=mxWindow.activeWindow;this.getElement().style.zIndex=parseInt(a)+1;mxWindow.activeWindow=this;this.fireEvent(new mxEventObject(mxEvent.ACTIVATE,"previousWindow",b))}};mxWindow.prototype.getElement=function(){return this.div};
+mxWindow.prototype.fit=function(){mxUtils.fit(this.div)};mxWindow.prototype.isResizable=function(){return null!=this.resize?"none"!=this.resize.style.display:!1};
+mxWindow.prototype.setResizable=function(a){if(a)if(null==this.resize){this.resize=document.createElement("img");this.resize.style.position="absolute";this.resize.style.bottom="2px";this.resize.style.right="2px";this.resize.setAttribute("src",this.resizeImage);this.resize.style.cursor="nw-resize";var b=null,c=null,d=null,e=null;a=mxUtils.bind(this,function(a){this.activate();b=mxEvent.getClientX(a);c=mxEvent.getClientY(a);d=this.div.offsetWidth;e=this.div.offsetHeight;mxEvent.addGestureListeners(document,
+null,f,g);this.fireEvent(new mxEventObject(mxEvent.RESIZE_START,"event",a));mxEvent.consume(a)});var f=mxUtils.bind(this,function(a){if(null!=b&&null!=c){var f=mxEvent.getClientX(a)-b,g=mxEvent.getClientY(a)-c;this.setSize(d+f,e+g);this.fireEvent(new mxEventObject(mxEvent.RESIZE,"event",a));mxEvent.consume(a)}}),g=mxUtils.bind(this,function(a){null!=b&&null!=c&&(c=b=null,mxEvent.removeGestureListeners(document,null,f,g),this.fireEvent(new mxEventObject(mxEvent.RESIZE_END,"event",a)),mxEvent.consume(a))});
+mxEvent.addGestureListeners(this.resize,a,f,g);this.div.appendChild(this.resize)}else this.resize.style.display="inline";else null!=this.resize&&(this.resize.style.display="none")};
+mxWindow.prototype.setSize=function(a,b){a=Math.max(this.minimumSize.width,a);b=Math.max(this.minimumSize.height,b);mxClient.IS_QUIRKS||(this.div.style.width=a+"px",this.div.style.height=b+"px");this.table.style.width=a+"px";this.table.style.height=b+"px";mxClient.IS_QUIRKS||(this.contentWrapper.style.height=this.div.offsetHeight-this.title.offsetHeight-this.contentHeightCorrection+"px")};mxWindow.prototype.setMinimizable=function(a){this.minimize.style.display=a?"":"none"};
+mxWindow.prototype.getMinimumSize=function(){return new mxRectangle(0,0,0,this.title.offsetHeight)};
+mxWindow.prototype.installMinimizeHandler=function(){this.minimize=document.createElement("img");this.minimize.setAttribute("src",this.minimizeImage);this.minimize.setAttribute("title","Minimize");this.minimize.style.cursor="pointer";this.minimize.style.marginLeft="2px";this.minimize.style.display="none";this.buttons.appendChild(this.minimize);var a=!1,b=null,c=null,d=mxUtils.bind(this,function(d){this.activate();if(a)a=!1,this.minimize.setAttribute("src",this.minimizeImage),this.minimize.setAttribute("title",
+"Minimize"),this.contentWrapper.style.display="",this.maximize.style.display=b,mxClient.IS_QUIRKS||(this.div.style.height=c),this.table.style.height=c,null!=this.resize&&(this.resize.style.visibility=""),this.fireEvent(new mxEventObject(mxEvent.NORMALIZE,"event",d));else{a=!0;this.minimize.setAttribute("src",this.normalizeImage);this.minimize.setAttribute("title","Normalize");this.contentWrapper.style.display="none";b=this.maximize.style.display;this.maximize.style.display="none";c=this.table.style.height;
+var e=this.getMinimumSize();0=e.x-f.x&&d>=e.y-f.y&&c<=e.x-f.x+a.container.offsetWidth&&d<=e.y-f.y+a.container.offsetHeight};
+mxDragSource.prototype.mouseMove=function(a){var b=this.getGraphForEvent(a);null==b||this.graphContainsEvent(b,a)||(b=null);b!=this.currentGraph&&(null!=this.currentGraph&&this.dragExit(this.currentGraph,a),this.currentGraph=b,null!=this.currentGraph&&this.dragEnter(this.currentGraph,a));null!=this.currentGraph&&this.dragOver(this.currentGraph,a);if(null==this.dragElement||null!=this.previewElement&&"visible"==this.previewElement.style.visibility)null!=this.dragElement&&(this.dragElement.style.visibility=
+"hidden");else{var b=mxEvent.getClientX(a),c=mxEvent.getClientY(a);null==this.dragElement.parentNode&&document.body.appendChild(this.dragElement);this.dragElement.style.visibility="visible";null!=this.dragOffset&&(b+=this.dragOffset.x,c+=this.dragOffset.y);var d=mxUtils.getDocumentScrollOrigin(document);this.dragElement.style.left=b+d.x+"px";this.dragElement.style.top=c+d.y+"px"}mxEvent.consume(a)};
+mxDragSource.prototype.mouseUp=function(a){if(null!=this.currentGraph){if(null!=this.currentPoint&&(null==this.previewElement||"hidden"!=this.previewElement.style.visibility)){var b=this.currentGraph.view.scale,c=this.currentGraph.view.translate;this.drop(this.currentGraph,a,this.currentDropTarget,this.currentPoint.x/b-c.x,this.currentPoint.y/b-c.y)}this.dragExit(this.currentGraph);this.currentGraph=null}this.stopDrag();this.removeListeners();mxEvent.consume(a)};
+mxDragSource.prototype.removeListeners=function(){null!=this.eventSource&&(mxEvent.removeGestureListeners(this.eventSource,null,this.mouseMoveHandler,this.mouseUpHandler),this.eventSource=null);mxEvent.removeGestureListeners(document,null,this.mouseMoveHandler,this.mouseUpHandler);this.mouseUpHandler=this.mouseMoveHandler=null};
+mxDragSource.prototype.dragEnter=function(a,b){a.isMouseDown=!0;a.isMouseTrigger=mxEvent.isMouseEvent(b);this.previewElement=this.createPreviewElement(a);null!=this.previewElement&&this.checkEventSource&&mxClient.IS_SVG&&(this.previewElement.style.pointerEvents="none");this.isGuidesEnabled()&&null!=this.previewElement&&(this.currentGuide=new mxGuide(a,a.graphHandler.getGuideStates()));this.highlightDropTargets&&(this.currentHighlight=new mxCellHighlight(a,mxConstants.DROP_TARGET_COLOR));a.addListener(mxEvent.FIRE_MOUSE_EVENT,
+this.eventConsumer)};mxDragSource.prototype.dragExit=function(a,b){this.currentPoint=this.currentDropTarget=null;a.isMouseDown=!1;a.removeListener(this.eventConsumer);null!=this.previewElement&&(null!=this.previewElement.parentNode&&this.previewElement.parentNode.removeChild(this.previewElement),this.previewElement=null);null!=this.currentGuide&&(this.currentGuide.destroy(),this.currentGuide=null);null!=this.currentHighlight&&(this.currentHighlight.destroy(),this.currentHighlight=null)};
+mxDragSource.prototype.dragOver=function(a,b){var c=mxUtils.getOffset(a.container),d=mxUtils.getScrollOrigin(a.container),e=mxEvent.getClientX(b)-c.x+d.x-a.panDx,c=mxEvent.getClientY(b)-c.y+d.y-a.panDy;a.autoScroll&&(null==this.autoscroll||this.autoscroll)&&a.scrollPointToVisible(e,c,a.autoExtend);null!=this.currentHighlight&&a.isDropEnabled()&&(this.currentDropTarget=this.getDropTarget(a,e,c,b),d=a.getView().getState(this.currentDropTarget),this.currentHighlight.highlight(d));if(null!=this.previewElement){null==
+this.previewElement.parentNode&&(a.container.appendChild(this.previewElement),this.previewElement.style.zIndex="3",this.previewElement.style.position="absolute");var d=this.isGridEnabled()&&a.isGridEnabledEvent(b),f=!0;if(null!=this.currentGuide&&this.currentGuide.isEnabledForEvent(b))var f=parseInt(this.previewElement.style.width),g=parseInt(this.previewElement.style.height),f=new mxRectangle(0,0,f,g),c=new mxPoint(e,c),c=this.currentGuide.move(f,c,d),f=!1,e=c.x,c=c.y;else if(d)var d=a.view.scale,
+g=a.view.translate,k=a.gridSize/2,e=(a.snap(e/d-g.x-k)+g.x)*d,c=(a.snap(c/d-g.y-k)+g.y)*d;null!=this.currentGuide&&f&&this.currentGuide.hide();null!=this.previewOffset&&(e+=this.previewOffset.x,c+=this.previewOffset.y);this.previewElement.style.left=Math.round(e)+"px";this.previewElement.style.top=Math.round(c)+"px";this.previewElement.style.visibility="visible"}this.currentPoint=new mxPoint(e,c)};
+mxDragSource.prototype.drop=function(a,b,c,d,e){this.dropHandler.apply(this,arguments);"hidden"!=a.container.style.visibility&&a.container.focus()};function mxToolbar(a){this.container=a}mxToolbar.prototype=new mxEventSource;mxToolbar.prototype.constructor=mxToolbar;mxToolbar.prototype.container=null;mxToolbar.prototype.enabled=!0;mxToolbar.prototype.noReset=!1;mxToolbar.prototype.updateDefaultMode=!0;
+mxToolbar.prototype.addItem=function(a,b,c,d,e,f){var g=document.createElement(null!=b?"img":"button"),k=e||(null!=f?"mxToolbarMode":"mxToolbarItem");g.className=k;g.setAttribute("src",b);null!=a&&(null!=b?g.setAttribute("title",a):mxUtils.write(g,a));this.container.appendChild(g);null!=c&&(mxEvent.addListener(g,"click",c),mxClient.IS_TOUCH&&mxEvent.addListener(g,"touchend",c));a=mxUtils.bind(this,function(a){null!=d?g.setAttribute("src",b):g.style.backgroundColor=""});mxEvent.addGestureListeners(g,
+mxUtils.bind(this,function(a){null!=d?g.setAttribute("src",d):g.style.backgroundColor="gray";if(null!=f){null==this.menu&&(this.menu=new mxPopupMenu,this.menu.init());var b=this.currentImg;this.menu.isMenuShowing()&&this.menu.hideMenu();b!=g&&(this.currentImg=g,this.menu.factoryMethod=f,b=new mxPoint(g.offsetLeft,g.offsetTop+g.offsetHeight),this.menu.popup(b.x,b.y,null,a),this.menu.isMenuShowing()&&(g.className=k+"Selected",this.menu.hideMenu=function(){mxPopupMenu.prototype.hideMenu.apply(this);
+g.className=k;this.currentImg=null}))}}),null,a);mxEvent.addListener(g,"mouseout",a);return g};mxToolbar.prototype.addCombo=function(a){var b=document.createElement("div");b.style.display="inline";b.className="mxToolbarComboContainer";var c=document.createElement("select");c.className=a||"mxToolbarCombo";b.appendChild(c);this.container.appendChild(b);return c};
+mxToolbar.prototype.addActionCombo=function(a,b){var c=document.createElement("select");c.className=b||"mxToolbarCombo";this.addOption(c,a,null);mxEvent.addListener(c,"change",function(a){var b=c.options[c.selectedIndex];c.selectedIndex=0;null!=b.funct&&b.funct(a)});this.container.appendChild(c);return c};mxToolbar.prototype.addOption=function(a,b,c){var d=document.createElement("option");mxUtils.writeln(d,b);"function"==typeof c?d.funct=c:d.setAttribute("value",c);a.appendChild(d);return d};
+mxToolbar.prototype.addSwitchMode=function(a,b,c,d,e){var f=document.createElement("img");f.initialClassName=e||"mxToolbarMode";f.className=f.initialClassName;f.setAttribute("src",b);f.altIcon=d;null!=a&&f.setAttribute("title",a);mxEvent.addListener(f,"click",mxUtils.bind(this,function(a){a=this.selectedMode.altIcon;null!=a?(this.selectedMode.altIcon=this.selectedMode.getAttribute("src"),this.selectedMode.setAttribute("src",a)):this.selectedMode.className=this.selectedMode.initialClassName;this.updateDefaultMode&&
+(this.defaultMode=f);this.selectedMode=f;a=f.altIcon;null!=a?(f.altIcon=f.getAttribute("src"),f.setAttribute("src",a)):f.className=f.initialClassName+"Selected";this.fireEvent(new mxEventObject(mxEvent.SELECT));c()}));this.container.appendChild(f);null==this.defaultMode&&(this.defaultMode=f,this.selectMode(f),c());return f};
+mxToolbar.prototype.addMode=function(a,b,c,d,e,f){f=null!=f?f:!0;var g=document.createElement(null!=b?"img":"button");g.initialClassName=e||"mxToolbarMode";g.className=g.initialClassName;g.setAttribute("src",b);g.altIcon=d;null!=a&&g.setAttribute("title",a);this.enabled&&f&&(mxEvent.addListener(g,"click",mxUtils.bind(this,function(a){this.selectMode(g,c);this.noReset=!1})),mxEvent.addListener(g,"dblclick",mxUtils.bind(this,function(a){this.selectMode(g,c);this.noReset=!0})),null==this.defaultMode&&
+(this.defaultMode=g,this.defaultFunction=c,this.selectMode(g,c)));this.container.appendChild(g);return g};
+mxToolbar.prototype.selectMode=function(a,b){if(this.selectedMode!=a){if(null!=this.selectedMode){var c=this.selectedMode.altIcon;null!=c?(this.selectedMode.altIcon=this.selectedMode.getAttribute("src"),this.selectedMode.setAttribute("src",c)):this.selectedMode.className=this.selectedMode.initialClassName}this.selectedMode=a;c=this.selectedMode.altIcon;null!=c?(this.selectedMode.altIcon=this.selectedMode.getAttribute("src"),this.selectedMode.setAttribute("src",c)):this.selectedMode.className=this.selectedMode.initialClassName+
+"Selected";this.fireEvent(new mxEventObject(mxEvent.SELECT,"function",b))}};mxToolbar.prototype.resetMode=function(a){!a&&this.noReset||this.selectedMode==this.defaultMode||this.selectMode(this.defaultMode,this.defaultFunction)};mxToolbar.prototype.addSeparator=function(a){return this.addItem(null,a,null)};mxToolbar.prototype.addBreak=function(){mxUtils.br(this.container)};
+mxToolbar.prototype.addLine=function(){var a=document.createElement("hr");a.style.marginRight="6px";a.setAttribute("size","1");this.container.appendChild(a)};mxToolbar.prototype.destroy=function(){mxEvent.release(this.container);this.selectedMode=this.defaultFunction=this.defaultMode=this.container=null;null!=this.menu&&this.menu.destroy()};function mxUndoableEdit(a,b){this.source=a;this.changes=[];this.significant=null!=b?b:!0}mxUndoableEdit.prototype.source=null;
+mxUndoableEdit.prototype.changes=null;mxUndoableEdit.prototype.significant=null;mxUndoableEdit.prototype.undone=!1;mxUndoableEdit.prototype.redone=!1;mxUndoableEdit.prototype.isEmpty=function(){return 0==this.changes.length};mxUndoableEdit.prototype.isSignificant=function(){return this.significant};mxUndoableEdit.prototype.add=function(a){this.changes.push(a)};mxUndoableEdit.prototype.notify=function(){};mxUndoableEdit.prototype.die=function(){};
+mxUndoableEdit.prototype.undo=function(){if(!this.undone){this.source.fireEvent(new mxEventObject(mxEvent.START_EDIT));for(var a=this.changes.length-1;0<=a;a--){var b=this.changes[a];null!=b.execute?b.execute():null!=b.undo&&b.undo();this.source.fireEvent(new mxEventObject(mxEvent.EXECUTED,"change",b))}this.undone=!0;this.redone=!1;this.source.fireEvent(new mxEventObject(mxEvent.END_EDIT))}this.notify()};
+mxUndoableEdit.prototype.redo=function(){if(!this.redone){this.source.fireEvent(new mxEventObject(mxEvent.START_EDIT));for(var a=this.changes.length,b=0;bthis.indexOfNextAdd)for(var a=this.history.splice(this.indexOfNextAdd,this.history.length-this.indexOfNextAdd),b=0;bthis.dx&&Math.abs(this.dx)<
+this.border?this.border+this.dx:this.handleMouseOut?Math.max(this.dx,0):0;0==this.dx&&(this.dx=c-g.scrollLeft,this.dx=0this.dy&&Math.abs(this.dy)e.x+(document.body.clientWidth||f.clientWidth)&&(b.div.style.left=Math.max(0,a.div.offsetLeft-d+(mxClient.IS_IE?6:-6))+"px");mxUtils.fit(b.div)}};
+mxPopupMenu.prototype.addSeparator=function(a,b){a=a||this;if(this.smartSeparators&&!b)a.willAddSeparator=!0;else if(null!=a.tbody){a.willAddSeparator=!1;var c=document.createElement("tr"),d=document.createElement("td");d.className="mxPopupMenuIcon";d.style.padding="0 0 0 0px";c.appendChild(d);d=document.createElement("td");d.style.padding="0 0 0 0px";d.setAttribute("colSpan","2");var e=document.createElement("hr");e.setAttribute("size","1");d.appendChild(e);c.appendChild(d);a.tbody.appendChild(c)}};
+mxPopupMenu.prototype.popup=function(a,b,c,d){if(null!=this.div&&null!=this.tbody&&null!=this.factoryMethod){this.div.style.left=a+"px";for(this.div.style.top=b+"px";null!=this.tbody.firstChild;)mxEvent.release(this.tbody.firstChild),this.tbody.removeChild(this.tbody.firstChild);this.itemCount=0;this.factoryMethod(this,c,d);0this.autoSaveDelay||this.ignoredChanges>=this.autoSaveThreshold&&a>this.autoSaveThrottle?(this.save(),this.reset()):this.ignoredChanges++};mxAutoSaveManager.prototype.reset=function(){this.lastSnapshot=(new Date).getTime();this.ignoredChanges=0};mxAutoSaveManager.prototype.destroy=function(){this.setGraph(null)};
+function mxAnimation(a){this.delay=null!=a?a:20}mxAnimation.prototype=new mxEventSource;mxAnimation.prototype.constructor=mxAnimation;mxAnimation.prototype.delay=null;mxAnimation.prototype.thread=null;mxAnimation.prototype.isRunning=function(){return null!=this.thread};mxAnimation.prototype.startAnimation=function(){null==this.thread&&(this.thread=window.setInterval(mxUtils.bind(this,this.updateAnimation),this.delay))};mxAnimation.prototype.updateAnimation=function(){this.fireEvent(new mxEventObject(mxEvent.EXECUTE))};
+mxAnimation.prototype.stopAnimation=function(){null!=this.thread&&(window.clearInterval(this.thread),this.thread=null,this.fireEvent(new mxEventObject(mxEvent.DONE)))};function mxMorphing(a,b,c,d){mxAnimation.call(this,d);this.graph=a;this.steps=null!=b?b:6;this.ease=null!=c?c:1.5}mxMorphing.prototype=new mxAnimation;mxMorphing.prototype.constructor=mxMorphing;mxMorphing.prototype.graph=null;mxMorphing.prototype.steps=null;mxMorphing.prototype.step=0;mxMorphing.prototype.ease=null;
+mxMorphing.prototype.cells=null;mxMorphing.prototype.updateAnimation=function(){mxAnimation.prototype.updateAnimation.apply(this,arguments);var a=new mxCellStatePreview(this.graph);if(null!=this.cells)for(var b=0;b=this.steps)&&this.stopAnimation()};mxMorphing.prototype.show=function(a){a.show()};
+mxMorphing.prototype.animateCell=function(a,b,c){var d=this.graph.getView().getState(a),e=null;if(null!=d&&(e=this.getDelta(d),this.graph.getModel().isVertex(a)&&(0!=e.x||0!=e.y))){var f=this.graph.view.getTranslate(),g=this.graph.view.getScale();e.x+=f.x*g;e.y+=f.y*g;b.moveState(d,-e.x/this.ease,-e.y/this.ease)}if(c&&!this.stopRecursion(d,e))for(d=this.graph.getModel().getChildCount(a),e=0;ec?";stop-opacity:"+c:"";e=this.createElement("stop");
+e.setAttribute("offset","0%");e.setAttribute("style","stop-color:"+a+c);f.appendChild(e);c=1>d?";stop-opacity:"+d:"";e=this.createElement("stop");e.setAttribute("offset","100%");e.setAttribute("style","stop-color:"+b+c);f.appendChild(e);return f};
+mxSvgCanvas2D.prototype.addNode=function(a,b){var c=this.node,d=this.state;if(null!=c){if("path"==c.nodeName)if(null!=this.path&&0a.alpha||1>a.fillAlpha)&&this.node.setAttribute("fill-opacity",a.alpha*a.fillAlpha);if(null!=a.fillColor)if(null!=a.gradientColor)if(a=this.getSvgGradient(a.fillColor,a.gradientColor,a.gradientFillAlpha,a.gradientAlpha,a.gradientDirection),mxClient.IS_CHROME_APP||mxClient.IS_IE||mxClient.IS_IE11||mxClient.IS_EDGE||this.root.ownerDocument!=document)this.node.setAttribute("fill","url(#"+a+")");else{var b=this.getBaseUrl().replace(/([\(\)])/g,
+"\\$1");this.node.setAttribute("fill","url("+b+"#"+a+")")}else this.node.setAttribute("fill",a.fillColor.toLowerCase())};mxSvgCanvas2D.prototype.getCurrentStrokeWidth=function(){return Math.max(this.minStrokeWidth,Math.max(.01,this.format(this.state.strokeWidth*this.state.scale)))};
+mxSvgCanvas2D.prototype.updateStroke=function(){var a=this.state;this.node.setAttribute("stroke",a.strokeColor.toLowerCase());(1>a.alpha||1>a.strokeAlpha)&&this.node.setAttribute("stroke-opacity",a.alpha*a.strokeAlpha);var b=this.getCurrentStrokeWidth();1!=b&&this.node.setAttribute("stroke-width",b);"path"==this.node.nodeName&&this.updateStrokeAttributes();a.dashed&&this.node.setAttribute("stroke-dasharray",this.createDashPattern((a.fixDash?1:a.strokeWidth)*a.scale))};
+mxSvgCanvas2D.prototype.updateStrokeAttributes=function(){var a=this.state;null!=a.lineJoin&&"miter"!=a.lineJoin&&this.node.setAttribute("stroke-linejoin",a.lineJoin);if(null!=a.lineCap){var b=a.lineCap;"flat"==b&&(b="butt");"butt"!=b&&this.node.setAttribute("stroke-linecap",b)}null==a.miterLimit||this.styleEnabled&&10==a.miterLimit||this.node.setAttribute("stroke-miterlimit",a.miterLimit)};
+mxSvgCanvas2D.prototype.createDashPattern=function(a){var b=[];if("string"===typeof this.state.dashPattern){var c=this.state.dashPattern.split(" ");if(0l.alpha||1>l.fillAlpha)&&m.setAttribute("opacity",l.alpha*l.fillAlpha);e=this.state.transform||"";if(g||k){var n=f=1,p=0,q=0;g&&(f=-1,p=-c-2*a);k&&(n=-1,q=-d-2*b);e+="scale("+f+","+n+")translate("+p*l.scale+","+q*l.scale+")"}0",5)+1)),""==a.substring(a.length-7,a.length)&&(a=a.substring(0,a.length-7)))}else{if(null!=document.implementation&&null!=document.implementation.createDocument){var b=document.implementation.createDocument("http://www.w3.org/1999/xhtml","html",null),c=b.createElement("body");
+b.documentElement.appendChild(c);var d=document.createElement("div");d.innerHTML=a;for(a=d.firstChild;null!=a;)d=a.nextSibling,c.appendChild(b.adoptNode(a)),a=d;return c.innerHTML}b=document.createElement("textarea");b.innerHTML=a.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(//g,">");a=b.value.replace(/&/g,"&").replace(/&lt;/g,"<").replace(/&gt;/g,">").replace(/&amp;/g,
+"&").replace(/
/g,"
").replace(/
/g,"
").replace(/(]+)>/gm,"$1 />")}return a}; +mxSvgCanvas2D.prototype.createDiv=function(a,b,c,d,e){c=this.state;d="display:inline-block;font-size:"+c.fontSize+"px;font-family:"+c.fontFamily+";color:"+c.fontColor+";line-height:"+(mxConstants.ABSOLUTE_LINE_HEIGHT?c.fontSize*mxConstants.LINE_HEIGHT+"px":mxConstants.LINE_HEIGHT*this.lineHeightCorrection)+";"+d;(c.fontStyle&mxConstants.FONT_BOLD)==mxConstants.FONT_BOLD&&(d+="font-weight:bold;");(c.fontStyle&mxConstants.FONT_ITALIC)==mxConstants.FONT_ITALIC&&(d+="font-style:italic;");(c.fontStyle& +mxConstants.FONT_UNDERLINE)==mxConstants.FONT_UNDERLINE&&(d+="text-decoration:underline;");b==mxConstants.ALIGN_CENTER?d+="text-align:center;":b==mxConstants.ALIGN_RIGHT&&(d+="text-align:right;");b="";null!=c.fontBackgroundColor&&(b+="background-color:"+c.fontBackgroundColor+";");null!=c.fontBorderColor&&(b+="border:1px solid "+c.fontBorderColor+";");mxUtils.isNode(a)||(a=this.convertHtml(a),"fill"!=e&&"width"!=e?a='
'+a+"
":d+=b);if(!mxClient.IS_IE&&document.createElementNS)return e=document.createElementNS("http://www.w3.org/1999/xhtml","div"),e.setAttribute("style",d),mxUtils.isNode(a)?this.root.ownerDocument!=document?e.appendChild(a.cloneNode(!0)):e.appendChild(a):e.innerHTML=a,e;mxUtils.isNode(a)&&this.root.ownerDocument!=document&&(a=a.outerHTML);return mxUtils.parseXml('
'+a+"
").documentElement}; +mxSvgCanvas2D.prototype.invalidateCachedOffsetSize=function(a){delete a.firstChild.mxCachedOffsetWidth;delete a.firstChild.mxCachedFinalOffsetWidth;delete a.firstChild.mxCachedFinalOffsetHeight}; +mxSvgCanvas2D.prototype.updateText=function(a,b,c,d,e,f,g,k,l,m,n){if(null!=n&&null!=n.firstChild&&null!=n.firstChild.firstChild&&null!=n.firstChild.firstChild.firstChild){n=n.firstChild;var p=n.firstChild,q=p.firstChild;m=null!=m?m:0;var r=this.state;a+=r.dx;b+=r.dy;l?(q.style.maxHeight=Math.round(d)+"px",q.style.maxWidth=Math.round(c)+"px"):"fill"==k?(q.style.width=Math.round(c+1)+"px",q.style.height=Math.round(d+1)+"px"):"width"==k&&(q.style.width=Math.round(c+1)+"px",0r.alpha&&u.setAttribute("opacity",r.alpha);var x=this.createElement("foreignObject");x.setAttribute("style","overflow:visible;");x.setAttribute("pointer-events","all");t=this.createDiv(e,f,g,t,m);if(null!=t){null!=q&&t.setAttribute("dir",q);u.appendChild(x);this.root.appendChild(u);var y,A;q=y=2;if(!mxClient.IS_IE||9!=document.documentMode&&mxClient.IS_SVG){this.root.ownerDocument!= +document?(t.style.visibility="hidden",document.body.appendChild(t)):x.appendChild(t);var z=t;null!=z.firstChild&&"DIV"==z.firstChild.nodeName&&(z=z.firstChild,k&&"break-word"==t.style.wordWrap&&(z.style.width="100%"));v=z.offsetWidth;0==v&&t.parentNode==x&&(t.style.visibility="hidden",document.body.appendChild(t),v=z.offsetWidth);this.cacheOffsetSize&&(u.mxCachedOffsetWidth=v);!n&&k&&0r.alpha&&u.setAttribute("opacity",r.alpha);q=t=0;f==mxConstants.ALIGN_CENTER?t-=c/2:f==mxConstants.ALIGN_RIGHT&&(t-=c);a+=t;g==mxConstants.ALIGN_MIDDLE?q-= +d/2:g==mxConstants.ALIGN_BOTTOM&&(q-=d);"fill"!=m&&mxClient.IS_FF&&mxClient.IS_WIN&&(q-=2);b+=q;z=1!=r.scale?"scale("+r.scale+")":"";0!=r.rotation&&this.rotateHtml?(z+="rotate("+r.rotation+","+c/2+","+d/2+")",b=this.rotatePoint((a+c/2)*r.scale,(b+d/2)*r.scale,r.rotation,r.rotationCx,r.rotationCy),a=b.x-c*r.scale/2,b=b.y-d*r.scale/2):(a*=r.scale,b*=r.scale);0!=p&&(z+="rotate("+p+","+-t+","+-q+")");u.setAttribute("transform","translate("+(Math.round(a)+this.foOffset)+","+(Math.round(b)+this.foOffset)+ +")"+z);x.setAttribute("width",Math.round(Math.max(1,c)));x.setAttribute("height",Math.round(Math.max(1,d)));this.root.ownerDocument!=document&&(a=this.createAlternateContent(x,a,b,c,d,e,f,g,k,l,m,n,p),null!=a&&(x.setAttribute("requiredFeatures","http://www.w3.org/TR/SVG11/feature#Extensibility"),c=this.createElement("switch"),c.appendChild(x),c.appendChild(a),u.appendChild(c)))}}else this.plainText(a,b,c,d,e,f,g,k,m,n,p,q)}}; +mxSvgCanvas2D.prototype.createClip=function(a,b,c,d){a=Math.round(a);b=Math.round(b);c=Math.round(c);d=Math.round(d);for(var e="mx-clip-"+a+"-"+b+"-"+c+"-"+d,f=0,g=e+"-"+f;null!=document.getElementById(g);)g=e+"-"+ ++f;clip=this.createElement("clipPath");clip.setAttribute("id",g);e=this.createElement("rect");e.setAttribute("x",a);e.setAttribute("y",b);e.setAttribute("width",c);e.setAttribute("height",d);clip.appendChild(e);return clip}; +mxSvgCanvas2D.prototype.plainText=function(a,b,c,d,e,f,g,k,l,m,n,p){n=null!=n?n:0;k=this.state;var q=k.fontSize,r=this.createElement("g"),t=k.transform||"";this.updateFont(r);0!=n&&(t+="rotate("+n+","+this.format(a*k.scale)+","+this.format(b*k.scale)+")");null!=p&&r.setAttribute("direction",p);m&&0k.alpha&&r.setAttribute("opacity",k.alpha);t=e.split("\n");p=Math.round(q*mxConstants.LINE_HEIGHT);var u=q+(t.length-1)*p;n=b+q-1;g==mxConstants.ALIGN_MIDDLE?"fill"==l?n-=d/2:(m=(this.matchHtmlAlignment&&m&&0"),document.body.appendChild(n),e=n.offsetWidth,f=n.offsetHeight,n.parentNode.removeChild(n),g==mxConstants.ALIGN_CENTER?c-=e/2:g==mxConstants.ALIGN_RIGHT&&(c-=e),k==mxConstants.ALIGN_MIDDLE?d-=f/2:k==mxConstants.ALIGN_BOTTOM&&(d-=f),n=new mxRectangle((c+1)*m.scale,(d+2)*m.scale,e*m.scale,(f+1)*m.scale);null!=n&&(b= +this.createElement("rect"),b.setAttribute("fill",m.fontBackgroundColor||"none"),b.setAttribute("stroke",m.fontBorderColor||"none"),b.setAttribute("x",Math.floor(n.x-1)),b.setAttribute("y",Math.floor(n.y-1)),b.setAttribute("width",Math.ceil(n.width+2)),b.setAttribute("height",Math.ceil(n.height)),m=null!=m.fontBorderColor?Math.max(1,this.format(m.scale)):0,b.setAttribute("stroke-width",m),this.root.ownerDocument==document&&1==mxUtils.mod(m,2)&&b.setAttribute("transform","translate(0.5, 0.5)"),a.insertBefore(b, +a.firstChild))}};mxSvgCanvas2D.prototype.stroke=function(){this.addNode(!1,!0)};mxSvgCanvas2D.prototype.fill=function(){this.addNode(!0,!1)};mxSvgCanvas2D.prototype.fillAndStroke=function(){this.addNode(!0,!0)};var mxVmlCanvas2D=function(a){mxAbstractCanvas2D.call(this);this.root=a};mxUtils.extend(mxVmlCanvas2D,mxAbstractCanvas2D);mxVmlCanvas2D.prototype.node=null;mxVmlCanvas2D.prototype.textEnabled=!0;mxVmlCanvas2D.prototype.moveOp="m";mxVmlCanvas2D.prototype.lineOp="l"; +mxVmlCanvas2D.prototype.curveOp="c";mxVmlCanvas2D.prototype.closeOp="x";mxVmlCanvas2D.prototype.rotatedHtmlBackground="";mxVmlCanvas2D.prototype.vmlScale=1;mxVmlCanvas2D.prototype.createElement=function(a){return document.createElement(a)};mxVmlCanvas2D.prototype.createVmlElement=function(a){return this.createElement(mxClient.VML_PREFIX+":"+a)}; +mxVmlCanvas2D.prototype.addNode=function(a,b){var c=this.node,d=this.state;if(null!=c){if("shape"==c.nodeName)if(null!=this.path&&0a.alpha||1>a.fillAlpha)b.opacity=a.alpha*a.fillAlpha*100+"%";return b}; +mxVmlCanvas2D.prototype.createStroke=function(){var a=this.state,b=this.createVmlElement("stroke");b.endcap=a.lineCap||"flat";b.joinstyle=a.lineJoin||"miter";b.miterlimit=a.miterLimit||"10";if(1>a.alpha||1>a.strokeAlpha)b.opacity=a.alpha*a.strokeAlpha*100+"%";a.dashed&&(b.dashstyle=this.getVmlDashStyle());return b};mxVmlCanvas2D.prototype.getVmlDashStyle=function(){var a="dash";if("string"===typeof this.state.dashPattern){var b=this.state.dashPattern.split(" ");0this.state.alpha||1>this.state.fillAlpha)a.style.filter+="alpha(opacity="+this.state.alpha*this.state.fillAlpha*100+")";this.root.appendChild(a)}; +mxVmlCanvas2D.prototype.createDiv=function(a,b,c,d){c=this.createElement("div");var e=this.state,f="";null!=e.fontBackgroundColor&&(f+="background-color:"+e.fontBackgroundColor+";");null!=e.fontBorderColor&&(f+="border:1px solid "+e.fontBorderColor+";");mxUtils.isNode(a)?c.appendChild(a):"fill"!=d&&"width"!=d?(d=this.createElement("div"),d.style.cssText=f,d.style.display=mxClient.IS_QUIRKS?"inline":"inline-block",d.style.zoom="1",d.style.textDecoration="inherit",d.innerHTML=a,c.appendChild(d)):(c.style.cssText= +f,c.innerHTML=a);a=c.style;a.fontSize=e.fontSize/this.vmlScale+"px";a.fontFamily=e.fontFamily;a.color=e.fontColor;a.verticalAlign="top";a.textAlign=b||"left";a.lineHeight=mxConstants.ABSOLUTE_LINE_HEIGHT?e.fontSize*mxConstants.LINE_HEIGHT/this.vmlScale+"px":mxConstants.LINE_HEIGHT;(e.fontStyle&mxConstants.FONT_BOLD)==mxConstants.FONT_BOLD&&(a.fontWeight="bold");(e.fontStyle&mxConstants.FONT_ITALIC)==mxConstants.FONT_ITALIC&&(a.fontStyle="italic");(e.fontStyle&mxConstants.FONT_UNDERLINE)==mxConstants.FONT_UNDERLINE&& +(a.textDecoration="underline");return c}; +mxVmlCanvas2D.prototype.text=function(a,b,c,d,e,f,g,k,l,m,n,p,q){if(this.textEnabled&&null!=e){var r=this.state;if("html"==l){null!=r.rotation&&(b=this.rotatePoint(a,b,r.rotation,r.rotationCx,r.rotationCy),a=b.x,b=b.y);8!=document.documentMode||mxClient.IS_EM?(a*=r.scale,b*=r.scale):(a+=r.dx,b+=r.dy,"fill"!=m&&g==mxConstants.ALIGN_TOP&&--b);l=8!=document.documentMode||mxClient.IS_EM?this.createElement("div"):this.createVmlElement("group");l.style.position="absolute";l.style.display="inline";l.style.left= +this.format(a)+"px";l.style.top=this.format(b)+"px";l.style.zoom=r.scale;var t=this.createElement("div");t.style.position="relative";t.style.display="inline";var u=mxUtils.getAlignmentAsPoint(f,g),x=u.x,u=u.y;e=this.createDiv(e,f,g,m);f=this.createElement("div");null!=q&&e.setAttribute("dir",q);if(k&&0y&&(y+=2*Math.PI);y%=Math.PI;y>Math.PI/2&&(y=Math.PI-y);g=Math.cos(y);var A=Math.sin(y);8!=document.documentMode||mxClient.IS_EM||(e.style.display="inline-block",f.style.display="inline-block",t.style.display="inline-block");e.style.visibility="hidden";e.style.position="absolute";document.body.appendChild(e);t=e;null!=t.firstChild&&"DIV"==t.firstChild.nodeName&&(t=t.firstChild);y=t.offsetWidth+3;t=t.offsetHeight;n?(c=Math.min(c,y),t= +Math.min(t,d)):c=y;k&&(e.style.width=c+"px");mxClient.IS_QUIRKS&&(n||"width"==m)&&t>d&&(t=d,e.style.height=t+"px");d=t;n=(d-d*g+c*-A)/2-q*c*(x+.5)+p*d*(u+.5);k=(c-c*g+d*-A)/2+p*c*(x+.5)+q*d*(u+.5);"group"==l.nodeName&&"DIV"==this.root.nodeName?(m=this.createElement("div"),m.style.display="inline-block",m.style.position="absolute",m.style.left=this.format(a+(k-c/2)*r.scale)+"px",m.style.top=this.format(b+(n-d/2)*r.scale)+"px",l.parentNode.appendChild(m),m.appendChild(l)):(r=8!=document.documentMode|| +mxClient.IS_EM?r.scale:1,l.style.left=this.format(a+(k-c/2)*r)+"px",l.style.top=this.format(b+(n-d/2)*r)+"px");f.style.filter="progid:DXImageTransform.Microsoft.Matrix(M11="+p+", M12="+q+", M21="+-q+", M22="+p+", sizingMethod='auto expand')";f.style.backgroundColor=this.rotatedHtmlBackground;1>this.state.alpha&&(f.style.filter+="alpha(opacity="+100*this.state.alpha+")");f.appendChild(e);e.style.position="";e.style.visibility=""}else 8!=document.documentMode||mxClient.IS_EM?(e.style.verticalAlign= +"top",1>this.state.alpha&&(l.style.filter="alpha(opacity="+100*this.state.alpha+")"),r=e.parentNode,e.style.visibility="hidden",document.body.appendChild(e),c=e.offsetWidth,t=e.offsetHeight,mxClient.IS_QUIRKS&&n&&t>d&&(t=d,e.style.height=t+"px"),d=t,e.style.visibility="",r.appendChild(e),l.style.left=this.format(a+c*x*this.state.scale)+"px",l.style.top=this.format(b+d*u*this.state.scale)+"px"):(1>this.state.alpha&&(e.style.filter="alpha(opacity="+100*this.state.alpha+")"),t.style.left=100*x+"%",t.style.top= +100*u+"%")}else this.plainText(a,b,c,d,mxUtils.htmlEntities(e,!1),f,g,k,l,m,n,p,q)}}; +mxVmlCanvas2D.prototype.plainText=function(a,b,c,d,e,f,g,k,l,m,n,p,q){k=this.state;a=(a+k.dx)*k.scale;b=(b+k.dy)*k.scale;c=this.createVmlElement("shape");c.style.width="1px";c.style.height="1px";c.stroked="false";d=this.createVmlElement("fill");d.color=k.fontColor;d.opacity=100*k.alpha+"%";c.appendChild(d);d=this.createVmlElement("path");d.textpathok="true";d.v="m "+this.format(0)+" "+this.format(0)+" l "+this.format(1)+" "+this.format(0);c.appendChild(d);d=this.createVmlElement("textpath");d.style.cssText= +"v-text-align:"+f;d.style.align=f;d.style.fontFamily=k.fontFamily;d.string=e;d.on="true";f=k.fontSize*k.scale/this.vmlScale;d.style.fontSize=f+"px";(k.fontStyle&mxConstants.FONT_BOLD)==mxConstants.FONT_BOLD&&(d.style.fontWeight="bold");(k.fontStyle&mxConstants.FONT_ITALIC)==mxConstants.FONT_ITALIC&&(d.style.fontStyle="italic");(k.fontStyle&mxConstants.FONT_UNDERLINE)==mxConstants.FONT_UNDERLINE&&(d.style.textDecoration="underline");e=e.split("\n");k=f+(e.length-1)*f*mxConstants.LINE_HEIGHT;f=e=0; +g==mxConstants.ALIGN_BOTTOM?f=-k/2:g!=mxConstants.ALIGN_MIDDLE&&(f=k/2);null!=p&&(c.style.rotation=p,g=Math.PI/180*p,e=Math.sin(g)*f,f*=Math.cos(g));c.appendChild(d);c.style.left=this.format(a-e)+"px";c.style.top=this.format(b+f)+"px";this.root.appendChild(c)};mxVmlCanvas2D.prototype.stroke=function(){this.addNode(!1,!0)};mxVmlCanvas2D.prototype.fill=function(){this.addNode(!0,!1)};mxVmlCanvas2D.prototype.fillAndStroke=function(){this.addNode(!0,!0)}; +function mxGuide(a,b){this.graph=a;this.setStates(b)}mxGuide.prototype.graph=null;mxGuide.prototype.states=null;mxGuide.prototype.horizontal=!0;mxGuide.prototype.vertical=!0;mxGuide.prototype.guideX=null;mxGuide.prototype.guideY=null;mxGuide.prototype.setStates=function(a){this.states=a};mxGuide.prototype.isEnabledForEvent=function(a){return!0};mxGuide.prototype.getGuideTolerance=function(){return this.graph.gridSize/2}; +mxGuide.prototype.createGuideShape=function(a){a=new mxPolyline([],mxConstants.GUIDE_COLOR,mxConstants.GUIDE_STROKEWIDTH);a.isDashed=!0;return a}; +mxGuide.prototype.move=function(a,b,c){if(null!=this.states&&(this.horizontal||this.vertical)&&null!=a&&null!=b){var d=function(b){b+=this.graph.panDy;var c=!1;Math.abs(b-F)this.opacity&&(b+="alpha(opacity="+this.opacity+")");this.isShadow&&(b+="progid:DXImageTransform.Microsoft.dropShadow (OffX='"+Math.round(mxConstants.SHADOW_OFFSET_X*this.scale)+"', OffY='"+Math.round(mxConstants.SHADOW_OFFSET_Y*this.scale)+"', Color='"+mxConstants.VML_SHADOWCOLOR+"')");if(null!=this.fill&&this.fill!=mxConstants.NONE&&this.gradient&&this.gradient!=mxConstants.NONE){var c=this.fill,d=this.gradient,e="0",f={east:0,south:1, +west:2,north:3},g=null!=this.direction?f[this.direction]:0;null!=this.gradientDirection&&(g=mxUtils.mod(g+f[this.gradientDirection]-1,4));1==g?(e="1",f=c,c=d,d=f):2==g?(f=c,c=d,d=f):3==g&&(e="1");b+="progid:DXImageTransform.Microsoft.gradient(startColorStr='"+c+"', endColorStr='"+d+"', gradientType='"+e+"')"}a.style.filter=b}; +mxShape.prototype.updateHtmlColors=function(a){var b=this.stroke;null!=b&&b!=mxConstants.NONE?(a.style.borderColor=b,this.isDashed?a.style.borderStyle="dashed":0mxUtils.indexOf(f,l-1))){var p=Math.sqrt(n*n+m*m);a.lineTo(g.x+n*Math.min(d,p/2)/p,g.y+m*Math.min(d,p/2)/p);for(m=b[mxUtils.mod(l+ +1,b.length)];l
"));var m=!mxUtils.isNode(this.value)&&this.replaceLinefeeds&&"html"==l?m.replace(/\n/g,"
"):m,n=this.textDirection;n!=mxConstants.TEXT_DIRECTION_AUTO||k||(n=this.getAutoDirection());n!=mxConstants.TEXT_DIRECTION_LTR&&n!=mxConstants.TEXT_DIRECTION_RTL&&(n=null); +a.text(d,e,f,c,m,this.align,this.valign,this.wrap,l,this.overflow,this.clipped,this.getTextRotation(),n)}this.lastUnscaledWidth=g}; +mxText.prototype.redraw=function(){if(this.visible&&this.checkBounds()&&this.cacheEnabled&&this.lastValue==this.value&&(mxUtils.isNode(this.value)||this.dialect==mxConstants.DIALECT_STRICTHTML))if("DIV"!=this.node.nodeName||!this.isHtmlAllowed()&&mxClient.IS_VML){var a=this.createCanvas();null!=a&&null!=a.updateText&&null!=a.invalidateCachedOffsetSize?(this.paint(a,!0),this.destroyCanvas(a),this.updateBoundingBox()):mxShape.prototype.redraw.apply(this,arguments)}else this.updateSize(this.node,null== +this.state||null==this.state.view.textDiv),mxClient.IS_IE&&(null==document.documentMode||8>=document.documentMode)?this.updateHtmlFilter():this.updateHtmlTransform(),this.updateBoundingBox();else mxShape.prototype.redraw.apply(this,arguments),mxUtils.isNode(this.value)||this.dialect==mxConstants.DIALECT_STRICTHTML?this.lastValue=this.value:this.lastValue=null}; +mxText.prototype.resetStyles=function(){mxShape.prototype.resetStyles.apply(this,arguments);this.color="black";this.align=mxConstants.ALIGN_CENTER;this.valign=mxConstants.ALIGN_MIDDLE;this.family=mxConstants.DEFAULT_FONTFAMILY;this.size=mxConstants.DEFAULT_FONTSIZE;this.fontStyle=mxConstants.DEFAULT_FONTSTYLE;this.spacingLeft=this.spacingBottom=this.spacingRight=this.spacingTop=this.spacing=2;this.horizontal=!0;delete this.background;delete this.border;this.textDirection=mxConstants.DEFAULT_TEXT_DIRECTION; +delete this.margin}; +mxText.prototype.apply=function(a){var b=this.spacing;mxShape.prototype.apply.apply(this,arguments);null!=this.style&&(this.fontStyle=mxUtils.getValue(this.style,mxConstants.STYLE_FONTSTYLE,this.fontStyle),this.family=mxUtils.getValue(this.style,mxConstants.STYLE_FONTFAMILY,this.family),this.size=mxUtils.getValue(this.style,mxConstants.STYLE_FONTSIZE,this.size),this.color=mxUtils.getValue(this.style,mxConstants.STYLE_FONTCOLOR,this.color),this.align=mxUtils.getValue(this.style,mxConstants.STYLE_ALIGN, +this.align),this.valign=mxUtils.getValue(this.style,mxConstants.STYLE_VERTICAL_ALIGN,this.valign),this.spacing=parseInt(mxUtils.getValue(this.style,mxConstants.STYLE_SPACING,this.spacing)),this.spacingTop=parseInt(mxUtils.getValue(this.style,mxConstants.STYLE_SPACING_TOP,this.spacingTop-b))+this.spacing,this.spacingRight=parseInt(mxUtils.getValue(this.style,mxConstants.STYLE_SPACING_RIGHT,this.spacingRight-b))+this.spacing,this.spacingBottom=parseInt(mxUtils.getValue(this.style,mxConstants.STYLE_SPACING_BOTTOM, +this.spacingBottom-b))+this.spacing,this.spacingLeft=parseInt(mxUtils.getValue(this.style,mxConstants.STYLE_SPACING_LEFT,this.spacingLeft-b))+this.spacing,this.horizontal=mxUtils.getValue(this.style,mxConstants.STYLE_HORIZONTAL,this.horizontal),this.background=mxUtils.getValue(this.style,mxConstants.STYLE_LABEL_BACKGROUNDCOLOR,this.background),this.border=mxUtils.getValue(this.style,mxConstants.STYLE_LABEL_BORDERCOLOR,this.border),this.textDirection=mxUtils.getValue(this.style,mxConstants.STYLE_TEXT_DIRECTION, +mxConstants.DEFAULT_TEXT_DIRECTION),this.opacity=mxUtils.getValue(this.style,mxConstants.STYLE_TEXT_OPACITY,100),this.updateMargin());this.flipH=this.flipV=null};mxText.prototype.getAutoDirection=function(){var a=/[A-Za-z\u05d0-\u065f\u066a-\u06ef\u06fa-\u07ff\ufb1d-\ufdff\ufe70-\ufefc]/.exec(this.value);return null!=a&&0=document.documentMode)?this.updateHtmlFilter():this.updateHtmlTransform()}; +mxText.prototype.updateHtmlTransform=function(){var a=this.getTextRotation(),b=this.node.style,c=this.margin.x,d=this.margin.y;0!=a?(mxUtils.setPrefixedStyle(b,"transformOrigin",100*-c+"% "+100*-d+"%"),mxUtils.setPrefixedStyle(b,"transform","translate("+100*c+"%,"+100*d+"%)scale("+this.scale+") rotate("+a+"deg)")):(mxUtils.setPrefixedStyle(b,"transformOrigin","0% 0%"),mxUtils.setPrefixedStyle(b,"transform","scale("+this.scale+")translate("+100*c+"%,"+100*d+"%)"));b.left=Math.round(this.bounds.x-Math.ceil(c* +("fill"!=this.overflow&&"width"!=this.overflow?3:1)))+"px";b.top=Math.round(this.bounds.y-d*("fill"!=this.overflow?3:1))+"px";b.opacity=100>this.opacity?this.opacity/100:""}; +mxText.prototype.updateInnerHtml=function(a){if(mxUtils.isNode(this.value))a.innerHTML=this.value.outerHTML;else{var b=this.value;this.dialect!=mxConstants.DIALECT_STRICTHTML&&(b=mxUtils.htmlEntities(b,!1));b=mxUtils.replaceTrailingNewlines(b,"
 
");b=this.replaceLinefeeds?b.replace(/\n/g,"
"):b;a.innerHTML='
'+b+"
"}}; +mxText.prototype.updateHtmlFilter=function(){var a=this.node.style,b=this.margin.x,c=this.margin.y,d=this.scale;mxUtils.setOpacity(this.node,this.opacity);var e,f=0,g=null!=this.state?this.state.view.textDiv:null,k=this.node;if(null!=g){g.style.overflow="";g.style.height="";g.style.width="";this.updateFont(g);this.updateSize(g,!1);this.updateInnerHtml(g);var l=Math.round(this.bounds.width/this.scale);this.wrap&&0m&&(m+=2*Math.PI);m%=Math.PI;m>Math.PI/2&&(m=Math.PI-m);var k=Math.cos(m),n=Math.sin(-m),b=l*-(b+.5),p=g*-(c+.5);0!=m&&(c="progid:DXImageTransform.Microsoft.Matrix(M11="+e+", M12="+f+", M21="+-f+", M22="+e+", sizingMethod='auto expand')",a.filter=null!=a.filter&&0
"),a=this.replaceLinefeeds?a.replace(/\n/g,"
"):a,b=null!=this.background&&this.background!=mxConstants.NONE?this.background:null,c=null!=this.border&&this.border!=mxConstants.NONE?this.border:null;if("fill"==this.overflow|| +"width"==this.overflow)null!=b&&(this.node.style.backgroundColor=b),null!=c&&(this.node.style.border="1px solid "+c);else{var d="";null!=b&&(d+="background-color:"+b+";");null!=c&&(d+="border:1px solid "+c+";");a='
'+a+"
"}this.node.innerHTML=a;a=this.node.getElementsByTagName("div"); +0=document.documentMode)&&0!=this.rotation?mxClient.VML_PREFIX+":image":"img");a.setAttribute("border","0");a.style.position="absolute";a.src=this.image;b=100>this.opacity?"alpha(opacity="+this.opacity+")":"";this.node.style.filter=b;this.flipH&&this.flipV?b+="progid:DXImageTransform.Microsoft.BasicImage(rotation=2)":this.flipH?b+="progid:DXImageTransform.Microsoft.BasicImage(mirror=1)":this.flipV&& +(b+="progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)");a.style.filter!=b&&(a.style.filter=b);"image"==a.nodeName?a.style.rotation=this.rotation:0!=this.rotation?mxUtils.setPrefixedStyle(a.style,"transform","rotate("+this.rotation+"deg)"):mxUtils.setPrefixedStyle(a.style,"transform","");a.style.width=this.node.style.width;a.style.height=this.node.style.height;this.node.style.backgroundImage="";this.node.appendChild(a)}else this.setTransparentBackgroundImage(this.node)}; +function mxLabel(a,b,c,d){mxRectangleShape.call(this,a,b,c,d)}mxUtils.extend(mxLabel,mxRectangleShape);mxLabel.prototype.imageSize=mxConstants.DEFAULT_IMAGESIZE;mxLabel.prototype.spacing=2;mxLabel.prototype.indicatorSize=10;mxLabel.prototype.indicatorSpacing=2;mxLabel.prototype.init=function(a){mxShape.prototype.init.apply(this,arguments);null!=this.indicatorShape&&(this.indicator=new this.indicatorShape,this.indicator.dialect=this.dialect,this.indicator.init(this.node))}; +mxLabel.prototype.redraw=function(){null!=this.indicator&&(this.indicator.fill=this.indicatorColor,this.indicator.stroke=this.indicatorStrokeColor,this.indicator.gradient=this.indicatorGradientColor,this.indicator.direction=this.indicatorDirection);mxShape.prototype.redraw.apply(this,arguments)};mxLabel.prototype.isHtmlAllowed=function(){return mxRectangleShape.prototype.isHtmlAllowed.apply(this,arguments)&&null==this.indicatorColor&&null==this.indicatorShape}; +mxLabel.prototype.paintForeground=function(a,b,c,d,e){this.paintImage(a,b,c,d,e);this.paintIndicator(a,b,c,d,e);mxRectangleShape.prototype.paintForeground.apply(this,arguments)};mxLabel.prototype.paintImage=function(a,b,c,d,e){null!=this.image&&(b=this.getImageBounds(b,c,d,e),a.image(b.x,b.y,b.width,b.height,this.image,!1,!1,!1))}; +mxLabel.prototype.getImageBounds=function(a,b,c,d){var e=mxUtils.getValue(this.style,mxConstants.STYLE_IMAGE_ALIGN,mxConstants.ALIGN_LEFT),f=mxUtils.getValue(this.style,mxConstants.STYLE_IMAGE_VERTICAL_ALIGN,mxConstants.ALIGN_MIDDLE),g=mxUtils.getNumber(this.style,mxConstants.STYLE_IMAGE_WIDTH,mxConstants.DEFAULT_IMAGESIZE),k=mxUtils.getNumber(this.style,mxConstants.STYLE_IMAGE_HEIGHT,mxConstants.DEFAULT_IMAGESIZE),l=mxUtils.getNumber(this.style,mxConstants.STYLE_SPACING,this.spacing)+5;a=e==mxConstants.ALIGN_CENTER? +a+(c-g)/2:e==mxConstants.ALIGN_RIGHT?a+(c-g-l):a+l;b=f==mxConstants.ALIGN_TOP?b+l:f==mxConstants.ALIGN_BOTTOM?b+(d-k-l):b+(d-k)/2;return new mxRectangle(a,b,g,k)};mxLabel.prototype.paintIndicator=function(a,b,c,d,e){null!=this.indicator?(this.indicator.bounds=this.getIndicatorBounds(b,c,d,e),this.indicator.paint(a)):null!=this.indicatorImage&&(b=this.getIndicatorBounds(b,c,d,e),a.image(b.x,b.y,b.width,b.height,this.indicatorImage,!1,!1,!1))}; +mxLabel.prototype.getIndicatorBounds=function(a,b,c,d){var e=mxUtils.getValue(this.style,mxConstants.STYLE_IMAGE_ALIGN,mxConstants.ALIGN_LEFT),f=mxUtils.getValue(this.style,mxConstants.STYLE_IMAGE_VERTICAL_ALIGN,mxConstants.ALIGN_MIDDLE),g=mxUtils.getNumber(this.style,mxConstants.STYLE_INDICATOR_WIDTH,this.indicatorSize),k=mxUtils.getNumber(this.style,mxConstants.STYLE_INDICATOR_HEIGHT,this.indicatorSize),l=this.spacing+5;a=e==mxConstants.ALIGN_RIGHT?a+(c-g-l):e==mxConstants.ALIGN_CENTER?a+(c-g)/ +2:a+l;b=f==mxConstants.ALIGN_BOTTOM?b+(d-k-l):f==mxConstants.ALIGN_TOP?b+l:b+(d-k)/2;return new mxRectangle(a,b,g,k)}; +mxLabel.prototype.redrawHtmlShape=function(){for(mxRectangleShape.prototype.redrawHtmlShape.apply(this,arguments);this.node.hasChildNodes();)this.node.removeChild(this.node.lastChild);if(null!=this.image){var a=document.createElement("img");a.style.position="relative";a.setAttribute("border","0");var b=this.getImageBounds(this.bounds.x,this.bounds.y,this.bounds.width,this.bounds.height);b.x-=this.bounds.x;b.y-=this.bounds.y;a.style.left=Math.round(b.x)+"px";a.style.top=Math.round(b.y)+"px";a.style.width= +Math.round(b.width)+"px";a.style.height=Math.round(b.height)+"px";a.src=this.image;this.node.appendChild(a)}};function mxCylinder(a,b,c,d){mxShape.call(this);this.bounds=a;this.fill=b;this.stroke=c;this.strokewidth=null!=d?d:1}mxUtils.extend(mxCylinder,mxShape);mxCylinder.prototype.maxHeight=40;mxCylinder.prototype.svgStrokeTolerance=0; +mxCylinder.prototype.paintVertexShape=function(a,b,c,d,e){a.translate(b,c);a.begin();this.redrawPath(a,b,c,d,e,!1);a.fillAndStroke();a.setShadow(!1);a.begin();this.redrawPath(a,b,c,d,e,!0);a.stroke()}; +mxCylinder.prototype.redrawPath=function(a,b,c,d,e,f){b=Math.min(this.maxHeight,Math.round(e/5));if(f&&null!=this.fill||!f&&null==this.fill)a.moveTo(0,b),a.curveTo(0,2*b,d,2*b,d,b),f||(a.stroke(),a.begin());f||(a.moveTo(0,b),a.curveTo(0,-b/3,d,-b/3,d,b),a.lineTo(d,e-b),a.curveTo(d,e+b/3,0,e+b/3,0,e-b),a.close())};function mxConnector(a,b,c){mxPolyline.call(this,a,b,c)}mxUtils.extend(mxConnector,mxPolyline); +mxConnector.prototype.updateBoundingBox=function(){this.useSvgBoundingBox=null!=this.style&&1==this.style[mxConstants.STYLE_CURVED];mxShape.prototype.updateBoundingBox.apply(this,arguments)};mxConnector.prototype.paintEdgeShape=function(a,b){var c=this.createMarker(a,b,!0),d=this.createMarker(a,b,!1);mxPolyline.prototype.paintEdgeShape.apply(this,arguments);a.setFillColor(this.stroke);a.setShadow(!1);a.setDashed(!1);null!=c&&c();null!=d&&d()}; +mxConnector.prototype.createMarker=function(a,b,c){var d=null,e=b.length,f=mxUtils.getValue(this.style,c?mxConstants.STYLE_STARTARROW:mxConstants.STYLE_ENDARROW),g=c?b[1]:b[e-2],k=c?b[0]:b[e-1];if(null!=f&&null!=g&&null!=k){for(d=1;d=e)&&a.close(),a.fillAndStroke(),f=d)&& +a.close(),a.fillAndStroke(),f=e)&&a.close(),a.fillAndStroke(),f=d)&&a.close(),a.fillAndStroke(),fa.weightedValue?-1:b.weightedValuec)break;g=l}}f=e.getIndex(a);f=Math.max(0,b-(b>f?1:0));d.add(e,a,f)}}; +mxStackLayout.prototype.getParentSize=function(a){var b=this.graph.getModel(),c=b.getGeometry(a);null!=this.graph.container&&(null==c&&b.isLayer(a)||a==this.graph.getView().currentRoot)&&(c=new mxRectangle(0,0,this.graph.container.offsetWidth-1,this.graph.container.offsetHeight-1));return c}; +mxStackLayout.prototype.execute=function(a){if(null!=a){var b=this.getParentSize(a),c=this.isHorizontal(),d=this.graph.getModel(),e=null;null!=b&&(e=c?b.height-this.marginTop-this.marginBottom:b.width-this.marginLeft-this.marginRight);var e=e-2*this.border,f=this.x0+this.border+this.marginLeft,g=this.y0+this.border+this.marginTop;if(this.graph.isSwimlane(a)){var k=this.graph.getCellStyle(a),l=mxUtils.getNumber(k,mxConstants.STYLE_STARTSIZE,mxConstants.DEFAULT_STARTSIZE),k=1==mxUtils.getValue(k,mxConstants.STYLE_HORIZONTAL, +!0);null!=b&&(l=k?Math.min(l,b.height):Math.min(l,b.width));c==k&&(e-=l);k?g+=l:f+=l}d.beginUpdate();try{for(var l=0,k=null,m=0,n=null,p=d.getChildCount(a),q=0;qthis.wrap||!c&&k.y+k.height+t.height+2*this.spacing>this.wrap)&&(k=null,c?g+=l+this.spacing:f+=l+this.spacing,l=0);var l=Math.max(l,c?t.height: +t.width),u=0;if(!this.borderCollapse)var x=this.graph.getCellStyle(r),u=mxUtils.getNumber(x,mxConstants.STYLE_STROKEWIDTH,1);null!=k?c?t.x=m+this.spacing+Math.floor(u/2):t.y=m+this.spacing+Math.floor(u/2):this.keepFirstLocation||(c?t.x=f:t.y=g);c?t.y=g:t.x=f;this.fill&&null!=e&&(c?t.height=e:t.width=e);this.setChildGeometry(r,t);n=r;k=t;m=c?k.x+k.width+Math.floor(u/2):k.y+k.height+Math.floor(u/2)}}}this.resizeParent&&null!=b&&null!=k&&!this.graph.isCellCollapsed(a)?this.updateParentGeometry(a,b,k): +this.resizeLast&&null!=b&&null!=k&&null!=n&&(c?k.width=b.width-k.x-this.spacing-this.marginRight-this.marginLeft:k.height=b.height-k.y-this.spacing-this.marginBottom,this.setChildGeometry(n,k))}finally{d.endUpdate()}}};mxStackLayout.prototype.setChildGeometry=function(a,b){var c=this.graph.getCellGeometry(a);null!=c&&b.x==c.x&&b.y==c.y&&b.width==c.width&&b.height==c.height||this.graph.getModel().setGeometry(a,b)}; +mxStackLayout.prototype.updateParentGeometry=function(a,b,c){var d=this.isHorizontal(),e=this.graph.getModel(),f=b.clone();d?(c=c.x+c.width+this.marginRight+this.border,f.width=this.resizeParentMax?Math.max(f.width,c):c):(c=c.y+c.height+this.marginBottom+this.border,f.height=this.resizeParentMax?Math.max(f.height,c):c);b.x==f.x&&b.y==f.y&&b.width==f.width&&b.height==f.height||e.setGeometry(a,f)}; +function mxPartitionLayout(a,b,c,d){mxGraphLayout.call(this,a);this.horizontal=null!=b?b:!0;this.spacing=c||0;this.border=d||0}mxPartitionLayout.prototype=new mxGraphLayout;mxPartitionLayout.prototype.constructor=mxPartitionLayout;mxPartitionLayout.prototype.horizontal=null;mxPartitionLayout.prototype.spacing=null;mxPartitionLayout.prototype.border=null;mxPartitionLayout.prototype.resizeVertices=!0;mxPartitionLayout.prototype.isHorizontal=function(){return this.horizontal}; +mxPartitionLayout.prototype.moveCell=function(a,b,c){c=this.graph.getModel();var d=c.getParent(a);if(null!=a&&null!=d){var e,f=0,g=c.getChildCount(d);for(e=0;eb)break;f=k}}b=d.getIndex(a);b=Math.max(0,e-(e>b?1:0));c.add(d,a,b)}}; +mxPartitionLayout.prototype.execute=function(a){var b=this.isHorizontal(),c=this.graph.getModel(),d=c.getGeometry(a);null!=this.graph.container&&(null==d&&c.isLayer(a)||a==this.graph.getView().currentRoot)&&(d=new mxRectangle(0,0,this.graph.container.offsetWidth-1,this.graph.container.offsetHeight-1));if(null!=d){for(var e=[],f=c.getChildCount(a),g=0;gk.x&&(e=Math.abs(g-k.x));0>k.y&&(l=Math.abs(d-k.y));0==e&&0==l||this.moveNode(this.node,e,l);this.resizeParent&&this.adjustParents();this.edgeRouting&&this.localEdgeProcessing(this.node)}null!=this.parentX&&null!=this.parentY&&(f=this.graph.getCellGeometry(a),null!=f&&(f=f.clone(),f.x=this.parentX,f.y=this.parentY,c.setGeometry(a,f)))}}finally{c.endUpdate()}}}; +mxCompactTreeLayout.prototype.moveNode=function(a,b,c){a.x+=b;a.y+=c;this.apply(a);for(a=a.child;null!=a;)this.moveNode(a,b,c),a=a.next}; +mxCompactTreeLayout.prototype.sortOutgoingEdges=function(a,b){var c=new mxDictionary;b.sort(function(b,e){var d=b.getTerminal(b.getTerminal(!1)==a),g=c.get(d);null==g&&(g=mxCellPath.create(d).split(mxCellPath.PATH_SEPARATOR),c.put(d,g));var d=e.getTerminal(e.getTerminal(!1)==a),k=c.get(d);null==k&&(k=mxCellPath.create(d).split(mxCellPath.PATH_SEPARATOR),c.put(d,k));return mxCellPath.compare(g,k)})}; +mxCompactTreeLayout.prototype.findRankHeights=function(a,b){if(null==this.maxRankHeight[b]||this.maxRankHeight[b]a.height&&(a.height=this.maxRankHeight[b]);for(var c=a.child;null!=c;)this.setCellHeights(c,b+1),c=c.next}; +mxCompactTreeLayout.prototype.dfs=function(a,b){var c=mxCellPath.create(a),d=null;if(null!=a&&null==this.visited[c]&&!this.isVertexIgnored(a)){this.visited[c]=a;var d=this.createNode(a),c=this.graph.getModel(),e=null,f=this.graph.getEdges(a,b,this.invert,!this.invert,!1,!0),g=this.graph.getView();this.sortEdges&&this.sortOutgoingEdges(a,f);for(var k=0;k=a+c)return 0;a=0a?a*d/c-b:0a+c?(c+a)*f/e-(b+d):f-(b+d);return 0g+2*this.prefHozEdgeSep&&(f-=2*this.prefHozEdgeSep);a=f/d;b=a/2;f>g+2*this.prefHozEdgeSep&&(b+=this.prefHozEdgeSep);for(var f=this.minEdgeJetty-this.prefVertEdgeOff,g=this.getVertexBounds(c),k=0;kd/2&&(f-=this.prefVertEdgeOff);b+=a}}; +function mxRadialTreeLayout(a){mxCompactTreeLayout.call(this,a,!1)}mxUtils.extend(mxRadialTreeLayout,mxCompactTreeLayout);mxRadialTreeLayout.prototype.angleOffset=.5;mxRadialTreeLayout.prototype.rootx=0;mxRadialTreeLayout.prototype.rooty=0;mxRadialTreeLayout.prototype.levelDistance=120;mxRadialTreeLayout.prototype.nodeDistance=10;mxRadialTreeLayout.prototype.autoRadius=!1;mxRadialTreeLayout.prototype.sortEdges=!1;mxRadialTreeLayout.prototype.rowMinX=[];mxRadialTreeLayout.prototype.rowMaxX=[]; +mxRadialTreeLayout.prototype.rowMinCenX=[];mxRadialTreeLayout.prototype.rowMaxCenX=[];mxRadialTreeLayout.prototype.rowRadi=[];mxRadialTreeLayout.prototype.row=[];mxRadialTreeLayout.prototype.isVertexIgnored=function(a){return mxGraphLayout.prototype.isVertexIgnored.apply(this,arguments)||0==this.graph.getConnections(a).length}; +mxRadialTreeLayout.prototype.execute=function(a,b){this.parent=a;this.edgeRouting=this.useBoundingBox=!1;mxCompactTreeLayout.prototype.execute.apply(this,arguments);var c=null,d=this.getVertexBounds(this.root);this.centerX=d.x+d.width/2;this.centerY=d.y+d.height/2;for(var e in this.visited){var f=this.getVertexBounds(this.visited[e]),c=null!=c?c:f.clone();c.add(f)}this.calcRowDims([this.node],0);for(var g=0,k=0,c=0;c +d.theta&&ethis.forceConstant&&(this.forceConstant= +.001);this.forceConstantSquared=this.forceConstant*this.forceConstant;for(d=0;db&&(b=.001);var c=this.dispX[a]/b*Math.min(b,this.temperature),b=this.dispY[a]/b*Math.min(b,this.temperature);this.dispX[a]=0;this.dispY[a]=0;this.cellLocation[a][0]+=c;this.cellLocation[a][1]+=b}}; +mxFastOrganicLayout.prototype.calcAttraction=function(){for(var a=0;athis.maxDistanceLimit||(ga?a+"-"+c:c+"-"+a):null}; +mxParallelEdgeLayout.prototype.layout=function(a){var b=a[0],c=this.graph.getView(),d=this.graph.getModel(),e=d.getGeometry(c.getVisibleTerminal(b,!0)),d=d.getGeometry(c.getVisibleTerminal(b,!1));if(e==d)for(var b=e.x+e.width+this.spacing,c=e.y+e.height/2,f=0;fmxUtils.indexOf(l.connectsAsTarget,g)&&l.connectsAsTarget.push(g))}}c[d].temp[0]=1}}mxGraphHierarchyModel.prototype.maxRank=null;mxGraphHierarchyModel.prototype.vertexMapper=null;mxGraphHierarchyModel.prototype.edgeMapper=null;mxGraphHierarchyModel.prototype.ranks=null;mxGraphHierarchyModel.prototype.roots=null;mxGraphHierarchyModel.prototype.parent=null; +mxGraphHierarchyModel.prototype.dfsCount=0;mxGraphHierarchyModel.prototype.SOURCESCANSTARTRANK=1E8;mxGraphHierarchyModel.prototype.tightenToSource=!1; +mxGraphHierarchyModel.prototype.createInternalCells=function(a,b,c){for(var d=a.getGraph(),e=0;e=l.length){for(var k= +new mxGraphHierarchyEdge(l),m=0;mmxUtils.indexOf(c[e].connectsAsSource,k)&&c[e].connectsAsSource.push(k)}}}c[e].temp[0]=0}}; +mxGraphHierarchyModel.prototype.initialRank=function(){var a=[];if(null!=this.roots)for(var b=0;bc.maxRank&&0>c.minRank&&(a[c.temp[0]].push(c),c.maxRank=c.temp[0],c.minRank=c.temp[0],c.temp[0]=a[c.maxRank].length-1);if(null!=b&&null!=d&&1mxUtils.indexOf(l.connectsAsTarget,g)&&l.connectsAsTarget.push(g))}}c[d].temp[0]=1}}mxSwimlaneModel.prototype.maxRank=null;mxSwimlaneModel.prototype.vertexMapper=null;mxSwimlaneModel.prototype.edgeMapper=null;mxSwimlaneModel.prototype.ranks=null;mxSwimlaneModel.prototype.roots=null;mxSwimlaneModel.prototype.parent=null;mxSwimlaneModel.prototype.dfsCount=0; +mxSwimlaneModel.prototype.SOURCESCANSTARTRANK=1E8;mxSwimlaneModel.prototype.tightenToSource=!1;mxSwimlaneModel.prototype.ranksPerGroup=null; +mxSwimlaneModel.prototype.createInternalCells=function(a,b,c){for(var d=a.getGraph(),e=a.swimlanes,f=0;f=m.length){for(var l=new mxGraphHierarchyEdge(m),n=0;nmxUtils.indexOf(c[f].connectsAsSource,l)&&c[f].connectsAsSource.push(l)}}}c[f].temp[0]=0}}; +mxSwimlaneModel.prototype.initialRank=function(){this.ranksPerGroup=[];var a=[],b={};if(null!=this.roots)for(var c=0;cb[d.swimlaneIndex]&&(k=b[d.swimlaneIndex]);d.temp[0]=k;if(null!=f)for(c=0;cc.maxRank&&0>c.minRank&&(a[c.temp[0]].push(c),c.maxRank=c.temp[0],c.minRank=c.temp[0],c.temp[0]=a[c.maxRank].length-1);if(null!=b&&null!=d&&1>1,++e[l];return c}; +mxMedianHybridCrossingReduction.prototype.transpose=function(a,b){for(var c=!0,d=0;c&&10>d++;)for(var e=1==a%2&&1==d%2,c=!1,f=0;fn&&(n=l);k[n]=m}for(var p=null,q=null,r,t,u=null,x=null,y,A=null,l=0;lu[C]&&v++,r[z]x[C]&&v++,t[z]a.medianValue?-1:b.medianValuex+1&&(m==d[l].length-1?(e.setGeneralPurposeVariable(l,y),p=!0):(m=d[l][m+1],x=m.getGeneralPurposeVariable(l),x=x-m.width/2-this.intraCellSpacing-e.width/2,x>y?(e.setGeneralPurposeVariable(l, +y),p=!0):x>e.getGeneralPurposeVariable(l)+1&&(e.setGeneralPurposeVariable(l,x),p=!0)));if(p){for(e=0;e=k&&l<=q?g.setGeneralPurposeVariable(a,l):lq&&(g.setGeneralPurposeVariable(a,q),this.currentXDelta+=l-q);d[f].visited=!0}};mxCoordinateAssignment.prototype.calculatedWeightedValue=function(a,b){for(var c=0,d=0;dthis.widestRankValue&&(this.widestRankValue=k,this.widestRank=e);this.rankWidths[e]=k}1==l&&mxLog.warn("At least one cell has no bounds");this.rankY[e]=c;k=f/2+d/2+this.interRankCellSpacing;d=f;c=this.orientation==mxConstants.DIRECTION_NORTH||this.orientation==mxConstants.DIRECTION_WEST?c+k:c- +k;for(m=0;me.maxRank-e.minRank-1)){for(var f=e.getGeneralPurposeVariable(e.minRank+1),g=!0,k=0,l=e.minRank+2;le.minRank+1;l--)q=e.getX(l-1),p==q?(n[l-e.minRank-2]=p,g++):this.repositionValid(b,e,l-1,p)?(n[l-e.minRank-2]=p,g++):(n[l-e.minRank-2]=e.getX(l-1),p=q);if(g>k||f>k)if(g>=f)for(l=e.maxRank-2;l>e.minRank;l--)e.setX(l,n[l-e.minRank-1]);else if(f>g)for(l=e.minRank+2;le)return!1;f=b.getGeneralPurposeVariable(c);if(df){if(e==a.length-1)return!0;a=a[e+1];c=a.getGeneralPurposeVariable(c);c=c-a.width/2-this.intraCellSpacing-b.width/2;if(!(c>=d))return!1}return!0}; +mxCoordinateAssignment.prototype.setCellLocations=function(a,b){this.rankTopY=[];this.rankBottomY=[];for(var c=0;ck;k++){if(-1(f+1)*this.prefHozEdgeSep+2*this.prefHozEdgeSep&&(n+=this.prefHozEdgeSep,p-=this.prefHozEdgeSep);l=(p-n)/f;n+=l/2;p=this.minEdgeJetty-this.prefVertEdgeOff;for(m=0;mf/2&&(p-=this.prefVertEdgeOff),t=0;tf&&(f=l,e=k)}}0==c.length&&null!=e&&c.push(e)}return c}; +mxHierarchicalLayout.prototype.getEdges=function(a){var b=this.edgesCache.get(a);if(null!=b)return b;for(var c=this.graph.model,b=[],d=this.graph.isCellCollapsed(a),e=c.getChildCount(a),f=0;fb.length)){null==a&&(a=c.getParent(b[0]));this.parentY=this.parentX=null;if(a!=this.root&&null!=c.isVertex(a)&&this.maintainParentLocation){var d=this.graph.getCellGeometry(a);null!=d&&(this.parentX=d.x,this.parentY=d.y)}this.swimlanes=b;this.dummyVertices=[];for(var e=0;ef&&(f=l,e=k)}}0==c.length&&null!=e&&c.push(e)}return c}; +mxSwimlaneLayout.prototype.getEdges=function(a){var b=this.edgesCache.get(a);if(null!=b)return b;for(var c=this.graph.model,b=[],d=this.graph.isCellCollapsed(a),e=c.getChildCount(a),f=0;f=this.swimlanes.length||!(q>k||(!b||p)&&q==k)||(e=this.traverse(n, +b,m[c],d,e,f,g,q))}}else if(null==e[l])for(c=0;cmxUtils.indexOf(this.edges,a))&&(null==this.edges&&(this.edges=[]),this.edges.push(a));return a};mxCell.prototype.removeEdge=function(a,b){if(null!=a){if(a.getTerminal(!b)!=this&&null!=this.edges){var c=this.getEdgeIndex(a);0<=c&&this.edges.splice(c,1)}a.setTerminal(null,b)}return a}; +mxCell.prototype.removeFromTerminal=function(a){var b=this.getTerminal(a);null!=b&&b.removeEdge(this,a)};mxCell.prototype.hasAttribute=function(a){var b=this.getValue();return null!=b&&b.nodeType==mxConstants.NODETYPE_ELEMENT&&b.hasAttribute?b.hasAttribute(a):null!=b.getAttribute(a)};mxCell.prototype.getAttribute=function(a,b){var c=this.getValue();return(null!=c&&c.nodeType==mxConstants.NODETYPE_ELEMENT?c.getAttribute(a):null)||b}; +mxCell.prototype.setAttribute=function(a,b){var c=this.getValue();null!=c&&c.nodeType==mxConstants.NODETYPE_ELEMENT&&c.setAttribute(a,b)};mxCell.prototype.clone=function(){var a=mxUtils.clone(this,this.mxTransient);a.setValue(this.cloneValue());return a};mxCell.prototype.cloneValue=function(){var a=this.getValue();null!=a&&("function"==typeof a.clone?a=a.clone():isNaN(a.nodeType)||(a=a.cloneNode(!0)));return a};function mxGeometry(a,b,c,d){mxRectangle.call(this,a,b,c,d)}mxGeometry.prototype=new mxRectangle; +mxGeometry.prototype.constructor=mxGeometry;mxGeometry.prototype.TRANSLATE_CONTROL_POINTS=!0;mxGeometry.prototype.alternateBounds=null;mxGeometry.prototype.sourcePoint=null;mxGeometry.prototype.targetPoint=null;mxGeometry.prototype.points=null;mxGeometry.prototype.offset=null;mxGeometry.prototype.relative=!1; +mxGeometry.prototype.swap=function(){if(null!=this.alternateBounds){var a=new mxRectangle(this.x,this.y,this.width,this.height);this.x=this.alternateBounds.x;this.y=this.alternateBounds.y;this.width=this.alternateBounds.width;this.height=this.alternateBounds.height;this.alternateBounds=a}};mxGeometry.prototype.getTerminalPoint=function(a){return a?this.sourcePoint:this.targetPoint};mxGeometry.prototype.setTerminalPoint=function(a,b){b?this.sourcePoint=a:this.targetPoint=a;return a}; +mxGeometry.prototype.rotate=function(a,b){var c=mxUtils.toRadians(a),d=Math.cos(c),c=Math.sin(c);if(!this.relative){var e=new mxPoint(this.getCenterX(),this.getCenterY()),e=mxUtils.getRotatedPoint(e,d,c,b);this.x=Math.round(e.x-this.width/2);this.y=Math.round(e.y-this.height/2)}null!=this.sourcePoint&&(e=mxUtils.getRotatedPoint(this.sourcePoint,d,c,b),this.sourcePoint.x=Math.round(e.x),this.sourcePoint.y=Math.round(e.y));null!=this.targetPoint&&(e=mxUtils.getRotatedPoint(this.targetPoint,d,c,b),this.targetPoint.x= +Math.round(e.x),this.targetPoint.y=Math.round(e.y));if(null!=this.points)for(var f=0;fb[e]?1:-1:(c=parseInt(a[e]),e=parseInt(b[e]),d=c==e?0:c>e?1:-1);break}0==d&&(c=a.length,e=b.length,c!=e&&(d=c>e?1:-1));return d}},mxPerimeter={RectanglePerimeter:function(a,b,c,d){b=a.getCenterX();var e=a.getCenterY(),f=Math.atan2(c.y-e,c.x-b),g=new mxPoint(0,0),k=Math.PI,l=Math.PI/2-f,m=Math.atan2(a.height,a.width);f<-k+m||f>k-m?(g.x=a.x,g.y=e-a.width* +Math.tan(f)/2):f<-m?(g.y=a.y,g.x=b-a.height*Math.tan(l)/2):f=a.x&&c.x<=a.x+a.width?g.x=c.x:c.y>=a.y&&c.y<=a.y+a.height&&(g.y=c.y),c.xa.x+a.width&&(g.x=a.x+a.width),c.ya.y+a.height&&(g.y=a.y+a.height));return g},EllipsePerimeter:function(a,b,c,d){var e=a.x,f=a.y,g=a.width/2,k=a.height/2,l=e+g,m=f+k;b=c.x;c=c.y;var n=parseInt(b-l),p=parseInt(c-m);if(0==n&&0!=p)return new mxPoint(l, +m+k*p/Math.abs(p));if(0==n&&0==p)return new mxPoint(b,c);if(d){if(c>=f&&c<=f+a.height)return a=c-m,a=Math.sqrt(g*g*(1-a*a/(k*k)))||0,b<=e&&(a=-a),new mxPoint(l+a,c);if(b>=e&&b<=e+a.width)return a=b-l,a=Math.sqrt(k*k*(1-a*a/(g*g)))||0,c<=f&&(a=-a),new mxPoint(b,m+a)}e=p/n;m-=e*l;f=g*g*e*e+k*k;a=-2*l*f;k=Math.sqrt(a*a-4*f*(g*g*e*e*l*l+k*k*l*l-g*g*k*k));g=(-a+k)/(2*f);l=(-a-k)/(2*f);k=e*g+m;m=e*l+m;Math.sqrt(Math.pow(g-b,2)+Math.pow(k-c,2))c?new mxPoint(g,e):new mxPoint(g,e+a);if(k==c)return g>l?new mxPoint(b,k):new mxPoint(b+f,k);var m=g,n=k;d&&(l>=b&&l<=b+f?m=l:c>=e&&c<=e+a&&(n=c));return l-t&&rMath.PI-t)?c=d&&(e&&c.x>=n.x&&c.x<=q.x||!e&&c.y>=n.y&&c.y<=q.y)?e?new mxPoint(c.x,n.y):new mxPoint(n.x,c.y):b==mxConstants.DIRECTION_NORTH?new mxPoint(f+k/2+l*Math.tan(r)/2,g+l):b==mxConstants.DIRECTION_SOUTH?new mxPoint(f+k/2-l*Math.tan(r)/2,g):b==mxConstants.DIRECTION_WEST?new mxPoint(f+k,g+l/2+k*Math.tan(r)/2):new mxPoint(f,g+ +l/2-k*Math.tan(r)/2):(d&&(d=new mxPoint(a,m),c.y>=g&&c.y<=g+l?(d.x=e?a:b==mxConstants.DIRECTION_WEST?f+k:f,d.y=c.y):c.x>=f&&c.x<=f+k&&(d.x=c.x,d.y=e?b==mxConstants.DIRECTION_NORTH?g+l:g:m),a=d.x,m=d.y),c=e&&c.x<=f+k/2||!e&&c.y<=g+l/2?mxUtils.intersection(c.x,c.y,a,m,n.x,n.y,p.x,p.y):mxUtils.intersection(c.x,c.y,a,m,p.x,p.y,q.x,q.y));null==c&&(c=new mxPoint(a,m));return c},HexagonPerimeter:function(a,b,c,d){var e=a.x,f=a.y,g=a.width,k=a.height,l=a.getCenterX();a=a.getCenterY();var m=c.x,n=c.y,p=-Math.atan2(n- +a,m-l),q=Math.PI,r=Math.PI/2;new mxPoint(l,a);b=null!=b?mxUtils.getValue(b.style,mxConstants.STYLE_DIRECTION,mxConstants.DIRECTION_EAST):mxConstants.DIRECTION_EAST;var t=b==mxConstants.DIRECTION_NORTH||b==mxConstants.DIRECTION_SOUTH;b=new mxPoint;var u=new mxPoint;if(mf+k||m>e+g&&ne+g&&n>f+k)d=!1;if(d){if(t){if(m==l){if(n<=f)return new mxPoint(l,f);if(n>=f+k)return new mxPoint(l,f+k)}else if(me+g){if(n==f+k/4)return new mxPoint(e+g,f+k/4);if(n==f+3*k/4)return new mxPoint(e+g,f+3*k/4)}else if(m==e){if(na)return new mxPoint(e,f+3*k/4)}else if(m==e+g){if(na)return new mxPoint(e+g,f+3*k/4)}if(n==f)return new mxPoint(l,f);if(n==f+k)return new mxPoint(l,f+k);mf+k/4&&nf+3*k/4&&(b=new mxPoint(e-Math.floor(.5*g),f+Math.floor(.5*k)),u=new mxPoint(e+g,f+Math.floor(1.25*k))):m>l&&(n>f+k/4&&nf+3*k/4&&(b=new mxPoint(e+Math.floor(1.5*g),f+Math.floor(.5*k)),u=new mxPoint(e,f+Math.floor(1.25*k))))}else{if(n==a){if(m<=e)return new mxPoint(e,f+k/2);if(m>=e+g)return new mxPoint(e+g,f+k/2)}else if(n< +f){if(m==e+g/4)return new mxPoint(e+g/4,f);if(m==e+3*g/4)return new mxPoint(e+3*g/4,f)}else if(n>f+k){if(m==e+g/4)return new mxPoint(e+g/4,f+k);if(m==e+3*g/4)return new mxPoint(e+3*g/4,f+k)}else if(n==f){if(ml)return new mxPoint(e+3*g/4,f)}else if(n==f+k){if(ma)return new mxPoint(e+3*g/4,f+k)}if(m==e)return new mxPoint(e,a);if(m==e+g)return new mxPoint(e+g,a);ne+g/4&&me+3*g/4&&(b=new mxPoint(e+Math.floor(.5*g),f-Math.floor(.5*k)),u=new mxPoint(e+Math.floor(1.25*g),f+k)):n>a&&(m>e+g/4&&me+3*g/4&&(b=new mxPoint(e+Math.floor(.5*g),f+Math.floor(1.5*k)),u=new mxPoint(e+Math.floor(1.25*g),f)))}d=l;p=a;m>=e&&m<= +e+g?(d=m,p=n=f&&n<=f+k&&(p=n,d=m-m?(b=new mxPoint(e+g,f),u=new mxPoint(e+g,f+ +k)):p>m&&pr&&pq-m&&p<=q||p<-q+m&&p>=-q?(b=new mxPoint(e,f),u=new mxPoint(e,f+k)):p<-m&&p>-r?(b=new mxPoint(e+Math.floor(1.5*g),f+Math.floor(.5*k)),u=new mxPoint(e,f+Math.floor(1.25*k))):p<-r&&p>-q+m&&(b=new mxPoint(e-Math.floor(.5*g),f+Math.floor(.5*k)),u=new mxPoint(e+g,f+Math.floor(1.25*k)))}else{m= +Math.atan2(k/2,g/4);if(p==m)return new mxPoint(e+Math.floor(.75*g),f);if(p==q-m)return new mxPoint(e+Math.floor(.25*g),f);if(p==q||p==-q)return new mxPoint(e,f+Math.floor(.5*k));if(0==p)return new mxPoint(e+g,f+Math.floor(.5*k));if(p==-m)return new mxPoint(e+Math.floor(.75*g),f+k);if(p==-q+m)return new mxPoint(e+Math.floor(.25*g),f+k);0m&&pq-m&& +pp&&p>-m?(b=new mxPoint(e+Math.floor(.5*g),f+Math.floor(1.5*k)),u=new mxPoint(e+Math.floor(1.25*g),f)):p<-m&&p>-q+m?(b=new mxPoint(e,f+k),u=new mxPoint(e+g,f+k)):p<-q+m&&p>-q&&(b=new mxPoint(e-Math.floor(.25*g),f),u=new mxPoint(e+Math.floor(.5*g),f+Math.floor(1.5*k)))}c=mxUtils.intersection(l,a,c.x,c.y,b.x,b.y,u.x,u.y)}return null==c?new mxPoint(l,a):c}}; +function mxPrintPreview(a,b,c,d,e,f,g,k,l){this.graph=a;this.scale=null!=b?b:1/a.pageScale;this.border=null!=d?d:0;this.pageFormat=mxRectangle.fromRectangle(null!=c?c:a.pageFormat);this.title=null!=k?k:"Printer-friendly version";this.x0=null!=e?e:0;this.y0=null!=f?f:0;this.borderColor=g;this.pageSelector=null!=l?l:!0}mxPrintPreview.prototype.graph=null;mxPrintPreview.prototype.pageFormat=null;mxPrintPreview.prototype.scale=null;mxPrintPreview.prototype.border=0; +mxPrintPreview.prototype.marginTop=0;mxPrintPreview.prototype.marginBottom=0;mxPrintPreview.prototype.x0=0;mxPrintPreview.prototype.y0=0;mxPrintPreview.prototype.autoOrigin=!0;mxPrintPreview.prototype.printOverlays=!1;mxPrintPreview.prototype.printControls=!1;mxPrintPreview.prototype.printBackgroundImage=!1;mxPrintPreview.prototype.backgroundColor="#ffffff";mxPrintPreview.prototype.borderColor=null;mxPrintPreview.prototype.title=null;mxPrintPreview.prototype.pageSelector=null; +mxPrintPreview.prototype.wnd=null;mxPrintPreview.prototype.targetWindow=null;mxPrintPreview.prototype.pageCount=0;mxPrintPreview.prototype.clipping=!0;mxPrintPreview.prototype.getWindow=function(){return this.wnd}; +mxPrintPreview.prototype.getDoctype=function(){var a="";5==document.documentMode?a='':8==document.documentMode?a='':8'):("CSS1Compat"===document.compatMode&&k.writeln(""),k.writeln(""));k.writeln("");this.writeHead(k,a);k.writeln("");k.writeln('')}var m=this.graph.getGraphBounds().clone(),n=this.graph.getView().getScale(),p=n/this.scale,q=this.graph.getView().getTranslate();this.autoOrigin||(this.x0-=q.x*this.scale,this.y0-= +q.y*this.scale,m.width+=m.x,m.height+=m.y,m.x=0,this.border=m.y=0);var r=this.pageFormat.width-2*this.border,t=this.pageFormat.height-2*this.border;this.pageFormat.height+=this.marginTop+this.marginBottom;m.width/=p;m.height/=p;var u=Math.max(1,Math.ceil((m.width+this.x0)/r)),x=Math.max(1,Math.ceil((m.height+this.y0)/t));this.pageCount=u*x;var y=mxUtils.bind(this,function(){if(this.pageSelector&&(1");a.writeln("");a.close();mxEvent.release(a.body)}}; +mxPrintPreview.prototype.writeHead=function(a,b){null!=this.title&&a.writeln(""+this.title+"");mxClient.IS_VML&&a.writeln('');mxClient.link("stylesheet",mxClient.basePath+"/css/common.css",a);a.writeln('")};mxPrintPreview.prototype.writePostfix=function(a){}; +mxPrintPreview.prototype.createPageSelector=function(a,b){var c=this.wnd.document,d=c.createElement("table");d.className="mxPageSelector";d.setAttribute("border","0");for(var e=c.createElement("tbody"),f=0;f":"";mxCellEditor.prototype.escapeCancelsEditing=!0;mxCellEditor.prototype.textNode="";mxCellEditor.prototype.zIndex=5;mxCellEditor.prototype.minResize=new mxRectangle(0,20); +mxCellEditor.prototype.wordWrapPadding=mxClient.IS_QUIRKS?2:mxClient.IS_IE11?0:1;mxCellEditor.prototype.blurEnabled=!1;mxCellEditor.prototype.initialValue=null;mxCellEditor.prototype.init=function(){this.textarea=document.createElement("div");this.textarea.className="mxCellEditor mxPlainTextEditor";this.textarea.contentEditable=!0;mxClient.IS_GC&&(this.textarea.style.minHeight="1em");this.textarea.style.position=this.isLegacyEditor()?"absolute":"relative";this.installListeners(this.textarea)}; +mxCellEditor.prototype.applyValue=function(a,b){this.graph.labelChanged(a.cell,b,this.trigger)};mxCellEditor.prototype.getInitialValue=function(a,b){var c=mxUtils.htmlEntities(this.graph.getEditingValue(a.cell,b),!1);mxClient.IS_QUIRKS||8==document.documentMode||9==document.documentMode||10==document.documentMode||(c=mxUtils.replaceTrailingNewlines(c,"

"));return c.replace(/\n/g,"
")};mxCellEditor.prototype.getCurrentValue=function(a){return mxUtils.extractTextWithWhitespace(this.textarea.childNodes)}; +mxCellEditor.prototype.isCancelEditingKeyEvent=function(a){return this.escapeCancelsEditing||mxEvent.isShiftDown(a)||mxEvent.isControlDown(a)||mxEvent.isMetaDown(a)}; +mxCellEditor.prototype.installListeners=function(a){mxEvent.addListener(a,"blur",mxUtils.bind(this,function(a){this.blurEnabled&&this.focusLost(a)}));mxEvent.addListener(a,"keydown",mxUtils.bind(this,function(a){mxEvent.isConsumed(a)||(this.isStopEditingEvent(a)?(this.graph.stopEditing(!1),mxEvent.consume(a)):27==a.keyCode&&(this.graph.stopEditing(this.isCancelEditingKeyEvent(a)),mxEvent.consume(a)))}));var b=mxUtils.bind(this,function(b){null!=this.editingCell&&this.clearOnChange&&a.innerHTML==this.getEmptyLabelText()&& +(!mxClient.IS_FF||8!=b.keyCode&&46!=b.keyCode)&&(this.clearOnChange=!1,a.innerHTML="")});mxEvent.addListener(a,"keypress",b);mxEvent.addListener(a,"paste",b);b=mxUtils.bind(this,function(a){null!=this.editingCell&&(0==this.textarea.innerHTML.length||"
"==this.textarea.innerHTML?(this.textarea.innerHTML=this.getEmptyLabelText(),this.clearOnChange=0e&&(this.textarea.style.width=this.textarea.scrollWidth+"px")):this.textarea.style.maxWidth=e+"px"):(this.textarea.style.whiteSpace="nowrap",this.textarea.style.width= +"");8==document.documentMode&&(this.textarea.style.zoom="1",this.textarea.style.height="auto");a=this.textarea.scrollWidth;e=this.textarea.scrollHeight;8==document.documentMode?(this.textarea.style.left=Math.max(0,Math.ceil((this.bounds.x-d.x*(this.bounds.width-(a+1)*c)+a*(c-1)*0+2*(d.x+.5))/c))+"px",this.textarea.style.top=Math.max(0,Math.ceil((this.bounds.y-d.y*(this.bounds.height-(e+.5)*c)+e*(c-1)*0+1*Math.abs(d.y+.5))/c))+"px",this.textarea.style.width=Math.round(a*c)+"px",this.textarea.style.height= +Math.round(e*c)+"px"):mxClient.IS_QUIRKS?(this.textarea.style.left=Math.max(0,Math.ceil(this.bounds.x-d.x*(this.bounds.width-(a+1)*c)+a*(c-1)*0+2*(d.x+.5)))+"px",this.textarea.style.top=Math.max(0,Math.ceil(this.bounds.y-d.y*(this.bounds.height-(e+.5)*c)+e*(c-1)*0+1*Math.abs(d.y+.5)))+"px"):(this.textarea.style.left=Math.max(0,Math.round(this.bounds.x-d.x*(this.bounds.width-2))+1)+"px",this.textarea.style.top=Math.max(0,Math.round(this.bounds.y-d.y*(this.bounds.height-4)+(-1==d.y?3:0))+1)+"px")}else this.bounds= +this.getEditorBounds(a),this.textarea.style.width=Math.round(this.bounds.width/c)+"px",this.textarea.style.height=Math.round(this.bounds.height/c)+"px",8==document.documentMode||mxClient.IS_QUIRKS?(this.textarea.style.left=Math.round(this.bounds.x)+"px",this.textarea.style.top=Math.round(this.bounds.y)+"px"):(this.textarea.style.left=Math.max(0,Math.round(this.bounds.x+1))+"px",this.textarea.style.top=Math.max(0,Math.round(this.bounds.y+1))+"px"),this.graph.isWrapping(a.cell)&&(2<=this.bounds.width|| +2<=this.bounds.height)&&this.textarea.innerHTML!=this.getEmptyLabelText()?(this.textarea.style.wordWrap=mxConstants.WORD_WRAP,this.textarea.style.whiteSpace="normal","fill"!=a.style[mxConstants.STYLE_OVERFLOW]&&(this.textarea.style.width=Math.round(this.bounds.width/c)+this.wordWrapPadding+"px")):(this.textarea.style.whiteSpace="nowrap","fill"!=a.style[mxConstants.STYLE_OVERFLOW]&&(this.textarea.style.width=""));mxClient.IS_VML?this.textarea.style.zoom=c:(mxUtils.setPrefixedStyle(this.textarea.style, +"transformOrigin","0px 0px"),mxUtils.setPrefixedStyle(this.textarea.style,"transform","scale("+c+","+c+")"+(null==d?"":" translate("+100*d.x+"%,"+100*d.y+"%)")))}};mxCellEditor.prototype.focusLost=function(){this.stopEditing(!this.graph.isInvokesStopCellEditing())};mxCellEditor.prototype.getBackgroundColor=function(a){return null}; +mxCellEditor.prototype.isLegacyEditor=function(){if(mxClient.IS_VML)return!0;var a=!1;if(mxClient.IS_SVG){var b=this.graph.view.getDrawPane().ownerSVGElement;null!=b&&(a="absolute"==mxUtils.getCurrentStyle(b).position)}return!a}; +mxCellEditor.prototype.startEditing=function(a,b){this.stopEditing(!0);null==this.textarea&&this.init();null!=this.graph.tooltipHandler&&this.graph.tooltipHandler.hideTooltip();var c=this.graph.getView().getState(a);if(null!=c){this.graph.getView();var d=mxUtils.getValue(c.style,mxConstants.STYLE_FONTSIZE,mxConstants.DEFAULT_FONTSIZE),e=mxUtils.getValue(c.style,mxConstants.STYLE_FONTFAMILY,mxConstants.DEFAULT_FONTFAMILY),f=mxUtils.getValue(c.style,mxConstants.STYLE_FONTCOLOR,"black"),g=mxUtils.getValue(c.style, +mxConstants.STYLE_ALIGN,mxConstants.ALIGN_LEFT),k=(mxUtils.getValue(c.style,mxConstants.STYLE_FONTSTYLE,0)&mxConstants.FONT_BOLD)==mxConstants.FONT_BOLD,l=(mxUtils.getValue(c.style,mxConstants.STYLE_FONTSTYLE,0)&mxConstants.FONT_ITALIC)==mxConstants.FONT_ITALIC,m=(mxUtils.getValue(c.style,mxConstants.STYLE_FONTSTYLE,0)&mxConstants.FONT_UNDERLINE)==mxConstants.FONT_UNDERLINE;this.textarea.style.lineHeight=mxConstants.ABSOLUTE_LINE_HEIGHT?Math.round(d*mxConstants.LINE_HEIGHT)+"px":mxConstants.LINE_HEIGHT; +this.textarea.style.backgroundColor=this.getBackgroundColor(c);this.textarea.style.textDecoration=m?"underline":"";this.textarea.style.fontWeight=k?"bold":"normal";this.textarea.style.fontStyle=l?"italic":"";this.textarea.style.fontSize=Math.round(d)+"px";this.textarea.style.zIndex=this.zIndex;this.textarea.style.fontFamily=e;this.textarea.style.textAlign=g;this.textarea.style.outline="none";this.textarea.style.color=f;d=this.textDirection=mxUtils.getValue(c.style,mxConstants.STYLE_TEXT_DIRECTION, +mxConstants.DEFAULT_TEXT_DIRECTION);d==mxConstants.TEXT_DIRECTION_AUTO&&(null==c||null==c.text||c.text.dialect==mxConstants.DIALECT_STRICTHTML||mxUtils.isNode(c.text.value)||(d=c.text.getAutoDirection()));d==mxConstants.TEXT_DIRECTION_LTR||d==mxConstants.TEXT_DIRECTION_RTL?this.textarea.setAttribute("dir",d):this.textarea.removeAttribute("dir");this.textarea.innerHTML=this.getInitialValue(c,b)||"";this.initialValue=this.textarea.innerHTML;0==this.textarea.innerHTML.length||"
"==this.textarea.innerHTML? +(this.textarea.innerHTML=this.getEmptyLabelText(),this.clearOnChange=!0):this.clearOnChange=this.textarea.innerHTML==this.getEmptyLabelText();this.graph.container.appendChild(this.textarea);this.editingCell=a;this.trigger=b;this.textNode=null;null!=c.text&&this.isHideLabel(c)&&(this.textNode=c.text.node,this.textNode.style.visibility="hidden");this.autoSize&&(this.graph.model.isEdge(c.cell)||"fill"!=c.style[mxConstants.STYLE_OVERFLOW])&&window.setTimeout(mxUtils.bind(this,function(){this.resize()}), +0);this.resize();try{this.textarea.focus(),this.isSelectText()&&0=l.x:null!=c&&(k=c.x+c.width=a.x:null!=b&&(l=b.x+b.widthb.x+b.width?null!=c?(d=c.x,m=Math.max(Math.abs(l-c.y),m)):a==mxConstants.DIRECTION_NORTH? +l=b.y-2*k:a==mxConstants.DIRECTION_SOUTH?l=b.y+b.height+2*k:d=a==mxConstants.DIRECTION_EAST?b.x-2*m:b.x+b.width+2*m:null!=c&&(d=f.getRoutingCenterX(b),k=Math.max(Math.abs(d-c.x),m),l=c.y,m=0);e.push(new mxPoint(d-k,l-m));e.push(new mxPoint(d+k,l+m))}},ElbowConnector:function(a,b,c,d,e){var f=null!=d&&0n,k=f.xm;else l=Math.max(b.x,c.x),m=Math.min(b.x+b.width,c.x+c.width),g=l==m,g||(k=Math.max(b.y,c.y),n=Math.min(b.y+b.height,c.y+c.height),k=k==n);k||!g&&a.style[mxConstants.STYLE_ELBOW]!=mxConstants.ELBOW_VERTICAL?mxEdgeStyle.SideToSide(a,b,c,d,e):mxEdgeStyle.TopToBottom(a,b,c,d,e)},SideToSide:function(a,b,c,d,e){var f=a.view;d=null!=d&&0=b.y&&d.y<=b.y+b.height&&(k=d.y),d.y>=c.y&&d.y<=c.y+c.height&&(f=d.y)),mxUtils.contains(c,a,k)||mxUtils.contains(b,a,k)||e.push(new mxPoint(a,k)),mxUtils.contains(c,a,f)||mxUtils.contains(b,a,f)||e.push(new mxPoint(a,f)),1==e.length&&(null!=d?mxUtils.contains(c,a, +d.y)||mxUtils.contains(b,a,d.y)||e.push(new mxPoint(a,d.y)):(f=Math.max(b.y,c.y),e.push(new mxPoint(a,f+(Math.min(b.y+b.height,c.y+c.height)-f)/2)))))},TopToBottom:function(a,b,c,d,e){var f=a.view;d=null!=d&&0=b.x&&d.x<=b.x+b.width&&(a=d.x),k=null!=d?d.y:Math.round(g+(k-g)/2),mxUtils.contains(c,a,k)||mxUtils.contains(b,a,k)||e.push(new mxPoint(a,k)),a=null!=d&&d.x>=c.x&&d.x<=c.x+c.width?d.x:f.getRoutingCenterX(c),mxUtils.contains(c,a,k)||mxUtils.contains(b,a,k)||e.push(new mxPoint(a,k)),1==e.length&&(null!=d&&1==e.length?mxUtils.contains(c,d.x,k)||mxUtils.contains(b,d.x,k)||e.push(new mxPoint(d.x,k)):(f=Math.max(b.x,c.x),e.push(new mxPoint(f+(Math.min(b.x+b.width,c.x+c.width)-f)/2,k)))))}, +SegmentConnector:function(a,b,c,d,e){function f(a){if(null==l||Math.abs(l.x-a.x)>=k||Math.abs(l.y-a.y)>=k)e.push(a),l=a;return l}var g=a.absolutePoints,k=Math.max(1,a.view.scale),l=0r;r++){var z=null!=x&&x.x==y.x,v=null!=x&&x.y==y.y,B=null!=u&&y.y>=u.y&&y.y<=u.y+ +u.height,u=null!=u&&y.x>=u.x&&y.x<=u.x+u.width,y=v||null==x&&B,A=z||null==x&&u;if(0!=r||!(y&&A||z&&v)){if(null!=x&&!v&&!z&&(B||u)){m=B?!1:!0;break}if(A||y){m=y;1==r&&(m=0==d.length%2?y:A);break}}u=c;x=g[q];null!=x&&(x.x=Math.round(x.x),x.y=Math.round(x.y),u=null);y=d[d.length-1];z&&v&&(d=d.slice(1))}m&&(null!=g[0]&&g[0].y!=n.y||null==g[0]&&null!=b&&(n.yb.y+b.height))?f(new mxPoint(p.x,n.y)):!m&&(null!=g[0]&&g[0].x!=n.x||null==g[0]&&null!=b&&(n.xb.x+b.width))&&f(new mxPoint(n.x, +p.y));m?p.y=n.y:p.x=n.x;for(r=0;rc.y+c.height))?f(new mxPoint(p.x,n.y)):!m&&(null!=g[q]&&g[q].x!=n.x||null==g[q]&&null!=c&&(n.xc.x+c.width))&&f(new mxPoint(n.x,p.y))));if(null==g[0]&&null!=b)for(;1< +e.length&&null!=e[1]&&mxUtils.contains(b,e[1].x,e[1].y);)e.splice(1,1);if(null==g[q]&&null!=c)for(;1v;v++)mxEdgeStyle.limits[v][1]= +n[v][0]-A[v],mxEdgeStyle.limits[v][2]=n[v][1]-A[v],mxEdgeStyle.limits[v][4]=n[v][0]+n[v][2]+A[v],mxEdgeStyle.limits[v][8]=n[v][1]+n[v][3]+A[v];A=n[0][1]+n[0][3]/2;p=n[1][1]+n[1][3]/2;v=n[0][0]+n[0][2]/2-(n[1][0]+n[1][2]/2);B=A-p;A=0;0>v?A=0>B?2:1:0>=B&&(A=3,0==v&&(A=2));p=null;null!=b&&(p=l);b=[[.5,.5],[.5,.5]];for(v=0;2>v;v++)null!=p&&(b[v][0]=(p.x-n[v][0])/n[v][2],1>=Math.abs(p.x-n[v][0])?a[v]=mxConstants.DIRECTION_MASK_WEST:1>=Math.abs(p.x-n[v][0]-n[v][2])&&(a[v]=mxConstants.DIRECTION_MASK_EAST), +b[v][1]=(p.y-n[v][1])/n[v][3],1>=Math.abs(p.y-n[v][1])?a[v]=mxConstants.DIRECTION_MASK_NORTH:1>=Math.abs(p.y-n[v][1]-n[v][3])&&(a[v]=mxConstants.DIRECTION_MASK_SOUTH)),p=null,null!=c&&(p=m);v=n[0][1]-(n[1][1]+n[1][3]);m=n[0][0]-(n[1][0]+n[1][2]);p=n[1][1]-(n[0][1]+n[0][3]);q=n[1][0]-(n[0][0]+n[0][2]);mxEdgeStyle.vertexSeperations[1]=Math.max(m-z,0);mxEdgeStyle.vertexSeperations[2]=Math.max(v-z,0);mxEdgeStyle.vertexSeperations[4]=Math.max(p-z,0);mxEdgeStyle.vertexSeperations[3]=Math.max(q-z,0);z=[]; +c=[];l=[];c[0]=m>=q?mxConstants.DIRECTION_MASK_WEST:mxConstants.DIRECTION_MASK_EAST;l[0]=v>=p?mxConstants.DIRECTION_MASK_NORTH:mxConstants.DIRECTION_MASK_SOUTH;c[1]=mxUtils.reversePortConstraints(c[0]);l[1]=mxUtils.reversePortConstraints(l[0]);m=m>=q?m:q;p=v>=p?v:p;q=[[0,0],[0,0]];r=!1;for(v=0;2>v;v++)0==a[v]&&(0==(c[v]&d[v])&&(c[v]=mxUtils.reversePortConstraints(c[v])),0==(l[v]&d[v])&&(l[v]=mxUtils.reversePortConstraints(l[v])),q[v][0]=l[v],q[v][1]=c[v]);0v;v++)0==a[v]&&(0==(q[v][0]&d[v])&&(q[v][0]=q[v][1]),z[v]=q[v][0]&d[v],z[v]|=(q[v][1]&d[v])<<8,z[v]|=(q[1-v][v]&d[v])<<16,z[v]|=(q[1-v][1-v]&d[v])<<24,0==(z[v]&15)&&(z[v]<<=8),0==(z[v]&3840)&&(z[v]=z[v]&15|z[v]>>8),0==(z[v]& +983040)&&(z[v]=z[v]&65535|(z[v]&251658240)>>8),a[v]=z[v]&15,d[v]==mxConstants.DIRECTION_MASK_WEST||d[v]==mxConstants.DIRECTION_MASK_NORTH||d[v]==mxConstants.DIRECTION_MASK_EAST||d[v]==mxConstants.DIRECTION_MASK_SOUTH)&&(a[v]=d[v]);d=a[0]==mxConstants.DIRECTION_MASK_EAST?3:a[0];z=a[1]==mxConstants.DIRECTION_MASK_EAST?3:a[1];d-=A;z-=A;1>d&&(d+=4);1>z&&(z+=4);d=mxEdgeStyle.routePatterns[d-1][z-1];mxEdgeStyle.wayPoints1[0][0]=n[0][0];mxEdgeStyle.wayPoints1[0][1]=n[0][1];switch(a[0]){case mxConstants.DIRECTION_MASK_WEST:mxEdgeStyle.wayPoints1[0][0]-= +f;mxEdgeStyle.wayPoints1[0][1]+=b[0][1]*n[0][3];break;case mxConstants.DIRECTION_MASK_SOUTH:mxEdgeStyle.wayPoints1[0][0]+=b[0][0]*n[0][2];mxEdgeStyle.wayPoints1[0][1]+=n[0][3]+f;break;case mxConstants.DIRECTION_MASK_EAST:mxEdgeStyle.wayPoints1[0][0]+=n[0][2]+f;mxEdgeStyle.wayPoints1[0][1]+=b[0][1]*n[0][3];break;case mxConstants.DIRECTION_MASK_NORTH:mxEdgeStyle.wayPoints1[0][0]+=b[0][0]*n[0][2],mxEdgeStyle.wayPoints1[0][1]-=f}f=0;c=z=0<(a[0]&(mxConstants.DIRECTION_MASK_EAST|mxConstants.DIRECTION_MASK_WEST))? +0:1;for(v=0;v>5,p<<=A,15>=4),q=0<(d[v]&mxEdgeStyle.CENTER_MASK),(u||t)&&9>p?(r=u?0:1,p=q&&0==l?n[r][0]+b[r][0]*n[r][2]:q?n[r][1]+b[r][1]* +n[r][3]:mxEdgeStyle.limits[r][p],0==l?(p=(p-mxEdgeStyle.wayPoints1[f][0])*m[0],0e&&(e+=4);1>a&&(a+=4);b=routePatterns[e-1][a-1];0!=c&&0!=d||null==inlineRoutePatterns[e-1][a- +1]||(b=inlineRoutePatterns[e-1][a-1]);return b}},mxStyleRegistry={values:[],putValue:function(a,b){mxStyleRegistry.values[a]=b},getValue:function(a){return mxStyleRegistry.values[a]},getName:function(a){for(var b in mxStyleRegistry.values)if(mxStyleRegistry.values[b]==a)return b;return null}};mxStyleRegistry.putValue(mxConstants.EDGESTYLE_ELBOW,mxEdgeStyle.ElbowConnector);mxStyleRegistry.putValue(mxConstants.EDGESTYLE_ENTITY_RELATION,mxEdgeStyle.EntityRelation); +mxStyleRegistry.putValue(mxConstants.EDGESTYLE_LOOP,mxEdgeStyle.Loop);mxStyleRegistry.putValue(mxConstants.EDGESTYLE_SIDETOSIDE,mxEdgeStyle.SideToSide);mxStyleRegistry.putValue(mxConstants.EDGESTYLE_TOPTOBOTTOM,mxEdgeStyle.TopToBottom);mxStyleRegistry.putValue(mxConstants.EDGESTYLE_ORTHOGONAL,mxEdgeStyle.OrthConnector);mxStyleRegistry.putValue(mxConstants.EDGESTYLE_SEGMENT,mxEdgeStyle.SegmentConnector);mxStyleRegistry.putValue(mxConstants.PERIMETER_ELLIPSE,mxPerimeter.EllipsePerimeter); +mxStyleRegistry.putValue(mxConstants.PERIMETER_RECTANGLE,mxPerimeter.RectanglePerimeter);mxStyleRegistry.putValue(mxConstants.PERIMETER_RHOMBUS,mxPerimeter.RhombusPerimeter);mxStyleRegistry.putValue(mxConstants.PERIMETER_TRIANGLE,mxPerimeter.TrianglePerimeter);mxStyleRegistry.putValue(mxConstants.PERIMETER_HEXAGON,mxPerimeter.HexagonPerimeter);function mxGraphView(a){this.graph=a;this.translate=new mxPoint;this.graphBounds=new mxRectangle;this.states=new mxDictionary}mxGraphView.prototype=new mxEventSource; +mxGraphView.prototype.constructor=mxGraphView;mxGraphView.prototype.EMPTY_POINT=new mxPoint;mxGraphView.prototype.doneResource="none"!=mxClient.language?"done":"";mxGraphView.prototype.updatingDocumentResource="none"!=mxClient.language?"updatingDocument":"";mxGraphView.prototype.allowEval=!1;mxGraphView.prototype.captureDocumentGesture=!0;mxGraphView.prototype.optimizeVmlReflows=!0;mxGraphView.prototype.rendering=!0;mxGraphView.prototype.graph=null;mxGraphView.prototype.currentRoot=null; +mxGraphView.prototype.graphBounds=null;mxGraphView.prototype.scale=1;mxGraphView.prototype.translate=null;mxGraphView.prototype.states=null;mxGraphView.prototype.updateStyle=!1;mxGraphView.prototype.lastNode=null;mxGraphView.prototype.lastHtmlNode=null;mxGraphView.prototype.lastForegroundNode=null;mxGraphView.prototype.lastForegroundHtmlNode=null;mxGraphView.prototype.getGraphBounds=function(){return this.graphBounds};mxGraphView.prototype.setGraphBounds=function(a){this.graphBounds=a}; +mxGraphView.prototype.getBounds=function(a){var b=null;if(null!=a&&0 +c.length||null==c[0]||null==c[c.length-1])?this.clear(a.cell,!0):(this.updateEdgeBounds(a),this.updateEdgeLabelOffset(a)))}; +mxGraphView.prototype.updateVertexLabelOffset=function(a){var b=mxUtils.getValue(a.style,mxConstants.STYLE_LABEL_POSITION,mxConstants.ALIGN_CENTER);if(b==mxConstants.ALIGN_LEFT)b=mxUtils.getValue(a.style,mxConstants.STYLE_LABEL_WIDTH,null),b=null!=b?b*this.scale:a.width,a.absoluteOffset.x-=b;else if(b==mxConstants.ALIGN_RIGHT)a.absoluteOffset.x+=a.width;else if(b==mxConstants.ALIGN_CENTER&&(b=mxUtils.getValue(a.style,mxConstants.STYLE_LABEL_WIDTH,null),null!=b)){var c=mxUtils.getValue(a.style,mxConstants.STYLE_ALIGN, +mxConstants.ALIGN_CENTER),d=0;c==mxConstants.ALIGN_CENTER?d=.5:c==mxConstants.ALIGN_RIGHT&&(d=1);0!=d&&(a.absoluteOffset.x-=(b*this.scale-a.width)*d)}b=mxUtils.getValue(a.style,mxConstants.STYLE_VERTICAL_LABEL_POSITION,mxConstants.ALIGN_MIDDLE);b==mxConstants.ALIGN_TOP?a.absoluteOffset.y-=a.height:b==mxConstants.ALIGN_BOTTOM&&(a.absoluteOffset.y+=a.height)};mxGraphView.prototype.resetValidationState=function(){this.lastForegroundHtmlNode=this.lastForegroundNode=this.lastHtmlNode=this.lastNode=null}; +mxGraphView.prototype.stateValidated=function(a){var b=this.graph.getModel().isEdge(a.cell)&&this.graph.keepEdgesInForeground||this.graph.getModel().isVertex(a.cell)&&this.graph.keepEdgesInBackground;a=this.graph.cellRenderer.insertStateAfter(a,b?this.lastForegroundNode||this.lastNode:this.lastNode,b?this.lastForegroundHtmlNode||this.lastHtmlNode:this.lastHtmlNode);b?(this.lastForegroundHtmlNode=a[1],this.lastForegroundNode=a[0]):(this.lastHtmlNode=a[1],this.lastNode=a[0])}; +mxGraphView.prototype.updateFixedTerminalPoints=function(a,b,c){this.updateFixedTerminalPoint(a,b,!0,this.graph.getConnectionConstraint(a,b,!0));this.updateFixedTerminalPoint(a,c,!1,this.graph.getConnectionConstraint(a,c,!1))};mxGraphView.prototype.updateFixedTerminalPoint=function(a,b,c,d){a.setAbsoluteTerminalPoint(this.getFixedTerminalPoint(a,b,c,d),c)}; +mxGraphView.prototype.getFixedTerminalPoint=function(a,b,c,d){var e=null;null!=d&&(e=this.graph.getConnectionPoint(b,d));if(null==e&&null==b){b=this.scale;d=this.translate;var f=a.origin,e=this.graph.getCellGeometry(a.cell).getTerminalPoint(c);null!=e&&(e=new mxPoint(b*(d.x+e.x+f.x),b*(d.y+e.y+f.y)))}return e}; +mxGraphView.prototype.updateBoundsFromStencil=function(a){var b=null;if(null!=a&&null!=a.shape&&null!=a.shape.stencil&&"fixed"==a.shape.stencil.aspect){var b=mxRectangle.fromRectangle(a),c=a.shape.stencil.computeAspect(a.style,a.x,a.y,a.width,a.height);a.setRect(c.x,c.y,a.shape.stencil.w0*c.width,a.shape.stencil.h0*c.height)}return b}; +mxGraphView.prototype.updatePoints=function(a,b,c,d){if(null!=a){var e=[];e.push(a.absolutePoints[0]);var f=this.getEdgeStyle(a,b,c,d);if(null!=f){c=this.getTerminalPort(a,c,!0);d=this.getTerminalPort(a,d,!1);var g=this.updateBoundsFromStencil(c),k=this.updateBoundsFromStencil(d);f(a,c,d,b,e);null!=g&&c.setRect(g.x,g.y,g.width,g.height);null!=k&&d.setRect(k.x,k.y,k.width,k.height)}else if(null!=b)for(f=0;fb.length)||mxUtils.getValue(a.style,mxConstants.STYLE_ORTHOGONAL_LOOP,!1)&&(null!=e&&null!=e.point||null!=f&&null!=f.point)?!1:null!=c&&c==d}; +mxGraphView.prototype.getEdgeStyle=function(a,b,c,d){a=this.isLoopStyleEnabled(a,b,c,d)?mxUtils.getValue(a.style,mxConstants.STYLE_LOOP,this.graph.defaultLoopStyle):mxUtils.getValue(a.style,mxConstants.STYLE_NOEDGESTYLE,!1)?null:a.style[mxConstants.STYLE_EDGE];"string"==typeof a&&(b=mxStyleRegistry.getValue(a),null==b&&this.isAllowEval()&&(b=mxUtils.eval(a)),a=b);return"function"==typeof a?a:null}; +mxGraphView.prototype.updateFloatingTerminalPoints=function(a,b,c){var d=a.absolutePoints,e=d[0];null==d[d.length-1]&&null!=c&&this.updateFloatingTerminalPoint(a,c,b,!1);null==e&&null!=b&&this.updateFloatingTerminalPoint(a,b,c,!0)};mxGraphView.prototype.updateFloatingTerminalPoint=function(a,b,c,d){a.setAbsoluteTerminalPoint(this.getFloatingTerminalPoint(a,b,c,d),d)}; +mxGraphView.prototype.getFloatingTerminalPoint=function(a,b,c,d){b=this.getTerminalPort(a,b,d);var e=this.getNextPoint(a,c,d),f=this.graph.isOrthogonal(a);c=mxUtils.toRadians(Number(b.style[mxConstants.STYLE_ROTATION]||"0"));var g=new mxPoint(b.getCenterX(),b.getCenterY());if(0!=c)var k=Math.cos(-c),l=Math.sin(-c),e=mxUtils.getRotatedPoint(e,k,l,g);k=parseFloat(a.style[mxConstants.STYLE_PERIMETER_SPACING]||0);k+=parseFloat(a.style[d?mxConstants.STYLE_SOURCE_PERIMETER_SPACING:mxConstants.STYLE_TARGET_PERIMETER_SPACING]|| +0);a=this.getPerimeterPoint(b,e,0==c&&f,k);0!=c&&(k=Math.cos(c),l=Math.sin(c),a=mxUtils.getRotatedPoint(a,k,l,g));return a};mxGraphView.prototype.getTerminalPort=function(a,b,c){a=mxUtils.getValue(a.style,c?mxConstants.STYLE_SOURCE_PORT:mxConstants.STYLE_TARGET_PORT);null!=a&&(a=this.getState(this.graph.getModel().getCell(a)),null!=a&&(b=a));return b}; +mxGraphView.prototype.getPerimeterPoint=function(a,b,c,d){var e=null;if(null!=a){var f=this.getPerimeterFunction(a);if(null!=f&&null!=b&&(d=this.getPerimeterBounds(a,d),0=Math.round(k+g)&&l=f?0:f*f/(a*a+m*m));a>e&&(a=e);e=Math.sqrt(mxUtils.ptSegDistSq(g.x,g.y,k.x,k.y,b,c));-1==mxUtils.relativeCcw(g.x,g.y,k.x,k.y,b,c)&&(e=-e);return new mxPoint((d/2-p-a)/d*-2,e/this.scale)}}return new mxPoint}; +mxGraphView.prototype.updateEdgeLabelOffset=function(a){var b=a.absolutePoints;a.absoluteOffset.x=a.getCenterX();a.absoluteOffset.y=a.getCenterY();if(null!=b&&0c&&a.x>c+2&&a.x<=b)return!0;b=this.graph.container.offsetHeight;c=this.graph.container.clientHeight;return b>c&&a.y>c+2&&a.y<=b?!0:!1}; +mxGraphView.prototype.init=function(){this.installListeners();var a=this.graph;a.dialect==mxConstants.DIALECT_SVG?this.createSvg():a.dialect==mxConstants.DIALECT_VML?this.createVml():this.createHtml()}; +mxGraphView.prototype.installListeners=function(){var a=this.graph,b=a.container;if(null!=b){mxClient.IS_TOUCH&&(mxEvent.addListener(b,"gesturestart",mxUtils.bind(this,function(b){a.fireGestureEvent(b);mxEvent.consume(b)})),mxEvent.addListener(b,"gesturechange",mxUtils.bind(this,function(b){a.fireGestureEvent(b);mxEvent.consume(b)})),mxEvent.addListener(b,"gestureend",mxUtils.bind(this,function(b){a.fireGestureEvent(b);mxEvent.consume(b)})));mxEvent.addGestureListeners(b,mxUtils.bind(this,function(b){!this.isContainerEvent(b)|| +(mxClient.IS_IE||mxClient.IS_IE11||mxClient.IS_GC||mxClient.IS_OP||mxClient.IS_SF)&&this.isScrollEvent(b)||a.fireMouseEvent(mxEvent.MOUSE_DOWN,new mxMouseEvent(b))}),mxUtils.bind(this,function(b){this.isContainerEvent(b)&&a.fireMouseEvent(mxEvent.MOUSE_MOVE,new mxMouseEvent(b))}),mxUtils.bind(this,function(b){this.isContainerEvent(b)&&a.fireMouseEvent(mxEvent.MOUSE_UP,new mxMouseEvent(b))}));mxEvent.addListener(b,"dblclick",mxUtils.bind(this,function(b){this.isContainerEvent(b)&&a.dblClick(b)})); +var c=function(c){var d=null;mxClient.IS_TOUCH&&(d=mxEvent.getClientX(c),c=mxEvent.getClientY(c),c=mxUtils.convertPoint(b,d,c),d=a.view.getState(a.getCellAt(c.x,c.y)));return d};a.addMouseListener({mouseDown:function(b,c){a.popupMenuHandler.hideMenu()},mouseMove:function(){},mouseUp:function(){}});this.moveHandler=mxUtils.bind(this,function(b){null!=a.tooltipHandler&&a.tooltipHandler.isHideOnHover()&&a.tooltipHandler.hide();this.captureDocumentGesture&&a.isMouseDown&&null!=a.container&&!this.isContainerEvent(b)&& +"none"!=a.container.style.display&&"hidden"!=a.container.style.visibility&&!mxEvent.isConsumed(b)&&a.fireMouseEvent(mxEvent.MOUSE_MOVE,new mxMouseEvent(b,c(b)))});this.endHandler=mxUtils.bind(this,function(b){this.captureDocumentGesture&&a.isMouseDown&&null!=a.container&&!this.isContainerEvent(b)&&"none"!=a.container.style.display&&"hidden"!=a.container.style.visibility&&a.fireMouseEvent(mxEvent.MOUSE_UP,new mxMouseEvent(b))});mxEvent.addGestureListeners(document,null,this.moveHandler,this.endHandler)}}; +mxGraphView.prototype.createHtml=function(){var a=this.graph.container;null!=a&&(this.canvas=this.createHtmlPane("100%","100%"),this.canvas.style.overflow="hidden",this.backgroundPane=this.createHtmlPane("1px","1px"),this.drawPane=this.createHtmlPane("1px","1px"),this.overlayPane=this.createHtmlPane("1px","1px"),this.decoratorPane=this.createHtmlPane("1px","1px"),this.canvas.appendChild(this.backgroundPane),this.canvas.appendChild(this.drawPane),this.canvas.appendChild(this.overlayPane),this.canvas.appendChild(this.decoratorPane), +a.appendChild(this.canvas),this.updateContainerStyle(a),mxClient.IS_QUIRKS&&(a=mxUtils.bind(this,function(a){a=this.getGraphBounds();this.updateHtmlCanvasSize(a.x+a.width+this.graph.border,a.y+a.height+this.graph.border)}),mxEvent.addListener(window,"resize",a)))}; +mxGraphView.prototype.updateHtmlCanvasSize=function(a,b){if(null!=this.graph.container){var c=this.graph.container.offsetHeight;this.canvas.style.width=this.graph.container.offsetWidth")}; +mxGraph.prototype.createHandlers=function(){this.tooltipHandler=this.createTooltipHandler();this.tooltipHandler.setEnabled(!1);this.selectionCellsHandler=this.createSelectionCellsHandler();this.connectionHandler=this.createConnectionHandler();this.connectionHandler.setEnabled(!1);this.graphHandler=this.createGraphHandler();this.panningHandler=this.createPanningHandler();this.panningHandler.panningEnabled=!1;this.popupMenuHandler=this.createPopupMenuHandler()}; +mxGraph.prototype.createTooltipHandler=function(){return new mxTooltipHandler(this)};mxGraph.prototype.createSelectionCellsHandler=function(){return new mxSelectionCellsHandler(this)};mxGraph.prototype.createConnectionHandler=function(){return new mxConnectionHandler(this)};mxGraph.prototype.createGraphHandler=function(){return new mxGraphHandler(this)};mxGraph.prototype.createPanningHandler=function(){return new mxPanningHandler(this)};mxGraph.prototype.createPopupMenuHandler=function(){return new mxPopupMenuHandler(this)}; +mxGraph.prototype.createSelectionModel=function(){return new mxGraphSelectionModel(this)};mxGraph.prototype.createStylesheet=function(){return new mxStylesheet};mxGraph.prototype.createGraphView=function(){return new mxGraphView(this)};mxGraph.prototype.createCellRenderer=function(){return new mxCellRenderer};mxGraph.prototype.createCellEditor=function(){return new mxCellEditor(this)};mxGraph.prototype.getModel=function(){return this.model};mxGraph.prototype.getView=function(){return this.view}; +mxGraph.prototype.getStylesheet=function(){return this.stylesheet};mxGraph.prototype.setStylesheet=function(a){this.stylesheet=a};mxGraph.prototype.getSelectionModel=function(){return this.selectionModel};mxGraph.prototype.setSelectionModel=function(a){this.selectionModel=a}; +mxGraph.prototype.getSelectionCellsForChanges=function(a){for(var b=[],c=0;cmxUtils.indexOf(b,e)&&b.push(e)}}return this.getModel().getTopmostCells(b)}; +mxGraph.prototype.graphModelChanged=function(a){for(var b=0;b"+b+""),d&&b.addListener(mxEvent.CLICK,mxUtils.bind(this,function(b,c){this.isEnabled()&&this.setSelectionCell(a)})),this.addCellOverlay(a,b);this.removeCellOverlays(a);return null};mxGraph.prototype.startEditing=function(a){this.startEditingAtCell(null,a)}; +mxGraph.prototype.startEditingAtCell=function(a,b){null!=b&&mxEvent.isMultiTouchEvent(b)||(null==a&&(a=this.getSelectionCell(),null==a||this.isCellEditable(a)||(a=null)),null!=a&&(this.fireEvent(new mxEventObject(mxEvent.START_EDITING,"cell",a,"event",b)),this.cellEditor.startEditing(a,b),this.fireEvent(new mxEventObject(mxEvent.EDITING_STARTED,"cell",a,"event",b))))};mxGraph.prototype.getEditingValue=function(a,b){return this.convertValueToString(a)}; +mxGraph.prototype.stopEditing=function(a){this.cellEditor.stopEditing(a);this.fireEvent(new mxEventObject(mxEvent.EDITING_STOPPED,"cancel",a))};mxGraph.prototype.labelChanged=function(a,b,c){this.model.beginUpdate();try{var d=a.value;this.cellLabelChanged(a,b,this.isAutoSizeCell(a));this.fireEvent(new mxEventObject(mxEvent.LABEL_CHANGED,"cell",a,"value",b,"old",d,"event",c))}finally{this.model.endUpdate()}return a}; +mxGraph.prototype.cellLabelChanged=function(a,b,c){this.model.beginUpdate();try{this.model.setValue(a,b),c&&this.cellSizeUpdated(a,!1)}finally{this.model.endUpdate()}};mxGraph.prototype.escape=function(a){this.fireEvent(new mxEventObject(mxEvent.ESCAPE,"event",a))}; +mxGraph.prototype.click=function(a){var b=a.getEvent(),c=a.getCell(),d=new mxEventObject(mxEvent.CLICK,"event",b,"cell",c);a.isConsumed()&&d.consume();this.fireEvent(d);if(this.isEnabled()&&!mxEvent.isConsumed(b)&&!d.isConsumed())if(null!=c){if(this.isTransparentClickEvent(b)){var e=!1;a=this.getCellAt(a.graphX,a.graphY,null,null,null,mxUtils.bind(this,function(a){a=this.isCellSelected(a.cell);e=e||a;return!e||a}));null!=a&&(c=a)}this.selectCellForEvent(c,b)}else c=null,this.isSwimlaneSelectionEnabled()&& +(c=this.getSwimlaneAt(a.getGraphX(),a.getGraphY())),null!=c?this.selectCellForEvent(c,b):this.isToggleEvent(b)||this.clearSelection()};mxGraph.prototype.dblClick=function(a,b){var c=new mxEventObject(mxEvent.DOUBLE_CLICK,"event",a,"cell",b);this.fireEvent(c);!this.isEnabled()||mxEvent.isConsumed(a)||c.isConsumed()||null==b||!this.isCellEditable(b)||this.isEditing(b)||(this.startEditingAtCell(b,a),mxEvent.consume(a))}; +mxGraph.prototype.tapAndHold=function(a){var b=a.getEvent(),c=new mxEventObject(mxEvent.TAP_AND_HOLD,"event",b,"cell",a.getCell());this.fireEvent(c);c.isConsumed()&&(this.panningHandler.panningTrigger=!1);this.isEnabled()&&!mxEvent.isConsumed(b)&&!c.isConsumed()&&this.connectionHandler.isEnabled()&&(b=this.view.getState(this.connectionHandler.marker.getCell(a)),null!=b&&(this.connectionHandler.marker.currentColor=this.connectionHandler.marker.validColor,this.connectionHandler.marker.markedState=b, +this.connectionHandler.marker.mark(),this.connectionHandler.first=new mxPoint(a.getGraphX(),a.getGraphY()),this.connectionHandler.edgeState=this.connectionHandler.createEdgeState(a),this.connectionHandler.previous=b,this.connectionHandler.fireEvent(new mxEventObject(mxEvent.START,"state",this.connectionHandler.previous))))}; +mxGraph.prototype.scrollPointToVisible=function(a,b,c,d){if(this.timerAutoScroll||!this.ignoreScrollbars&&!mxUtils.hasScrollbars(this.container))this.allowAutoPanning&&!this.panningHandler.isActive()&&(null==this.panningManager&&(this.panningManager=this.createPanningManager()),this.panningManager.panTo(a+this.panDx,b+this.panDy));else{var e=this.container;d=null!=d?d:20;if(a>=e.scrollLeft&&b>=e.scrollTop&&a<=e.scrollLeft+e.clientWidth&&b<=e.scrollTop+e.clientHeight){var f=e.scrollLeft+e.clientWidth- +a;if(fthis.minPageBreakDist)?Math.ceil(d.height/f.height)+1:0,k=a?Math.ceil(d.width/f.width)+1:0,l=(k-1)*f.width,m=(g-1)*f.height;null==this.horizontalPageBreaks&&0this.model.getChildCount(b)&&c--;this.model.add(b,a[l],c+l);this.autoSizeCellsOnAdd&&this.autoSizeCell(a[l],!0);(null==k||k)&&this.isExtendParentsOnAdd(a[l])&&this.isExtendParent(a[l])&&this.extendParent(a[l]);(null==g||g)&&this.constrainChild(a[l]);null!=d&&this.cellConnected(a[l],d,!0);null!=e&&this.cellConnected(a[l],e,!1)}this.fireEvent(new mxEventObject(mxEvent.CELLS_ADDED,"cells",a,"parent",b,"index",c,"source", +d,"target",e,"absolute",f))}finally{this.model.endUpdate()}}};mxGraph.prototype.autoSizeCell=function(a,b){if(null!=b?b:1)for(var c=this.model.getChildCount(a),d=0;d"),e=mxUtils.getSizeForString(f,e,d[mxConstants.STYLE_FONTFAMILY]),c=e.width+b,a=e.height+a,mxUtils.getValue(d,mxConstants.STYLE_HORIZONTAL,!0)||(d=a,a=c,c=d),this.gridEnabled&&(c=this.snap(c+this.gridSize/2),a=this.snap(a+this.gridSize/2)),b=new mxRectangle(0,0,c,a)):(d=4*this.gridSize,b=new mxRectangle(0,0,d,d))}}return b};mxGraph.prototype.resizeCell=function(a,b,c){return this.resizeCells([a],[b],c)[0]}; +mxGraph.prototype.resizeCells=function(a,b,c){c=null!=c?c:this.isRecursiveResize();this.model.beginUpdate();try{this.cellsResized(a,b,c),this.fireEvent(new mxEventObject(mxEvent.RESIZE_CELLS,"cells",a,"bounds",b))}finally{this.model.endUpdate()}return a}; +mxGraph.prototype.cellsResized=function(a,b,c){c=null!=c?c:!1;if(null!=a&&null!=b&&a.length==b.length){this.model.beginUpdate();try{for(var d=0;de.width&&(f=c.width-e.width,c.width-=f);d.x+d.width>e.x+e.width&&(f-=d.x+d.width-e.x-e.width-f);g=0;c.height>e.height&&(g=c.height-e.height,c.height-=g);d.y+d.height> +e.y+e.height&&(g-=d.y+d.height-e.y-e.height-g);d.xf&&(n=0),b>g&&(p=0),this.view.setTranslate(Math.floor(n/2-k.x),Math.floor(p/2-k.y)),this.container.scrollLeft=(a-f)/ +2,this.container.scrollTop=(b-g)/2):this.view.setTranslate(a?Math.floor(l.x-k.x*m+n*c/m):l.x,b?Math.floor(l.y-k.y*m+p*d/m):l.y)}; +mxGraph.prototype.zoom=function(a,b){b=null!=b?b:this.centerZoom;var c=Math.round(this.view.scale*a*100)/100,d=this.view.getState(this.getSelectionCell());a=c/this.view.scale;if(this.keepSelectionVisibleOnZoom&&null!=d)d=new mxRectangle(d.x*a,d.y*a,d.width*a,d.height*a),this.view.scale=c,this.scrollRectToVisible(d)||(this.view.revalidate(),this.view.setScale(c));else if(d=mxUtils.hasScrollbars(this.container),b&&!d){var d=this.container.offsetWidth,e=this.container.offsetHeight;if(1b?(b=a.height/b,c=(b-a.height)/2,a.height=b,a.y-=Math.min(a.y,c),d=Math.min(this.container.scrollHeight,a.y+a.height),a.height=d-a.y):(b*=a.width,c=(b-a.width)/2,a.width=b,a.x-=Math.min(a.x,c),c=Math.min(this.container.scrollWidth, +a.x+a.width),a.width=c-a.x);b=this.container.clientWidth/a.width;c=this.view.scale*b;mxUtils.hasScrollbars(this.container)?(this.view.setScale(c),this.container.scrollLeft=Math.round(a.x*b),this.container.scrollTop=Math.round(a.y*b)):this.view.scaleAndTranslate(c,this.view.translate.x-a.x/this.view.scale,this.view.translate.y-a.y/this.view.scale)}; +mxGraph.prototype.scrollCellToVisible=function(a,b){var c=-this.view.translate.x,d=-this.view.translate.y,e=this.view.getState(a);null!=e&&(c=new mxRectangle(c+e.x,d+e.y,e.width,e.height),b&&null!=this.container&&(d=this.container.clientWidth,e=this.container.clientHeight,c.x=c.getCenterX()-d/2,c.width=d,c.y=c.getCenterY()-e/2,c.height=e),d=new mxPoint(this.view.translate.x,this.view.translate.y),this.scrollRectToVisible(c)&&(c=new mxPoint(this.view.translate.x,this.view.translate.y),this.view.translate.x= +d.x,this.view.translate.y=d.y,this.view.setTranslate(c.x,c.y)))}; +mxGraph.prototype.scrollRectToVisible=function(a){var b=!1;if(null!=a){var c=this.container.offsetWidth,d=this.container.offsetHeight,e=Math.min(c,a.width),f=Math.min(d,a.height);if(mxUtils.hasScrollbars(this.container)){c=this.container;a.x+=this.view.translate.x;a.y+=this.view.translate.y;var g=c.scrollLeft-a.x,d=Math.max(g-c.scrollLeft,0);0g+c&&(this.view.translate.x-=(a.x+e-c-g)/l,b=!0);a.y+f>k+d&&(this.view.translate.y-=(a.y+f-d-k)/l,b=!0);a.x")):this.setCellWarning(f,null);c=c&&null==g}d="";this.isCellCollapsed(a)&&!c&&(d+=(mxResources.get(this.containsValidationErrorsResource)||this.containsValidationErrorsResource)+"\n");d=this.model.isEdge(a)?d+ +(this.getEdgeValidationError(a,this.model.getTerminal(a,!0),this.model.getTerminal(a,!1))||""):d+(this.getCellValidationError(a)||"");e=this.validateCell(a,b);null!=e&&(d+=e);null==this.model.getParent(a)&&this.view.validate();return 0f.max||bf.max||c")),null==e&&null!=a.overlays&&a.overlays.visit(function(a,c){null!=e||b!=c.node&&b.parentNode!=c.node||(e=c.overlay.toString())}),null==e&&(c=this.selectionCellsHandler.getHandler(a.cell),null!=c&&"function"==typeof c.getTooltipForNode&&(e=c.getTooltipForNode(b))),null== +e&&(e=this.getTooltipForCell(a.cell)));return e};mxGraph.prototype.getTooltipForCell=function(a){return null!=a&&null!=a.getTooltip?a.getTooltip():this.convertValueToString(a)};mxGraph.prototype.getLinkForCell=function(a){return null};mxGraph.prototype.getCursorForMouseEvent=function(a){return this.getCursorForCell(a.getCell())};mxGraph.prototype.getCursorForCell=function(a){return null}; +mxGraph.prototype.getStartSize=function(a){var b=new mxRectangle,c=this.view.getState(a);a=null!=c?c.style:this.getCellStyle(a);null!=a&&(c=parseInt(mxUtils.getValue(a,mxConstants.STYLE_STARTSIZE,mxConstants.DEFAULT_STARTSIZE)),mxUtils.getValue(a,mxConstants.STYLE_HORIZONTAL,!0)?b.height=c:b.width=c);return b};mxGraph.prototype.getImage=function(a){return null!=a&&null!=a.style?a.style[mxConstants.STYLE_IMAGE]:null}; +mxGraph.prototype.getVerticalAlign=function(a){return null!=a&&null!=a.style?a.style[mxConstants.STYLE_VERTICAL_ALIGN]||mxConstants.ALIGN_MIDDLE:null};mxGraph.prototype.getIndicatorColor=function(a){return null!=a&&null!=a.style?a.style[mxConstants.STYLE_INDICATOR_COLOR]:null};mxGraph.prototype.getIndicatorGradientColor=function(a){return null!=a&&null!=a.style?a.style[mxConstants.STYLE_INDICATOR_GRADIENTCOLOR]:null}; +mxGraph.prototype.getIndicatorShape=function(a){return null!=a&&null!=a.style?a.style[mxConstants.STYLE_INDICATOR_SHAPE]:null};mxGraph.prototype.getIndicatorImage=function(a){return null!=a&&null!=a.style?a.style[mxConstants.STYLE_INDICATOR_IMAGE]:null};mxGraph.prototype.getBorder=function(){return this.border};mxGraph.prototype.setBorder=function(a){this.border=a}; +mxGraph.prototype.isSwimlane=function(a){if(null!=a&&this.model.getParent(a)!=this.model.getRoot()){var b=this.view.getState(a),b=null!=b?b.style:this.getCellStyle(a);if(null!=b&&!this.model.isEdge(a))return b[mxConstants.STYLE_SHAPE]==mxConstants.SHAPE_SWIMLANE}return!1};mxGraph.prototype.isResizeContainer=function(){return this.resizeContainer};mxGraph.prototype.setResizeContainer=function(a){this.resizeContainer=a};mxGraph.prototype.isEnabled=function(){return this.enabled}; +mxGraph.prototype.setEnabled=function(a){this.enabled=a};mxGraph.prototype.isEscapeEnabled=function(){return this.escapeEnabled};mxGraph.prototype.setEscapeEnabled=function(a){this.escapeEnabled=a};mxGraph.prototype.isInvokesStopCellEditing=function(){return this.invokesStopCellEditing};mxGraph.prototype.setInvokesStopCellEditing=function(a){this.invokesStopCellEditing=a};mxGraph.prototype.isEnterStopsCellEditing=function(){return this.enterStopsCellEditing}; +mxGraph.prototype.setEnterStopsCellEditing=function(a){this.enterStopsCellEditing=a};mxGraph.prototype.isCellLocked=function(a){var b=this.model.getGeometry(a);return this.isCellsLocked()||null!=b&&this.model.isVertex(a)&&b.relative};mxGraph.prototype.isCellsLocked=function(){return this.cellsLocked};mxGraph.prototype.setCellsLocked=function(a){this.cellsLocked=a};mxGraph.prototype.getCloneableCells=function(a){return this.model.filterCells(a,mxUtils.bind(this,function(a){return this.isCellCloneable(a)}))}; +mxGraph.prototype.isCellCloneable=function(a){var b=this.view.getState(a);a=null!=b?b.style:this.getCellStyle(a);return this.isCellsCloneable()&&0!=a[mxConstants.STYLE_CLONEABLE]};mxGraph.prototype.isCellsCloneable=function(){return this.cellsCloneable};mxGraph.prototype.setCellsCloneable=function(a){this.cellsCloneable=a};mxGraph.prototype.getExportableCells=function(a){return this.model.filterCells(a,mxUtils.bind(this,function(a){return this.canExportCell(a)}))}; +mxGraph.prototype.canExportCell=function(a){return this.exportEnabled};mxGraph.prototype.getImportableCells=function(a){return this.model.filterCells(a,mxUtils.bind(this,function(a){return this.canImportCell(a)}))};mxGraph.prototype.canImportCell=function(a){return this.importEnabled};mxGraph.prototype.isCellSelectable=function(a){return this.isCellsSelectable()};mxGraph.prototype.isCellsSelectable=function(){return this.cellsSelectable}; +mxGraph.prototype.setCellsSelectable=function(a){this.cellsSelectable=a};mxGraph.prototype.getDeletableCells=function(a){return this.model.filterCells(a,mxUtils.bind(this,function(a){return this.isCellDeletable(a)}))};mxGraph.prototype.isCellDeletable=function(a){var b=this.view.getState(a);a=null!=b?b.style:this.getCellStyle(a);return this.isCellsDeletable()&&0!=a[mxConstants.STYLE_DELETABLE]};mxGraph.prototype.isCellsDeletable=function(){return this.cellsDeletable}; +mxGraph.prototype.setCellsDeletable=function(a){this.cellsDeletable=a};mxGraph.prototype.isLabelMovable=function(a){return!this.isCellLocked(a)&&(this.model.isEdge(a)&&this.edgeLabelsMovable||this.model.isVertex(a)&&this.vertexLabelsMovable)};mxGraph.prototype.isCellRotatable=function(a){var b=this.view.getState(a);return 0!=(null!=b?b.style:this.getCellStyle(a))[mxConstants.STYLE_ROTATABLE]};mxGraph.prototype.getMovableCells=function(a){return this.model.filterCells(a,mxUtils.bind(this,function(a){return this.isCellMovable(a)}))}; +mxGraph.prototype.isCellMovable=function(a){var b=this.view.getState(a),b=null!=b?b.style:this.getCellStyle(a);return this.isCellsMovable()&&!this.isCellLocked(a)&&0!=b[mxConstants.STYLE_MOVABLE]};mxGraph.prototype.isCellsMovable=function(){return this.cellsMovable};mxGraph.prototype.setCellsMovable=function(a){this.cellsMovable=a};mxGraph.prototype.isGridEnabled=function(){return this.gridEnabled};mxGraph.prototype.setGridEnabled=function(a){this.gridEnabled=a};mxGraph.prototype.isPortsEnabled=function(){return this.portsEnabled}; +mxGraph.prototype.setPortsEnabled=function(a){this.portsEnabled=a};mxGraph.prototype.getGridSize=function(){return this.gridSize};mxGraph.prototype.setGridSize=function(a){this.gridSize=a};mxGraph.prototype.getTolerance=function(){return this.tolerance};mxGraph.prototype.setTolerance=function(a){this.tolerance=a};mxGraph.prototype.isVertexLabelsMovable=function(){return this.vertexLabelsMovable};mxGraph.prototype.setVertexLabelsMovable=function(a){this.vertexLabelsMovable=a}; +mxGraph.prototype.isEdgeLabelsMovable=function(){return this.edgeLabelsMovable};mxGraph.prototype.setEdgeLabelsMovable=function(a){this.edgeLabelsMovable=a};mxGraph.prototype.isSwimlaneNesting=function(){return this.swimlaneNesting};mxGraph.prototype.setSwimlaneNesting=function(a){this.swimlaneNesting=a};mxGraph.prototype.isSwimlaneSelectionEnabled=function(){return this.swimlaneSelectionEnabled};mxGraph.prototype.setSwimlaneSelectionEnabled=function(a){this.swimlaneSelectionEnabled=a}; +mxGraph.prototype.isMultigraph=function(){return this.multigraph};mxGraph.prototype.setMultigraph=function(a){this.multigraph=a};mxGraph.prototype.isAllowLoops=function(){return this.allowLoops};mxGraph.prototype.setAllowDanglingEdges=function(a){this.allowDanglingEdges=a};mxGraph.prototype.isAllowDanglingEdges=function(){return this.allowDanglingEdges};mxGraph.prototype.setConnectableEdges=function(a){this.connectableEdges=a};mxGraph.prototype.isConnectableEdges=function(){return this.connectableEdges}; +mxGraph.prototype.setCloneInvalidEdges=function(a){this.cloneInvalidEdges=a};mxGraph.prototype.isCloneInvalidEdges=function(){return this.cloneInvalidEdges};mxGraph.prototype.setAllowLoops=function(a){this.allowLoops=a};mxGraph.prototype.isDisconnectOnMove=function(){return this.disconnectOnMove};mxGraph.prototype.setDisconnectOnMove=function(a){this.disconnectOnMove=a};mxGraph.prototype.isDropEnabled=function(){return this.dropEnabled}; +mxGraph.prototype.setDropEnabled=function(a){this.dropEnabled=a};mxGraph.prototype.isSplitEnabled=function(){return this.splitEnabled};mxGraph.prototype.setSplitEnabled=function(a){this.splitEnabled=a};mxGraph.prototype.isCellResizable=function(a){var b=this.view.getState(a),b=null!=b?b.style:this.getCellStyle(a);return this.isCellsResizable()&&!this.isCellLocked(a)&&"0"!=mxUtils.getValue(b,mxConstants.STYLE_RESIZABLE,"1")};mxGraph.prototype.isCellsResizable=function(){return this.cellsResizable}; +mxGraph.prototype.setCellsResizable=function(a){this.cellsResizable=a};mxGraph.prototype.isTerminalPointMovable=function(a,b){return!0};mxGraph.prototype.isCellBendable=function(a){var b=this.view.getState(a),b=null!=b?b.style:this.getCellStyle(a);return this.isCellsBendable()&&!this.isCellLocked(a)&&0!=b[mxConstants.STYLE_BENDABLE]};mxGraph.prototype.isCellsBendable=function(){return this.cellsBendable};mxGraph.prototype.setCellsBendable=function(a){this.cellsBendable=a}; +mxGraph.prototype.isCellEditable=function(a){var b=this.view.getState(a),b=null!=b?b.style:this.getCellStyle(a);return this.isCellsEditable()&&!this.isCellLocked(a)&&0!=b[mxConstants.STYLE_EDITABLE]};mxGraph.prototype.isCellsEditable=function(){return this.cellsEditable};mxGraph.prototype.setCellsEditable=function(a){this.cellsEditable=a};mxGraph.prototype.isCellDisconnectable=function(a,b,c){return this.isCellsDisconnectable()&&!this.isCellLocked(a)};mxGraph.prototype.isCellsDisconnectable=function(){return this.cellsDisconnectable}; +mxGraph.prototype.setCellsDisconnectable=function(a){this.cellsDisconnectable=a};mxGraph.prototype.isValidSource=function(a){return null==a&&this.allowDanglingEdges||null!=a&&(!this.model.isEdge(a)||this.connectableEdges)&&this.isCellConnectable(a)};mxGraph.prototype.isValidTarget=function(a){return this.isValidSource(a)};mxGraph.prototype.isValidConnection=function(a,b){return this.isValidSource(a)&&this.isValidTarget(b)};mxGraph.prototype.setConnectable=function(a){this.connectionHandler.setEnabled(a)}; +mxGraph.prototype.isConnectable=function(a){return this.connectionHandler.isEnabled()};mxGraph.prototype.setTooltips=function(a){this.tooltipHandler.setEnabled(a)};mxGraph.prototype.setPanning=function(a){this.panningHandler.panningEnabled=a};mxGraph.prototype.isEditing=function(a){if(null!=this.cellEditor){var b=this.cellEditor.getEditingCell();return null==a?null!=b:a==b}return!1}; +mxGraph.prototype.isAutoSizeCell=function(a){var b=this.view.getState(a);a=null!=b?b.style:this.getCellStyle(a);return this.isAutoSizeCells()||1==a[mxConstants.STYLE_AUTOSIZE]};mxGraph.prototype.isAutoSizeCells=function(){return this.autoSizeCells};mxGraph.prototype.setAutoSizeCells=function(a){this.autoSizeCells=a};mxGraph.prototype.isExtendParent=function(a){return!this.getModel().isEdge(a)&&this.isExtendParents()};mxGraph.prototype.isExtendParents=function(){return this.extendParents}; +mxGraph.prototype.setExtendParents=function(a){this.extendParents=a};mxGraph.prototype.isExtendParentsOnAdd=function(a){return this.extendParentsOnAdd};mxGraph.prototype.setExtendParentsOnAdd=function(a){this.extendParentsOnAdd=a};mxGraph.prototype.isExtendParentsOnMove=function(){return this.extendParentsOnMove};mxGraph.prototype.setExtendParentsOnMove=function(a){this.extendParentsOnMove=a};mxGraph.prototype.isRecursiveResize=function(a){return this.recursiveResize}; +mxGraph.prototype.setRecursiveResize=function(a){this.recursiveResize=a};mxGraph.prototype.isConstrainChild=function(a){return this.isConstrainChildren()&&!this.getModel().isEdge(this.getModel().getParent(a))};mxGraph.prototype.isConstrainChildren=function(){return this.constrainChildren};mxGraph.prototype.setConstrainChildren=function(a){this.constrainChildren=a};mxGraph.prototype.isConstrainRelativeChildren=function(){return this.constrainRelativeChildren}; +mxGraph.prototype.setConstrainRelativeChildren=function(a){this.constrainRelativeChildren=a};mxGraph.prototype.isAllowNegativeCoordinates=function(){return this.allowNegativeCoordinates};mxGraph.prototype.setAllowNegativeCoordinates=function(a){this.allowNegativeCoordinates=a};mxGraph.prototype.getOverlap=function(a){return this.isAllowOverlapParent(a)?this.defaultOverlap:0};mxGraph.prototype.isAllowOverlapParent=function(a){return!1}; +mxGraph.prototype.getFoldableCells=function(a,b){return this.model.filterCells(a,mxUtils.bind(this,function(a){return this.isCellFoldable(a,b)}))};mxGraph.prototype.isCellFoldable=function(a,b){var c=this.view.getState(a),c=null!=c?c.style:this.getCellStyle(a);return 0mxUtils.indexOf(a,g);)g=this.model.getParent(g);return this.model.isLayer(c)||null!=g?null:c};mxGraph.prototype.getDefaultParent=function(){var a=this.getCurrentRoot();null==a&&(a=this.defaultParent,null==a&&(a=this.model.getRoot(),a=this.model.getChildAt(a,0)));return a};mxGraph.prototype.setDefaultParent=function(a){this.defaultParent=a};mxGraph.prototype.getSwimlane=function(a){for(;null!=a&&!this.isSwimlane(a);)a=this.model.getParent(a);return a}; +mxGraph.prototype.getSwimlaneAt=function(a,b,c){c=c||this.getDefaultParent();if(null!=c)for(var d=this.model.getChildCount(c),e=0;ea.width*e||0a.height*e)return!0}return!1};mxGraph.prototype.getChildVertices=function(a){return this.getChildCells(a,!0,!1)};mxGraph.prototype.getChildEdges=function(a){return this.getChildCells(a,!1,!0)}; +mxGraph.prototype.getChildCells=function(a,b,c){a=null!=a?a:this.getDefaultParent();a=this.model.getChildCells(a,null!=b?b:!1,null!=c?c:!1);b=[];for(c=0;c=a&&q.y+q.height<=l&&q.y>=b&&q.x+q.width<=k?f.push(p):this.getCells(a, +b,c,d,p,f)}}}return f};mxGraph.prototype.getCellsBeyond=function(a,b,c,d,e){var f=[];if(d||e)if(null==c&&(c=this.getDefaultParent()),null!=c)for(var g=this.model.getChildCount(c),k=0;k=a)&&(!e||m.y>=b)&&f.push(l)}return f}; +mxGraph.prototype.findTreeRoots=function(a,b,c){b=null!=b?b:!1;c=null!=c?c:!1;var d=[];if(null!=a){for(var e=this.getModel(),f=e.getChildCount(a),g=null,k=0,l=0;lk&&(k=n,g=m)}}0==d.length&&null!=g&&d.push(g)}return d}; +mxGraph.prototype.traverse=function(a,b,c,d,e,f){if(null!=c&&null!=a&&(b=null!=b?b:!0,f=null!=f?f:!1,e=e||new mxDictionary,!e.get(a)&&(e.put(a,!0),d=c(a,d),null==d||d))&&(d=this.model.getEdgeCount(a),0b?f-1:b)),this.setSelectionCell(a)):this.getCurrentRoot()!=d&&this.setSelectionCell(d)};mxGraph.prototype.selectAll=function(a,b){a=a||this.getDefaultParent();var c=b?this.model.filterDescendants(function(b){return b!=a},a):this.model.getChildren(a);null!=c&&this.setSelectionCells(c)};mxGraph.prototype.selectVertices=function(a){this.selectCells(!0,!1,a)};mxGraph.prototype.selectEdges=function(a){this.selectCells(!1,!0,a)}; +mxGraph.prototype.selectCells=function(a,b,c){c=c||this.getDefaultParent();var d=mxUtils.bind(this,function(c){return null!=this.view.getState(c)&&(0==this.model.getChildCount(c)&&this.model.isVertex(c)&&a&&!this.model.isEdge(this.model.getParent(c))||this.model.isEdge(c)&&b)});c=this.model.filterDescendants(d,c);this.setSelectionCells(c)}; +mxGraph.prototype.selectCellForEvent=function(a,b){var c=this.isCellSelected(a);this.isToggleEvent(b)?c?this.removeSelectionCell(a):this.addSelectionCell(a):c&&1==this.getSelectionCount()||this.setSelectionCell(a)};mxGraph.prototype.selectCellsForEvent=function(a,b){this.isToggleEvent(b)?this.addSelectionCells(a):this.setSelectionCells(a)}; +mxGraph.prototype.createHandler=function(a){var b=null;if(null!=a)if(this.model.isEdge(a.cell))var b=a.getVisibleTerminalState(!0),c=a.getVisibleTerminalState(!1),d=this.getCellGeometry(a.cell),b=this.view.getEdgeStyle(a,null!=d?d.points:null,b,c),b=this.createEdgeHandler(a,b);else b=this.createVertexHandler(a);return b};mxGraph.prototype.createVertexHandler=function(a){return new mxVertexHandler(a)}; +mxGraph.prototype.createEdgeHandler=function(a,b){return b==mxEdgeStyle.Loop||b==mxEdgeStyle.ElbowConnector||b==mxEdgeStyle.SideToSide||b==mxEdgeStyle.TopToBottom?this.createElbowEdgeHandler(a):b==mxEdgeStyle.SegmentConnector||b==mxEdgeStyle.OrthConnector?this.createEdgeSegmentHandler(a):new mxEdgeHandler(a)};mxGraph.prototype.createEdgeSegmentHandler=function(a){return new mxEdgeSegmentHandler(a)};mxGraph.prototype.createElbowEdgeHandler=function(a){return new mxElbowEdgeHandler(a)}; +mxGraph.prototype.addMouseListener=function(a){null==this.mouseListeners&&(this.mouseListeners=[]);this.mouseListeners.push(a)};mxGraph.prototype.removeMouseListener=function(a){if(null!=this.mouseListeners)for(var b=0;bthis.doubleClickCounter){if(this.doubleClickCounter++,d=!1,a==mxEvent.MOUSE_UP?b.getCell()==this.lastTouchCell&&null!=this.lastTouchCell&&(this.lastTouchTime=0,d=this.lastTouchCell,this.lastTouchCell=null,mxClient.IS_QUIRKS&&b.getSource().fireEvent("ondblclick"),this.dblClick(b.getEvent(), +d),d=!0):(this.fireDoubleClick=!0,this.lastTouchTime=0),!mxClient.IS_QUIRKS||d){mxEvent.consume(b.getEvent());return}}else{if(null==this.lastTouchEvent||this.lastTouchEvent!=b.getEvent())this.lastTouchCell=b.getCell(),this.lastTouchX=b.getX(),this.lastTouchY=b.getY(),this.lastTouchTime=d,this.lastTouchEvent=b.getEvent(),this.doubleClickCounter=0}else if((this.isMouseDown||a==mxEvent.MOUSE_UP)&&this.fireDoubleClick){this.fireDoubleClick=!1;d=this.lastTouchCell;this.lastTouchCell=null;this.isMouseDown= +!1;(null!=d||(mxEvent.isTouchEvent(b.getEvent())||mxEvent.isPenEvent(b.getEvent()))&&(mxClient.IS_GC||mxClient.IS_SF))&&Math.abs(this.lastTouchX-b.getX())c.x&&(f-=c.x);0>c.y&&(g-=c.y);if(b.translate.x!=f||b.translate.y!=g)b.translate.x=f,b.translate.y=g,a=!0;var c=b.translate,d=this.source.getView().scale,f=d/b.scale,g=1/b.scale,k=this.source.container;this.bounds=new mxRectangle((c.x-e.x-this.source.panDx)/g,(c.y-e.y-this.source.panDy)/g,k.clientWidth/f,k.clientHeight/f);this.bounds.x+=this.source.container.scrollLeft*b.scale/d;this.bounds.y+=this.source.container.scrollTop*b.scale/d;c=this.selectionBorder.bounds;if(c.x!=this.bounds.x|| +c.y!=this.bounds.y||c.width!=this.bounds.width||c.height!=this.bounds.height)this.selectionBorder.bounds=this.bounds,this.selectionBorder.redraw();c=this.sizer.bounds;b=new mxRectangle(this.bounds.x+this.bounds.width-c.width/2,this.bounds.y+this.bounds.height-c.height/2,c.width,c.height);if(c.x!=b.x||c.y!=b.y||c.width!=b.width||c.height!=b.height)this.sizer.bounds=b,"hidden"!=this.sizer.node.style.visibility&&this.sizer.redraw();a&&this.outline.view.revalidate()}}}; +mxOutline.prototype.mouseDown=function(a,b){if(this.enabled&&this.showViewport){var c=mxEvent.isMouseEvent(b.getEvent())?0:this.source.tolerance,c=this.source.allowHandleBoundsCheck&&(mxClient.IS_IE||0=this.max)||!this.source&&(0==this.max||f>=this.max))&&(g+=this.countError+"\n"),null!=this.validNeighbors&&null!=this.typeError&&0mxUtils.indexOf(a,f)&&(f=this.getLayout(f),null!=f&&f.moveCell(a[e],c.x,c.y))}}; +mxLayoutManager.prototype.getCellsForChanges=function(a){for(var b=new mxDictionary,c=[],d=0;df||Math.abs(d)>f){null==this.highlight&&(this.highlight=new mxCellHighlight(this.graph,mxConstants.DROP_TARGET_COLOR,3));null==this.shape&&(this.shape=this.createPreviewShape(this.bounds));var g=c.isGridEnabledEvent(b.getEvent()), +f=!0;if(null!=this.guide&&this.useGuidesForEvent(b))d=this.guide.move(this.bounds,new mxPoint(e,d),g),f=!1,e=d.x,d=d.y;else if(g)var k=c.getView().translate,l=c.getView().scale,g=this.bounds.x-(c.snap(this.bounds.x/l-k.x)+k.x)*l,k=this.bounds.y-(c.snap(this.bounds.y/l-k.y)+k.y)*l,d=this.snap(new mxPoint(e,d)),e=d.x-g,d=d.y-k;null!=this.guide&&f&&this.guide.hide();c.isConstrainedEvent(b.getEvent())&&(Math.abs(e)>Math.abs(d)?d=0:e=0);this.currentDx=e;this.currentDy=d;this.updatePreviewShape();f=null; +d=b.getCell();g=c.isCloneEvent(b.getEvent())&&c.isCellsCloneable()&&this.isCloneEnabled();c.isDropEnabled()&&this.highlightEnabled&&(f=c.getDropTarget(this.cells,b.getEvent(),d,g));e=c.getView().getState(f);k=!1;null==e||c.model.getParent(this.cell)==f&&!g?(this.target=null,this.connectOnDrop&&null!=d&&1==this.cells.length&&c.getModel().isVertex(d)&&c.isCellConnectable(d)&&(e=c.getView().getState(d),null!=e&&(c=null==c.getEdgeValidationError(null,this.cell,d)?mxConstants.VALID_COLOR:mxConstants.INVALID_CONNECT_TARGET_COLOR, +this.setHighlightColor(c),k=!0))):(this.target!=f&&(this.target=f,this.setHighlightColor(mxConstants.DROP_TARGET_COLOR)),k=!0);null!=e&&k?this.highlight.highlight(e):this.highlight.hide()}this.updateHint(b);this.consumeMouseEvent(mxEvent.MOUSE_MOVE,b);mxEvent.consume(b.getEvent())}else!this.isMoveEnabled()&&!this.isCloneEnabled()||!this.updateCursor||b.isConsumed()||null==b.getState()&&null==b.sourceState||c.isMouseDown||(e=c.getCursorForMouseEvent(b),null==e&&c.isEnabled()&&c.isCellMovable(b.getCell())&& +(e=c.getModel().isEdge(b.getCell())?mxConstants.CURSOR_MOVABLE_EDGE:mxConstants.CURSOR_MOVABLE_VERTEX),null!=e&&null!=b.sourceState&&b.sourceState.setCursor(e))};mxGraphHandler.prototype.updatePreviewShape=function(){null!=this.shape&&(this.shape.bounds=new mxRectangle(Math.round(this.pBounds.x+this.currentDx-this.graph.panDx),Math.round(this.pBounds.y+this.currentDy-this.graph.panDy),this.pBounds.width,this.pBounds.height),this.shape.redraw())}; +mxGraphHandler.prototype.setHighlightColor=function(a){null!=this.highlight&&this.highlight.setHighlightColor(a)}; +mxGraphHandler.prototype.mouseUp=function(a,b){if(!b.isConsumed()){var c=this.graph;if(null!=this.cell&&null!=this.first&&null!=this.shape&&null!=this.currentDx&&null!=this.currentDy){var d=b.getCell();if(this.connectOnDrop&&null==this.target&&null!=d&&c.getModel().isVertex(d)&&c.isCellConnectable(d)&&c.isEdgeValid(null,this.cell,d))c.connectionHandler.connect(this.cell,d,b.getEvent());else{var d=c.isCloneEvent(b.getEvent())&&c.isCellsCloneable()&&this.isCloneEnabled(),e=c.getView().scale,f=this.roundLength(this.currentDx/ +e),e=this.roundLength(this.currentDy/e),g=this.target;c.isSplitEnabled()&&c.isSplitTarget(g,this.cells,b.getEvent())?c.splitEdge(g,this.cells,null,f,e):this.moveCells(this.cells,f,e,d,this.target,b.getEvent())}}else this.isSelectEnabled()&&this.delayedSelection&&null!=this.cell&&this.selectDelayed(b)}this.cellWasClicked&&this.consumeMouseEvent(mxEvent.MOUSE_UP,b);this.reset()}; +mxGraphHandler.prototype.selectDelayed=function(a){this.graph.isCellSelected(this.cell)&&this.graph.popupMenuHandler.isPopupTrigger(a)||this.graph.selectCellForEvent(this.cell,a.getEvent())};mxGraphHandler.prototype.reset=function(){this.destroyShapes();this.removeHint();this.delayedSelection=this.cellWasClicked=!1;this.target=this.cell=this.first=this.guides=this.currentDy=this.currentDx=null}; +mxGraphHandler.prototype.shouldRemoveCellsFromParent=function(a,b,c){if(this.graph.getModel().isVertex(a)&&(a=this.graph.getView().getState(a),null!=a)){c=mxUtils.convertPoint(this.graph.container,mxEvent.getClientX(c),mxEvent.getClientY(c));var d=mxUtils.toRadians(mxUtils.getValue(a.style,mxConstants.STYLE_ROTATION)||0);if(0!=d){b=Math.cos(-d);var d=Math.sin(-d),e=new mxPoint(a.getCenterX(),a.getCenterY());c=mxUtils.getRotatedPoint(c,b,d,e)}return!mxUtils.contains(a,c.x,c.y)}return!1}; +mxGraphHandler.prototype.moveCells=function(a,b,c,d,e,f){d&&(a=this.graph.getCloneableCells(a));null==e&&this.isRemoveCellsFromParent()&&this.shouldRemoveCellsFromParent(this.graph.getModel().getParent(this.cell),a,f)&&(e=this.graph.getDefaultParent());d=d&&!this.graph.isCellLocked(e||this.graph.getDefaultParent());a=this.graph.moveCells(a,b-this.graph.panDx/this.graph.view.scale,c-this.graph.panDy/this.graph.view.scale,d,e,f);this.isSelectEnabled()&&this.scrollOnMove&&this.graph.scrollCellToVisible(a[0]); +d&&this.graph.setSelectionCells(a)};mxGraphHandler.prototype.destroyShapes=function(){null!=this.shape&&(this.shape.destroy(),this.shape=null);null!=this.guide&&(this.guide.destroy(),this.guide=null);null!=this.highlight&&(this.highlight.destroy(),this.highlight=null)}; +mxGraphHandler.prototype.destroy=function(){this.graph.removeMouseListener(this);this.graph.removeListener(this.panHandler);null!=this.escapeHandler&&(this.graph.removeListener(this.escapeHandler),this.escapeHandler=null);this.destroyShapes();this.removeHint()}; +function mxPanningHandler(a){null!=a&&(this.graph=a,this.graph.addMouseListener(this),this.forcePanningHandler=mxUtils.bind(this,function(a,c){var b=c.getProperty("eventName"),e=c.getProperty("event");b==mxEvent.MOUSE_DOWN&&this.isForcePanningEvent(e)&&(this.start(e),this.active=!0,this.fireEvent(new mxEventObject(mxEvent.PAN_START,"event",e)),e.consume())}),this.graph.addListener(mxEvent.FIRE_MOUSE_EVENT,this.forcePanningHandler),this.gestureHandler=mxUtils.bind(this,function(a,c){if(this.isPinchEnabled()){var b= +c.getProperty("event");mxEvent.isConsumed(b)||"gesturestart"!=b.type?"gestureend"==b.type&&null!=this.initialScale&&(this.initialScale=null):(this.initialScale=this.graph.view.scale,this.active||null==this.mouseDownEvent||(this.start(this.mouseDownEvent),this.mouseDownEvent=null));if(null!=this.initialScale){var e=Math.round(this.initialScale*b.scale*100)/100;null!=this.minScale&&(e=Math.max(this.minScale,e));null!=this.maxScale&&(e=Math.min(this.maxScale,e));this.graph.view.scale!=e&&(this.graph.zoomTo(e), +mxEvent.consume(b))}}}),this.graph.addListener(mxEvent.GESTURE,this.gestureHandler),this.mouseUpListener=mxUtils.bind(this,function(){this.active&&this.reset()}),mxEvent.addListener(document,"mouseup",this.mouseUpListener))}mxPanningHandler.prototype=new mxEventSource;mxPanningHandler.prototype.constructor=mxPanningHandler;mxPanningHandler.prototype.graph=null;mxPanningHandler.prototype.useLeftButtonForPanning=!1;mxPanningHandler.prototype.usePopupTrigger=!0; +mxPanningHandler.prototype.ignoreCell=!1;mxPanningHandler.prototype.previewEnabled=!0;mxPanningHandler.prototype.useGrid=!1;mxPanningHandler.prototype.panningEnabled=!0;mxPanningHandler.prototype.pinchEnabled=!0;mxPanningHandler.prototype.maxScale=8;mxPanningHandler.prototype.minScale=.01;mxPanningHandler.prototype.dx=null;mxPanningHandler.prototype.dy=null;mxPanningHandler.prototype.startX=0;mxPanningHandler.prototype.startY=0; +mxPanningHandler.prototype.isActive=function(){return this.active||null!=this.initialScale};mxPanningHandler.prototype.isPanningEnabled=function(){return this.panningEnabled};mxPanningHandler.prototype.setPanningEnabled=function(a){this.panningEnabled=a};mxPanningHandler.prototype.isPinchEnabled=function(){return this.pinchEnabled};mxPanningHandler.prototype.setPinchEnabled=function(a){this.pinchEnabled=a}; +mxPanningHandler.prototype.isPanningTrigger=function(a){var b=a.getEvent();return this.useLeftButtonForPanning&&null==a.getState()&&mxEvent.isLeftMouseButton(b)||mxEvent.isControlDown(b)&&mxEvent.isShiftDown(b)||this.usePopupTrigger&&mxEvent.isPopupTrigger(b)};mxPanningHandler.prototype.isForcePanningEvent=function(a){return this.ignoreCell||mxEvent.isMultiTouchEvent(a.getEvent())}; +mxPanningHandler.prototype.mouseDown=function(a,b){this.mouseDownEvent=b;!b.isConsumed()&&this.isPanningEnabled()&&!this.active&&this.isPanningTrigger(b)&&(this.start(b),this.consumePanningTrigger(b))};mxPanningHandler.prototype.start=function(a){this.dx0=-this.graph.container.scrollLeft;this.dy0=-this.graph.container.scrollTop;this.startX=a.getX();this.startY=a.getY();this.dy=this.dx=null;this.panningTrigger=!0};mxPanningHandler.prototype.consumePanningTrigger=function(a){a.consume()}; +mxPanningHandler.prototype.mouseMove=function(a,b){this.dx=b.getX()-this.startX;this.dy=b.getY()-this.startY;if(this.active)this.previewEnabled&&(this.useGrid&&(this.dx=this.graph.snap(this.dx),this.dy=this.graph.snap(this.dy)),this.graph.panGraph(this.dx+this.dx0,this.dy+this.dy0)),this.fireEvent(new mxEventObject(mxEvent.PAN,"event",b));else if(this.panningTrigger){var c=this.active;this.active=Math.abs(this.dx)>this.graph.tolerance||Math.abs(this.dy)>this.graph.tolerance;!c&&this.active&&this.fireEvent(new mxEventObject(mxEvent.PAN_START, +"event",b))}(this.active||this.panningTrigger)&&b.consume()};mxPanningHandler.prototype.mouseUp=function(a,b){if(this.active){if(null!=this.dx&&null!=this.dy){if(!this.graph.useScrollbarsForPanning||!mxUtils.hasScrollbars(this.graph.container)){var c=this.graph.getView().scale,d=this.graph.getView().translate;this.graph.panGraph(0,0);this.panGraph(d.x+this.dx/c,d.y+this.dy/c)}b.consume()}this.fireEvent(new mxEventObject(mxEvent.PAN_END,"event",b))}this.reset()}; +mxPanningHandler.prototype.reset=function(){this.panningTrigger=!1;this.mouseDownEvent=null;this.active=!1;this.dy=this.dx=null};mxPanningHandler.prototype.panGraph=function(a,b){this.graph.getView().setTranslate(a,b)};mxPanningHandler.prototype.destroy=function(){this.graph.removeMouseListener(this);this.graph.removeListener(this.forcePanningHandler);this.graph.removeListener(this.gestureHandler);mxEvent.removeListener(document,"mouseup",this.mouseUpListener)}; +function mxPopupMenuHandler(a,b){null!=a&&(this.graph=a,this.factoryMethod=b,this.graph.addMouseListener(this),this.gestureHandler=mxUtils.bind(this,function(a,b){this.inTolerance=!1}),this.graph.addListener(mxEvent.GESTURE,this.gestureHandler),this.init())}mxPopupMenuHandler.prototype=new mxPopupMenu;mxPopupMenuHandler.prototype.constructor=mxPopupMenuHandler;mxPopupMenuHandler.prototype.graph=null;mxPopupMenuHandler.prototype.selectOnPopup=!0; +mxPopupMenuHandler.prototype.clearSelectionOnBackground=!0;mxPopupMenuHandler.prototype.triggerX=null;mxPopupMenuHandler.prototype.triggerY=null;mxPopupMenuHandler.prototype.screenX=null;mxPopupMenuHandler.prototype.screenY=null;mxPopupMenuHandler.prototype.init=function(){mxPopupMenu.prototype.init.apply(this);mxEvent.addGestureListeners(this.div,mxUtils.bind(this,function(a){this.graph.tooltipHandler.hide()}))};mxPopupMenuHandler.prototype.isSelectOnPopup=function(a){return this.selectOnPopup}; +mxPopupMenuHandler.prototype.mouseDown=function(a,b){this.isEnabled()&&!mxEvent.isMultiTouchEvent(b.getEvent())&&(this.hideMenu(),this.triggerX=b.getGraphX(),this.triggerY=b.getGraphY(),this.screenX=mxEvent.getMainEvent(b.getEvent()).screenX,this.screenY=mxEvent.getMainEvent(b.getEvent()).screenY,this.popupTrigger=this.isPopupTrigger(b),this.inTolerance=!0)}; +mxPopupMenuHandler.prototype.mouseMove=function(a,b){this.inTolerance&&null!=this.screenX&&null!=this.screenY&&(Math.abs(mxEvent.getMainEvent(b.getEvent()).screenX-this.screenX)>this.graph.tolerance||Math.abs(mxEvent.getMainEvent(b.getEvent()).screenY-this.screenY)>this.graph.tolerance)&&(this.inTolerance=!1)}; +mxPopupMenuHandler.prototype.mouseUp=function(a,b){if(this.popupTrigger&&this.inTolerance&&null!=this.triggerX&&null!=this.triggerY){var c=this.getCellForPopupEvent(b);this.graph.isEnabled()&&this.isSelectOnPopup(b)&&null!=c&&!this.graph.isCellSelected(c)?this.graph.setSelectionCell(c):this.clearSelectionOnBackground&&null==c&&this.graph.clearSelection();this.graph.tooltipHandler.hide();var d=mxUtils.getScrollOrigin();this.popup(b.getX()+d.x+1,b.getY()+d.y+1,c,b.getEvent());b.consume()}this.inTolerance= +this.popupTrigger=!1};mxPopupMenuHandler.prototype.getCellForPopupEvent=function(a){return a.getCell()};mxPopupMenuHandler.prototype.destroy=function(){this.graph.removeMouseListener(this);this.graph.removeListener(this.gestureHandler);mxPopupMenu.prototype.destroy.apply(this)}; +function mxCellMarker(a,b,c,d){mxEventSource.call(this);null!=a&&(this.graph=a,this.validColor=null!=b?b:mxConstants.DEFAULT_VALID_COLOR,this.invalidColor=null!=b?c:mxConstants.DEFAULT_INVALID_COLOR,this.hotspot=null!=d?d:mxConstants.DEFAULT_HOTSPOT,this.highlight=new mxCellHighlight(a))}mxUtils.extend(mxCellMarker,mxEventSource);mxCellMarker.prototype.graph=null;mxCellMarker.prototype.enabled=!0;mxCellMarker.prototype.hotspot=mxConstants.DEFAULT_HOTSPOT;mxCellMarker.prototype.hotspotEnabled=!1; +mxCellMarker.prototype.validColor=null;mxCellMarker.prototype.invalidColor=null;mxCellMarker.prototype.currentColor=null;mxCellMarker.prototype.validState=null;mxCellMarker.prototype.markedState=null;mxCellMarker.prototype.setEnabled=function(a){this.enabled=a};mxCellMarker.prototype.isEnabled=function(){return this.enabled};mxCellMarker.prototype.setHotspot=function(a){this.hotspot=a};mxCellMarker.prototype.getHotspot=function(){return this.hotspot}; +mxCellMarker.prototype.setHotspotEnabled=function(a){this.hotspotEnabled=a};mxCellMarker.prototype.isHotspotEnabled=function(){return this.hotspotEnabled};mxCellMarker.prototype.hasValidState=function(){return null!=this.validState};mxCellMarker.prototype.getValidState=function(){return this.validState};mxCellMarker.prototype.getMarkedState=function(){return this.markedState};mxCellMarker.prototype.reset=function(){this.validState=null;null!=this.markedState&&(this.markedState=null,this.unmark())}; +mxCellMarker.prototype.process=function(a){var b=null;this.isEnabled()&&(b=this.getState(a),this.setCurrentState(b,a));return b};mxCellMarker.prototype.setCurrentState=function(a,b,c){var d=null!=a?this.isValidState(a):!1;c=null!=c?c:this.getMarkerColor(b.getEvent(),a,d);this.validState=d?a:null;if(a!=this.markedState||c!=this.currentColor)this.currentColor=c,null!=a&&null!=this.currentColor?(this.markedState=a,this.mark()):null!=this.markedState&&(this.markedState=null,this.unmark())}; +mxCellMarker.prototype.markCell=function(a,b){var c=this.graph.getView().getState(a);null!=c&&(this.currentColor=null!=b?b:this.validColor,this.markedState=c,this.mark())};mxCellMarker.prototype.mark=function(){this.highlight.setHighlightColor(this.currentColor);this.highlight.highlight(this.markedState);this.fireEvent(new mxEventObject(mxEvent.MARK,"state",this.markedState))};mxCellMarker.prototype.unmark=function(){this.mark()};mxCellMarker.prototype.isValidState=function(a){return!0}; +mxCellMarker.prototype.getMarkerColor=function(a,b,c){return c?this.validColor:this.invalidColor};mxCellMarker.prototype.getState=function(a){var b=this.graph.getView(),c=this.getCell(a),b=this.getStateToMark(b.getState(c));return null!=b&&this.intersects(b,a)?b:null};mxCellMarker.prototype.getCell=function(a){return a.getCell()};mxCellMarker.prototype.getStateToMark=function(a){return a}; +mxCellMarker.prototype.intersects=function(a,b){return this.hotspotEnabled?mxUtils.intersectsHotspot(a,b.getGraphX(),b.getGraphY(),this.hotspot,mxConstants.MIN_HOTSPOT_SIZE,mxConstants.MAX_HOTSPOT_SIZE):!0};mxCellMarker.prototype.destroy=function(){this.graph.getView().removeListener(this.resetHandler);this.graph.getModel().removeListener(this.resetHandler);this.highlight.destroy()}; +function mxSelectionCellsHandler(a){mxEventSource.call(this);this.graph=a;this.handlers=new mxDictionary;this.graph.addMouseListener(this);this.refreshHandler=mxUtils.bind(this,function(a,c){this.isEnabled()&&this.refresh()});this.graph.getSelectionModel().addListener(mxEvent.CHANGE,this.refreshHandler);this.graph.getModel().addListener(mxEvent.CHANGE,this.refreshHandler);this.graph.getView().addListener(mxEvent.SCALE,this.refreshHandler);this.graph.getView().addListener(mxEvent.TRANSLATE,this.refreshHandler); +this.graph.getView().addListener(mxEvent.SCALE_AND_TRANSLATE,this.refreshHandler);this.graph.getView().addListener(mxEvent.DOWN,this.refreshHandler);this.graph.getView().addListener(mxEvent.UP,this.refreshHandler)}mxUtils.extend(mxSelectionCellsHandler,mxEventSource);mxSelectionCellsHandler.prototype.graph=null;mxSelectionCellsHandler.prototype.enabled=!0;mxSelectionCellsHandler.prototype.refreshHandler=null;mxSelectionCellsHandler.prototype.maxHandlers=100; +mxSelectionCellsHandler.prototype.handlers=null;mxSelectionCellsHandler.prototype.isEnabled=function(){return this.enabled};mxSelectionCellsHandler.prototype.setEnabled=function(a){this.enabled=a};mxSelectionCellsHandler.prototype.getHandler=function(a){return this.handlers.get(a)};mxSelectionCellsHandler.prototype.reset=function(){this.handlers.visit(function(a,b){b.reset.apply(b)})}; +mxSelectionCellsHandler.prototype.refresh=function(){var a=this.handlers;this.handlers=new mxDictionary;for(var b=this.graph.getSelectionCells(),c=0;cthis.graph.tolerance||g>this.graph.tolerance)&&(this.shape=this.createShape(), +null!=this.edgeState&&this.shape.apply(this.edgeState),this.updateCurrentState(b,c));null!=this.shape&&(null!=this.edgeState?this.shape.points=this.edgeState.absolutePoints:(c=[e],null!=this.waypoints&&(c=c.concat(this.waypoints)),c.push(d),this.shape.points=c),this.drawPreview());null!=this.cursor&&(this.graph.container.style.cursor=this.cursor);mxEvent.consume(b.getEvent());b.consume()}else this.isEnabled()&&this.graph.isEnabled()?this.previous!=this.currentState&&null==this.edgeState?(this.destroyIcons(), +null!=this.currentState&&null==this.error&&null==this.constraintHandler.currentConstraint&&(this.icons=this.createIcons(this.currentState),null==this.icons&&(this.currentState.setCursor(mxConstants.CURSOR_CONNECT),b.consume())),this.previous=this.currentState):this.previous!=this.currentState||null==this.currentState||null!=this.icons||this.graph.isMouseDown||b.consume():this.constraintHandler.reset();if(!this.graph.isMouseDown&&null!=this.currentState&&null!=this.icons){c=!1;d=b.getSource();for(e= +0;ethis.graph.tolerance||b>this.graph.tolerance))null==this.waypoints&&(this.waypoints=[]),c=this.graph.view.scale,b=new mxPoint(this.graph.snap(a.getGraphX()/c)*c,this.graph.snap(a.getGraphY()/c)*c),this.waypoints.push(b)}; +mxConnectionHandler.prototype.mouseUp=function(a,b){if(!b.isConsumed()&&this.isConnecting()){if(this.waypointsEnabled&&!this.isStopEvent(b)){this.addWaypointForEvent(b);b.consume();return}if(null==this.error){var c=null!=this.previous?this.previous.cell:null,d=null;null!=this.constraintHandler.currentConstraint&&null!=this.constraintHandler.currentFocus&&(d=this.constraintHandler.currentFocus.cell);null==d&&null!=this.currentState&&(d=this.currentState.cell);this.connect(c,d,b.getEvent(),b.getCell())}else null!= +this.previous&&null!=this.marker.validState&&this.previous.cell==this.marker.validState.cell&&this.graph.selectCellForEvent(this.marker.source,evt),0g||Math.abs(f)>g)null==this.div&&(this.div=this.createShape()),mxUtils.clearSelection(),this.update(d,c),b.consume()}}; +mxRubberband.prototype.createShape=function(){null==this.sharedDiv&&(this.sharedDiv=document.createElement("div"),this.sharedDiv.className="mxRubberband",mxUtils.setOpacity(this.sharedDiv,this.defaultOpacity));this.graph.container.appendChild(this.sharedDiv);var a=this.sharedDiv;mxClient.IS_SVG&&(!mxClient.IS_IE||10<=document.documentMode)&&this.fadeOut&&(this.sharedDiv=null);return a};mxRubberband.prototype.isActive=function(a,b){return null!=this.div&&"none"!=this.div.style.display}; +mxRubberband.prototype.mouseUp=function(a,b){var c=this.isActive();this.reset();c&&(this.execute(b.getEvent()),b.consume())};mxRubberband.prototype.execute=function(a){var b=new mxRectangle(this.x,this.y,this.width,this.height);this.graph.selectRegion(b,a)}; +mxRubberband.prototype.reset=function(){if(null!=this.div)if(mxClient.IS_SVG&&(!mxClient.IS_IE||10<=document.documentMode)&&this.fadeOut){var a=this.div;mxUtils.setPrefixedStyle(a.style,"transition","all 0.2s linear");a.style.pointerEvents="none";a.style.opacity=0;window.setTimeout(function(){a.parentNode.removeChild(a)},200)}else this.div.parentNode.removeChild(this.div);mxEvent.removeGestureListeners(document,null,this.dragHandler,this.dropHandler);this.dropHandler=this.dragHandler=null;this.currentY= +this.currentX=0;this.div=this.first=null};mxRubberband.prototype.update=function(a,b){this.currentX=a;this.currentY=b;this.repaint()}; +mxRubberband.prototype.repaint=function(){if(null!=this.div){var a=this.currentX-this.graph.panDx,b=this.currentY-this.graph.panDy;this.x=Math.min(this.first.x,a);this.y=Math.min(this.first.y,b);this.width=Math.max(this.first.x,a)-this.x;this.height=Math.max(this.first.y,b)-this.y;a=mxClient.IS_VML?this.graph.panDy:0;this.div.style.left=this.x+(mxClient.IS_VML?this.graph.panDx:0)+"px";this.div.style.top=this.y+a+"px";this.div.style.width=Math.max(1,this.width)+"px";this.div.style.height=Math.max(1, +this.height)+"px"}};mxRubberband.prototype.destroy=function(){this.destroyed||(this.destroyed=!0,this.graph.removeMouseListener(this),this.graph.removeListener(this.forceRubberbandHandler),this.graph.removeListener(this.panHandler),this.reset(),null!=this.sharedDiv&&(this.sharedDiv=null))};function mxHandle(a,b,c){this.graph=a.view.graph;this.state=a;this.cursor=null!=b?b:this.cursor;this.image=null!=c?c:this.image;this.init()}mxHandle.prototype.cursor="default";mxHandle.prototype.image=null; +mxHandle.prototype.ignoreGrid=!1;mxHandle.prototype.getPosition=function(a){};mxHandle.prototype.setPosition=function(a,b,c){};mxHandle.prototype.execute=function(){};mxHandle.prototype.copyStyle=function(a){this.graph.setCellStyles(a,this.state.style[a],[this.state.cell])}; +mxHandle.prototype.processEvent=function(a){var b=this.graph.view.scale,c=this.graph.view.translate,c=new mxPoint(a.getGraphX()/b-c.x,a.getGraphY()/b-c.y);null!=this.shape&&null!=this.shape.bounds&&(c.x-=this.shape.bounds.width/b/4,c.y-=this.shape.bounds.height/b/4);var b=-mxUtils.toRadians(this.getRotation()),d=-mxUtils.toRadians(this.getTotalRotation())-b,c=this.flipPoint(this.rotatePoint(this.snapPoint(this.rotatePoint(c,b),this.ignoreGrid||!this.graph.isGridEnabledEvent(a.getEvent())),d));this.setPosition(this.state.getPaintBounds(), +c,a);this.positionChanged();this.redraw()};mxHandle.prototype.positionChanged=function(){null!=this.state.text&&this.state.text.apply(this.state);null!=this.state.shape&&this.state.shape.apply(this.state);this.graph.cellRenderer.redraw(this.state,!0)};mxHandle.prototype.getRotation=function(){return null!=this.state.shape?this.state.shape.getRotation():0};mxHandle.prototype.getTotalRotation=function(){return null!=this.state.shape?this.state.shape.getShapeRotation():0}; +mxHandle.prototype.init=function(){var a=this.isHtmlRequired();null!=this.image?(this.shape=new mxImageShape(new mxRectangle(0,0,this.image.width,this.image.height),this.image.src),this.shape.preserveImageAspect=!1):this.shape=this.createShape(a);this.initShape(a)};mxHandle.prototype.createShape=function(a){a=new mxRectangle(0,0,mxConstants.HANDLE_SIZE,mxConstants.HANDLE_SIZE);return new mxRectangleShape(a,mxConstants.HANDLE_FILLCOLOR,mxConstants.HANDLE_STROKECOLOR)}; +mxHandle.prototype.initShape=function(a){a&&this.shape.isHtmlAllowed()?(this.shape.dialect=mxConstants.DIALECT_STRICTHTML,this.shape.init(this.graph.container)):(this.shape.dialect=this.graph.dialect!=mxConstants.DIALECT_SVG?mxConstants.DIALECT_MIXEDHTML:mxConstants.DIALECT_SVG,null!=this.cursor&&this.shape.init(this.graph.getView().getOverlayPane()));mxEvent.redirectMouseEvents(this.shape.node,this.graph,this.state);this.shape.node.style.cursor=this.cursor}; +mxHandle.prototype.redraw=function(){if(null!=this.shape&&null!=this.state.shape){var a=this.getPosition(this.state.getPaintBounds());if(null!=a){var b=mxUtils.toRadians(this.getTotalRotation()),a=this.rotatePoint(this.flipPoint(a),b),b=this.graph.view.scale,c=this.graph.view.translate;this.shape.bounds.x=Math.floor((a.x+c.x)*b-this.shape.bounds.width/2);this.shape.bounds.y=Math.floor((a.y+c.y)*b-this.shape.bounds.height/2);this.shape.redraw()}}}; +mxHandle.prototype.isHtmlRequired=function(){return null!=this.state.text&&this.state.text.node.parentNode==this.graph.container};mxHandle.prototype.rotatePoint=function(a,b){var c=this.state.getCellBounds(),c=new mxPoint(c.getCenterX(),c.getCenterY());return mxUtils.getRotatedPoint(a,Math.cos(b),Math.sin(b),c)}; +mxHandle.prototype.flipPoint=function(a){if(null!=this.state.shape){var b=this.state.getCellBounds();this.state.shape.flipH&&(a.x=2*b.x+b.width-a.x);this.state.shape.flipV&&(a.y=2*b.y+b.height-a.y)}return a};mxHandle.prototype.snapPoint=function(a,b){b||(a.x=this.graph.snap(a.x),a.y=this.graph.snap(a.y));return a};mxHandle.prototype.setVisible=function(a){null!=this.shape&&null!=this.shape.node&&(this.shape.node.style.display=a?"":"none")}; +mxHandle.prototype.reset=function(){this.setVisible(!0);this.state.style=this.graph.getCellStyle(this.state.cell);this.positionChanged()};mxHandle.prototype.destroy=function(){null!=this.shape&&(this.shape.destroy(),this.shape=null)}; +function mxVertexHandler(a){null!=a&&(this.state=a,this.init(),this.escapeHandler=mxUtils.bind(this,function(a,c){this.livePreview&&null!=this.index&&(this.state.view.graph.cellRenderer.redraw(this.state,!0),this.state.view.invalidate(this.state.cell),this.state.invalid=!1,this.state.view.validate());this.reset()}),this.state.view.graph.addListener(mxEvent.ESCAPE,this.escapeHandler))}mxVertexHandler.prototype.graph=null;mxVertexHandler.prototype.state=null;mxVertexHandler.prototype.singleSizer=!1; +mxVertexHandler.prototype.index=null;mxVertexHandler.prototype.allowHandleBoundsCheck=!0;mxVertexHandler.prototype.handleImage=null;mxVertexHandler.prototype.tolerance=0;mxVertexHandler.prototype.rotationEnabled=!1;mxVertexHandler.prototype.parentHighlightEnabled=!1;mxVertexHandler.prototype.rotationRaster=!0;mxVertexHandler.prototype.rotationCursor="crosshair";mxVertexHandler.prototype.livePreview=!1;mxVertexHandler.prototype.manageSizers=!1;mxVertexHandler.prototype.constrainGroupByChildren=!1; +mxVertexHandler.prototype.rotationHandleVSpacing=-16;mxVertexHandler.prototype.horizontalOffset=0;mxVertexHandler.prototype.verticalOffset=0; +mxVertexHandler.prototype.init=function(){this.graph=this.state.view.graph;this.selectionBounds=this.getSelectionBounds(this.state);this.bounds=new mxRectangle(this.selectionBounds.x,this.selectionBounds.y,this.selectionBounds.width,this.selectionBounds.height);this.selectionBorder=this.createSelectionShape(this.bounds);this.selectionBorder.dialect=this.graph.dialect!=mxConstants.DIALECT_SVG?mxConstants.DIALECT_VML:mxConstants.DIALECT_SVG;this.selectionBorder.pointerEvents=!1;this.selectionBorder.rotation= +Number(this.state.style[mxConstants.STYLE_ROTATION]||"0");this.selectionBorder.init(this.graph.getView().getOverlayPane());mxEvent.redirectMouseEvents(this.selectionBorder.node,this.graph,this.state);this.graph.isCellMovable(this.state.cell)&&this.selectionBorder.setCursor(mxConstants.CURSOR_MOVABLE_VERTEX);if(0>=mxGraphHandler.prototype.maxCells||this.graph.getSelectionCount()this.state.width&&2>this.state.height&&(this.labelShape=this.createSizer(mxConstants.CURSOR_MOVABLE_VERTEX,mxEvent.LABEL_HANDLE, +null,mxConstants.LABEL_HANDLE_FILLCOLOR),this.sizers.push(this.labelShape))}this.isRotationHandleVisible()&&(this.rotationShape=this.createSizer(this.rotationCursor,mxEvent.ROTATION_HANDLE,mxConstants.HANDLE_SIZE+3,mxConstants.HANDLE_FILLCOLOR),this.sizers.push(this.rotationShape));this.customHandles=this.createCustomHandles();this.redraw();this.constrainGroupByChildren&&this.updateMinBounds()}; +mxVertexHandler.prototype.isRotationHandleVisible=function(){return this.graph.isEnabled()&&this.rotationEnabled&&this.graph.isCellRotatable(this.state.cell)&&(0>=mxGraphHandler.prototype.maxCells||this.graph.getSelectionCount()this.graph.tolerance||Math.abs(a.getGraphY()-this.startY)>this.graph.tolerance)&&(this.inTolerance=!1)};mxVertexHandler.prototype.updateHint=function(a){};mxVertexHandler.prototype.removeHint=function(){};mxVertexHandler.prototype.roundAngle=function(a){return Math.round(10*a)/10}; +mxVertexHandler.prototype.roundLength=function(a){return Math.round(a)}; +mxVertexHandler.prototype.mouseMove=function(a,b){b.isConsumed()||null==this.index?this.graph.isMouseDown||null==this.getHandleForEvent(b)||b.consume(!1):(this.checkTolerance(b),this.inTolerance||(this.index<=mxEvent.CUSTOM_HANDLE?null!=this.customHandles&&(this.customHandles[mxEvent.CUSTOM_HANDLE-this.index].processEvent(b),this.customHandles[mxEvent.CUSTOM_HANDLE-this.index].active=!0):this.index==mxEvent.LABEL_HANDLE?this.moveLabel(b):this.index==mxEvent.ROTATION_HANDLE?this.rotateVertex(b):this.resizeVertex(b), +this.updateHint(b)),b.consume())};mxVertexHandler.prototype.moveLabel=function(a){var b=new mxPoint(a.getGraphX(),a.getGraphY()),c=this.graph.view.translate,d=this.graph.view.scale;this.graph.isGridEnabledEvent(a.getEvent())&&(b.x=(this.graph.snap(b.x/d-c.x)+c.x)*d,b.y=(this.graph.snap(b.y/d-c.y)+c.y)*d);this.moveSizerTo(this.sizers[null!=this.rotationShape?this.sizers.length-2:this.sizers.length-1],b.x,b.y)}; +mxVertexHandler.prototype.rotateVertex=function(a){var b=new mxPoint(a.getGraphX(),a.getGraphY()),c=this.state.x+this.state.width/2-b.x,d=this.state.y+this.state.height/2-b.y;this.currentAlpha=0!=c?180*Math.atan(d/c)/Math.PI+90:0>d?180:0;0k.x+k.width&&(this.unscaledBounds.width-=this.unscaledBounds.x+this.unscaledBounds.width-k.x-k.width),this.unscaledBounds.y+this.unscaledBounds.height> +k.y+k.height&&(this.unscaledBounds.height-=this.unscaledBounds.y+this.unscaledBounds.height-k.y-k.height)));this.bounds=new mxRectangle((null!=this.parentState?this.parentState.x:e.x*f)+this.unscaledBounds.x*f,(null!=this.parentState?this.parentState.y:e.y*f)+this.unscaledBounds.y*f,this.unscaledBounds.width*f,this.unscaledBounds.height*f);g.relative&&null!=this.parentState&&(this.bounds.x+=this.state.x-this.parentState.x,this.bounds.y+=this.state.y-this.parentState.y);g=Math.cos(c);k=Math.sin(c); +c=new mxPoint(this.bounds.getCenterX(),this.bounds.getCenterY());l=c.x-b.x;d=c.y-b.y;b=g*l-k*d-l;c=k*l+g*d-d;l=this.bounds.x-this.state.x;d=this.bounds.y-this.state.y;e=g*l-k*d;g=k*l+g*d;this.bounds.x+=b;this.bounds.y+=c;this.unscaledBounds.x=this.roundLength(this.unscaledBounds.x+b/f);this.unscaledBounds.y=this.roundLength(this.unscaledBounds.y+c/f);this.unscaledBounds.width=this.roundLength(this.unscaledBounds.width);this.unscaledBounds.height=this.roundLength(this.unscaledBounds.height);this.graph.isCellCollapsed(this.state.cell)|| +0==b&&0==c?this.childOffsetY=this.childOffsetX=0:(this.childOffsetX=this.state.x-this.bounds.x+e,this.childOffsetY=this.state.y-this.bounds.y+g);this.livePreview&&this.updateLivePreview(a);null!=this.preview&&this.drawPreview()}; +mxVertexHandler.prototype.updateLivePreview=function(a){var b=this.graph.view.scale,c=this.graph.view.translate;a=this.state.clone();this.state.x=this.bounds.x;this.state.y=this.bounds.y;this.state.origin=new mxPoint(this.state.x/b-c.x,this.state.y/b-c.y);this.state.width=this.bounds.width;this.state.height=this.bounds.height;this.state.unscaledWidth=null;b=this.state.absoluteOffset;new mxPoint(b.x,b.y);this.state.absoluteOffset.x=0;this.state.absoluteOffset.y=0;b=this.graph.getCellGeometry(this.state.cell); +null!=b&&(c=b.offset||this.EMPTY_POINT,null==c||b.relative||(this.state.absoluteOffset.x=this.state.view.scale*c.x,this.state.absoluteOffset.y=this.state.view.scale*c.y),this.state.view.updateVertexLabelOffset(this.state));this.state.view.graph.cellRenderer.redraw(this.state,!0);this.state.view.invalidate(this.state.cell);this.state.invalid=!1;this.state.view.validate();this.redrawHandles();this.state.setState(a)}; +mxVertexHandler.prototype.mouseUp=function(a,b){if(null!=this.index&&null!=this.state){var c=new mxPoint(b.getGraphX(),b.getGraphY());this.graph.getModel().beginUpdate();try{if(this.index<=mxEvent.CUSTOM_HANDLE)null!=this.customHandles&&(this.customHandles[mxEvent.CUSTOM_HANDLE-this.index].active=!1,this.customHandles[mxEvent.CUSTOM_HANDLE-this.index].execute());else if(this.index==mxEvent.ROTATION_HANDLE)if(null!=this.currentAlpha){var d=this.currentAlpha-(this.state.style[mxConstants.STYLE_ROTATION]|| +0);0!=d&&this.rotateCell(this.state.cell,d)}else this.rotateClick();else{var e=this.graph.isGridEnabledEvent(b.getEvent()),f=mxUtils.toRadians(this.state.style[mxConstants.STYLE_ROTATION]||"0"),g=Math.cos(-f),k=Math.sin(-f),l=c.x-this.startX,m=c.y-this.startY,c=k*l+g*m,l=g*l-k*m,m=c,n=this.graph.view.scale,p=this.isRecursiveResize(this.state,b);this.resizeCell(this.state.cell,this.roundLength(l/n),this.roundLength(m/n),this.index,e,this.isConstrainedEvent(b),p)}}finally{this.graph.getModel().endUpdate()}b.consume(); +this.reset()}};mxVertexHandler.prototype.isRecursiveResize=function(a,b){return this.graph.isRecursiveResize(this.state)};mxVertexHandler.prototype.rotateClick=function(){}; +mxVertexHandler.prototype.rotateCell=function(a,b,c){if(0!=b){var d=this.graph.getModel();if(d.isVertex(a)||d.isEdge(a)){if(!d.isEdge(a)){var e=this.graph.view.getState(a),e=null!=e?e.style:this.graph.getCellStyle(a);null!=e&&this.graph.setCellStyles(mxConstants.STYLE_ROTATION,(e[mxConstants.STYLE_ROTATION]||0)+b,[a])}e=this.graph.getCellGeometry(a);if(null!=e){var f=this.graph.getCellGeometry(c);null==f||d.isEdge(c)||(e=e.clone(),e.rotate(b,new mxPoint(f.width/2,f.height/2)),d.setGeometry(a,e)); +if(d.isVertex(a)&&!e.relative||d.isEdge(a))for(c=d.getChildCount(a),e=0;ed&&(a+=c,e&&(a=this.graph.snap(a/f)*f));if(0==d||3==d||5==d)p+=b,e&&(p=this.graph.snap(p/f)*f);else if(2==d||4==d||7==d)q+=b,e&&(q=this.graph.snap(q/ +f)*f);e=q-p;c=r-a;k&&(k=this.graph.getCellGeometry(this.state.cell),null!=k&&(k=k.width/k.height,1==d||2==d||7==d||6==d?e=c*k:c=e/k,0==d&&(p=q-e,a=r-c)));l&&(e+=e-m,c+=c-n,p+=t-(p+e/2),a+=u-(a+c/2));0>e&&(p+=e,e=Math.abs(e));0>c&&(a+=c,c=Math.abs(c));d=new mxRectangle(p+g.x*f,a+g.y*f,e,c);null!=this.minBounds&&(d.width=Math.max(d.width,this.minBounds.x*f+this.minBounds.width*f+Math.max(0,this.x0*f-d.x)),d.height=Math.max(d.height,this.minBounds.y*f+this.minBounds.height*f+Math.max(0,this.y0*f-d.y))); +return d};mxVertexHandler.prototype.redraw=function(){this.selectionBounds=this.getSelectionBounds(this.state);this.bounds=new mxRectangle(this.selectionBounds.x,this.selectionBounds.y,this.selectionBounds.width,this.selectionBounds.height);this.redrawHandles();this.drawPreview()}; +mxVertexHandler.prototype.getHandlePadding=function(){var a=new mxPoint(0,0),b=this.tolerance;null!=this.sizers&&0=mxGraphHandler.prototype.maxCells)this.bends=this.createBends(),this.isVirtualBendsEnabled()&&(this.virtualBends=this.createVirtualBends());this.label=new mxPoint(this.state.absoluteOffset.x,this.state.absoluteOffset.y);this.labelShape=this.createLabelHandleShape();this.initBend(this.labelShape);this.labelShape.setCursor(mxConstants.CURSOR_LABEL_HANDLE);this.customHandles=this.createCustomHandles();this.redraw()};mxEdgeHandler.prototype.createCustomHandles=function(){return null}; +mxEdgeHandler.prototype.isVirtualBendsEnabled=function(a){return this.virtualBendsEnabled&&(null==this.state.style[mxConstants.STYLE_EDGE]||this.state.style[mxConstants.STYLE_EDGE]==mxConstants.NONE||1==this.state.style[mxConstants.STYLE_NOEDGESTYLE])&&"arrow"!=mxUtils.getValue(this.state.style,mxConstants.STYLE_SHAPE,null)};mxEdgeHandler.prototype.isAddPointEvent=function(a){return mxEvent.isShiftDown(a)};mxEdgeHandler.prototype.isRemovePointEvent=function(a){return mxEvent.isShiftDown(a)}; +mxEdgeHandler.prototype.getSelectionPoints=function(a){return a.absolutePoints};mxEdgeHandler.prototype.createParentHighlightShape=function(a){a=new mxRectangleShape(a,null,this.getSelectionColor());a.strokewidth=this.getSelectionStrokeWidth();a.isDashed=this.isSelectionDashed();return a};mxEdgeHandler.prototype.createSelectionShape=function(a){a=new this.state.shape.constructor;a.outline=!0;a.apply(this.state);a.isDashed=this.isSelectionDashed();a.stroke=this.getSelectionColor();a.isShadow=!1;return a}; +mxEdgeHandler.prototype.getSelectionColor=function(){return mxConstants.EDGE_SELECTION_COLOR};mxEdgeHandler.prototype.getSelectionStrokeWidth=function(){return mxConstants.EDGE_SELECTION_STROKEWIDTH};mxEdgeHandler.prototype.isSelectionDashed=function(){return mxConstants.EDGE_SELECTION_DASHED};mxEdgeHandler.prototype.isConnectableCell=function(a){return!0};mxEdgeHandler.prototype.getCellAt=function(a,b){return this.outlineConnect?null:this.graph.getCellAt(a,b)}; +mxEdgeHandler.prototype.createMarker=function(){var a=new mxCellMarker(this.graph),b=this;a.getCell=function(a){var c=mxCellMarker.prototype.getCell.apply(this,arguments);c!=b.state.cell&&null!=c||null==b.currentPoint||(c=b.graph.getCellAt(b.currentPoint.x,b.currentPoint.y));if(null!=c&&!this.graph.isCellConnectable(c)){var e=this.graph.getModel().getParent(c);this.graph.getModel().isVertex(e)&&this.graph.isCellConnectable(e)&&(c=e)}e=b.graph.getModel();if(this.graph.isSwimlane(c)&&null!=b.currentPoint&& +this.graph.hitsSwimlaneContent(c,b.currentPoint.x,b.currentPoint.y)||!b.isConnectableCell(c)||c==b.state.cell||null!=c&&!b.graph.connectableEdges&&e.isEdge(c)||e.isAncestor(b.state.cell,c))c=null;this.graph.isCellConnectable(c)||(c=null);return c};a.isValidState=function(a){var c=b.graph.getModel(),c=b.graph.view.getTerminalPort(a,b.graph.view.getState(c.getTerminal(b.state.cell,!b.isSource)),!b.isSource),c=null!=c?c.cell:null;b.error=b.validateConnection(b.isSource?a.cell:c,b.isSource?c:a.cell); +return null==b.error};return a};mxEdgeHandler.prototype.validateConnection=function(a,b){return this.graph.getEdgeValidationError(this.state.cell,a,b)}; +mxEdgeHandler.prototype.createBends=function(){for(var a=this.state.cell,b=[],c=0;c +mxEvent.VIRTUAL_HANDLE&&null!=this.customHandles)for(c=0;cmxEvent.VIRTUAL_HANDLE&&(c[this.index-1]=d)}return null!=e?e:c}; +mxEdgeHandler.prototype.isOutlineConnectEvent=function(a){var b=mxUtils.getOffset(this.graph.container),c=a.getEvent(),d=mxEvent.getClientX(c),c=mxEvent.getClientY(c),e=document.documentElement,f=this.currentPoint.x-this.graph.container.scrollLeft+b.x-((window.pageXOffset||e.scrollLeft)-(e.clientLeft||0)),b=this.currentPoint.y-this.graph.container.scrollTop+b.y-((window.pageYOffset||e.scrollTop)-(e.clientTop||0));return this.outlineConnect&&!mxEvent.isShiftDown(a.getEvent())&&(a.isSource(this.marker.highlight.shape)|| +mxEvent.isAltDown(a.getEvent())&&null!=a.getState()||this.marker.highlight.isHighlightAt(d,c)||(f!=d||b!=c)&&null==a.getState()&&this.marker.highlight.isHighlightAt(f,b))}; +mxEdgeHandler.prototype.updatePreviewState=function(a,b,c,d,e){var f=this.isSource?c:this.state.getVisibleTerminalState(!0),g=this.isTarget?c:this.state.getVisibleTerminalState(!1),k=this.graph.getConnectionConstraint(a,f,!0),l=this.graph.getConnectionConstraint(a,g,!1),m=this.constraintHandler.currentConstraint;null==m&&e&&(null!=c?(d.isSource(this.marker.highlight.shape)&&(b=new mxPoint(d.getGraphX(),d.getGraphY())),m=this.graph.getOutlineConstraint(b,c,d),this.constraintHandler.setFocus(d,c,this.isSource), +this.constraintHandler.currentConstraint=m,this.constraintHandler.currentPoint=b):m=new mxConnectionConstraint);if(this.outlineConnect&&null!=this.marker.highlight&&null!=this.marker.highlight.shape){var n=this.graph.view.scale;null!=this.constraintHandler.currentConstraint&&null!=this.constraintHandler.currentFocus?(this.marker.highlight.shape.stroke=e?mxConstants.OUTLINE_HIGHLIGHT_COLOR:"transparent",this.marker.highlight.shape.strokewidth=mxConstants.OUTLINE_HIGHLIGHT_STROKEWIDTH/n/n,this.marker.highlight.repaint()): +this.marker.hasValidState()&&(this.marker.highlight.shape.stroke=this.marker.getValidState()==d.getState()?mxConstants.DEFAULT_VALID_COLOR:"transparent",this.marker.highlight.shape.strokewidth=mxConstants.HIGHLIGHT_STROKEWIDTH/n/n,this.marker.highlight.repaint())}this.isSource?k=m:this.isTarget&&(l=m);if(this.isSource||this.isTarget)null!=m&&null!=m.point?(a.style[this.isSource?mxConstants.STYLE_EXIT_X:mxConstants.STYLE_ENTRY_X]=m.point.x,a.style[this.isSource?mxConstants.STYLE_EXIT_Y:mxConstants.STYLE_ENTRY_Y]= +m.point.y):(delete a.style[this.isSource?mxConstants.STYLE_EXIT_X:mxConstants.STYLE_ENTRY_X],delete a.style[this.isSource?mxConstants.STYLE_EXIT_Y:mxConstants.STYLE_ENTRY_Y]);a.setVisibleTerminalState(f,!0);a.setVisibleTerminalState(g,!1);this.isSource&&null==f||a.view.updateFixedTerminalPoint(a,f,!0,k);this.isTarget&&null==g||a.view.updateFixedTerminalPoint(a,g,!1,l);(this.isSource||this.isTarget)&&null==c&&(a.setAbsoluteTerminalPoint(b,this.isSource),null==this.marker.getMarkedState()&&(this.error= +this.graph.allowDanglingEdges?null:""));a.view.updatePoints(a,this.points,f,g);a.view.updateFloatingTerminalPoints(a,f,g)}; +mxEdgeHandler.prototype.mouseMove=function(a,b){if(null!=this.index&&null!=this.marker){this.currentPoint=this.getPointForEvent(b);this.error=null;!this.graph.isIgnoreTerminalEvent(b.getEvent())&&mxEvent.isShiftDown(b.getEvent())&&null!=this.snapPoint&&(Math.abs(this.snapPoint.x-this.currentPoint.x)mxEvent.VIRTUAL_HANDLE)null!= +this.customHandles&&this.customHandles[mxEvent.CUSTOM_HANDLE-this.index].processEvent(b);else if(this.isLabel)this.label.x=this.currentPoint.x,this.label.y=this.currentPoint.y;else{this.points=this.getPreviewPoints(this.currentPoint,b);var c=this.isSource||this.isTarget?this.getPreviewTerminalState(b):null;if(null!=this.constraintHandler.currentConstraint&&null!=this.constraintHandler.currentFocus&&null!=this.constraintHandler.currentPoint)this.currentPoint=this.constraintHandler.currentPoint.clone(); +else if(this.outlineConnect){var d=this.isSource||this.isTarget?this.isOutlineConnectEvent(b):!1;d?c=this.marker.highlight.state:null!=c&&c!=b.getState()&&null!=this.marker.highlight.shape&&(this.marker.highlight.shape.stroke="transparent",this.marker.highlight.repaint(),c=null)}null!=c&&this.graph.isCellLocked(c.cell)&&(c=null,this.marker.reset());var e=this.clonePreviewState(this.currentPoint,null!=c?c.cell:null);this.updatePreviewState(e,this.currentPoint,c,b,d);this.setPreviewColor(null==this.error? +this.marker.validColor:this.marker.invalidColor);this.abspoints=e.absolutePoints;this.active=!0}this.updateHint(b,this.currentPoint);this.drawPreview();mxEvent.consume(b.getEvent());b.consume()}else mxClient.IS_IE&&null!=this.getHandleForEvent(b)&&b.consume(!1)}; +mxEdgeHandler.prototype.mouseUp=function(a,b){if(null!=this.index&&null!=this.marker){var c=this.state.cell;if(b.getX()!=this.startX||b.getY()!=this.startY){var d=!this.graph.isIgnoreTerminalEvent(b.getEvent())&&this.graph.isCloneEvent(b.getEvent())&&this.cloneEnabled&&this.graph.isCellsCloneable();if(null!=this.error)0mxEvent.VIRTUAL_HANDLE){if(null!=this.customHandles){var e=this.graph.getModel(); +e.beginUpdate();try{this.customHandles[mxEvent.CUSTOM_HANDLE-this.index].execute()}finally{e.endUpdate()}}}else if(this.isLabel)this.moveLabel(this.state,this.label.x,this.label.y);else if(this.isSource||this.isTarget){var f=null;null!=this.constraintHandler.currentConstraint&&null!=this.constraintHandler.currentFocus&&(f=this.constraintHandler.currentFocus.cell);null==f&&this.marker.hasValidState()&&null!=this.marker.highlight&&null!=this.marker.highlight.shape&&"transparent"!=this.marker.highlight.shape.stroke&& +"white"!=this.marker.highlight.shape.stroke&&(f=this.marker.validState.cell);if(null!=f){var e=this.graph.getModel(),g=e.getParent(c);e.beginUpdate();try{if(d){var k=e.getGeometry(c),d=this.graph.cloneCells([c])[0];e.add(g,d,e.getChildCount(g));null!=k&&(k=k.clone(),e.setGeometry(d,k));var l=e.getTerminal(c,!this.isSource);this.graph.connectCell(d,l,!this.isSource);c=d}c=this.connect(c,f,this.isSource,d,b)}finally{e.endUpdate()}}else this.graph.isAllowDanglingEdges()&&(e=this.abspoints[this.isSource? +0:this.abspoints.length-1],e.x=this.roundLength(e.x/this.graph.view.scale-this.graph.view.translate.x),e.y=this.roundLength(e.y/this.graph.view.scale-this.graph.view.translate.y),f=this.graph.getView().getState(this.graph.getModel().getParent(c)),null!=f&&(e.x-=f.origin.x,e.y-=f.origin.y),e.x-=this.graph.panDx/this.graph.view.scale,e.y-=this.graph.panDy/this.graph.view.scale,c=this.changeTerminalPoint(c,e,this.isSource,d))}else this.active?c=this.changePoints(c,this.points,d):(this.graph.getView().invalidate(this.state.cell), +this.graph.getView().validate(this.state.cell))}null!=this.marker&&(this.reset(),c!=this.state.cell&&this.graph.setSelectionCell(c));b.consume()}}; +mxEdgeHandler.prototype.reset=function(){this.active&&this.refresh();this.snapPoint=this.points=this.label=this.index=this.error=null;this.active=this.isTarget=this.isSource=this.isLabel=!1;if(this.livePreview&&null!=this.sizers)for(var a=0;a");this.div.style.visibility="";mxUtils.fit(this.div)}}; +mxTooltipHandler.prototype.destroy=function(){this.destroyed||(this.graph.removeMouseListener(this),mxEvent.release(this.div),null!=this.div&&null!=this.div.parentNode&&this.div.parentNode.removeChild(this.div),this.destroyed=!0,this.div=null)};function mxCellTracker(a,b,c){mxCellMarker.call(this,a,b);this.graph.addMouseListener(this);null!=c&&(this.getCell=c);mxClient.IS_IE&&mxEvent.addListener(window,"unload",mxUtils.bind(this,function(){this.destroy()}))}mxUtils.extend(mxCellTracker,mxCellMarker); +mxCellTracker.prototype.mouseDown=function(a,b){};mxCellTracker.prototype.mouseMove=function(a,b){this.isEnabled()&&this.process(b)};mxCellTracker.prototype.mouseUp=function(a,b){};mxCellTracker.prototype.destroy=function(){this.destroyed||(this.destroyed=!0,this.graph.removeMouseListener(this),mxCellMarker.prototype.destroy.apply(this))}; +function mxCellHighlight(a,b,c,d){null!=a&&(this.graph=a,this.highlightColor=null!=b?b:mxConstants.DEFAULT_VALID_COLOR,this.strokeWidth=null!=c?c:mxConstants.HIGHLIGHT_STROKEWIDTH,this.dashed=null!=d?d:!1,this.opacity=mxConstants.HIGHLIGHT_OPACITY,this.repaintHandler=mxUtils.bind(this,function(){if(null!=this.state){var a=this.graph.view.getState(this.state.cell);null==a?this.hide():(this.state=a,this.repaint())}}),this.graph.getView().addListener(mxEvent.SCALE,this.repaintHandler),this.graph.getView().addListener(mxEvent.TRANSLATE, +this.repaintHandler),this.graph.getView().addListener(mxEvent.SCALE_AND_TRANSLATE,this.repaintHandler),this.graph.getModel().addListener(mxEvent.CHANGE,this.repaintHandler),this.resetHandler=mxUtils.bind(this,function(){this.hide()}),this.graph.getView().addListener(mxEvent.DOWN,this.resetHandler),this.graph.getView().addListener(mxEvent.UP,this.resetHandler))}mxCellHighlight.prototype.keepOnTop=!1;mxCellHighlight.prototype.graph=!0;mxCellHighlight.prototype.state=null; +mxCellHighlight.prototype.spacing=2;mxCellHighlight.prototype.resetHandler=null;mxCellHighlight.prototype.setHighlightColor=function(a){this.highlightColor=a;null!=this.shape&&(this.shape.stroke=a)};mxCellHighlight.prototype.drawHighlight=function(){this.shape=this.createShape();this.repaint();this.keepOnTop||this.shape.node.parentNode.firstChild==this.shape.node||this.shape.node.parentNode.insertBefore(this.shape.node,this.shape.node.parentNode.firstChild)}; +mxCellHighlight.prototype.createShape=function(){var a=this.graph.cellRenderer.createShape(this.state);a.svgStrokeTolerance=this.graph.tolerance;a.points=this.state.absolutePoints;a.apply(this.state);a.stroke=this.highlightColor;a.opacity=this.opacity;a.isDashed=this.dashed;a.isShadow=!1;a.dialect=this.graph.dialect!=mxConstants.DIALECT_SVG?mxConstants.DIALECT_VML:mxConstants.DIALECT_SVG;a.init(this.graph.getView().getOverlayPane());mxEvent.redirectMouseEvents(a.node,this.graph,this.state);this.graph.dialect!= +mxConstants.DIALECT_SVG?a.pointerEvents=!1:a.svgPointerEvents="stroke";return a};mxCellHighlight.prototype.getStrokeWidth=function(a){return this.strokeWidth}; +mxCellHighlight.prototype.repaint=function(){if(null!=this.state&&null!=this.shape){this.shape.scale=this.state.view.scale;this.graph.model.isEdge(this.state.cell)?(this.shape.strokewidth=this.getStrokeWidth(),this.shape.points=this.state.absolutePoints,this.shape.outline=!1):(this.shape.bounds=new mxRectangle(this.state.x-this.spacing,this.state.y-this.spacing,this.state.width+2*this.spacing,this.state.height+2*this.spacing),this.shape.rotation=Number(this.state.style[mxConstants.STYLE_ROTATION]|| +"0"),this.shape.strokewidth=this.getStrokeWidth()/this.state.view.scale,this.shape.outline=!0);null!=this.state.shape&&this.shape.setCursor(this.state.shape.getCursor());if(mxClient.IS_QUIRKS||8==document.documentMode)"transparent"==this.shape.stroke?(this.shape.stroke="white",this.shape.opacity=1):this.shape.opacity=this.opacity;this.shape.redraw()}};mxCellHighlight.prototype.hide=function(){this.highlight(null)}; +mxCellHighlight.prototype.highlight=function(a){this.state!=a&&(null!=this.shape&&(this.shape.destroy(),this.shape=null),this.state=a,null!=this.state&&this.drawHighlight())};mxCellHighlight.prototype.isHighlightAt=function(a,b){var c=!1;if(null!=this.shape&&null!=document.elementFromPoint&&!mxClient.IS_QUIRKS)for(var d=document.elementFromPoint(a,b);null!=d;){if(d==this.shape.node){c=!0;break}d=d.parentNode}return c}; +mxCellHighlight.prototype.destroy=function(){this.graph.getView().removeListener(this.resetHandler);this.graph.getView().removeListener(this.repaintHandler);this.graph.getModel().removeListener(this.repaintHandler);null!=this.shape&&(this.shape.destroy(),this.shape=null)}; +function mxDefaultKeyHandler(a){if(null!=a){this.editor=a;this.handler=new mxKeyHandler(a.graph);var b=this.handler.escape;this.handler.escape=function(c){b.apply(this,arguments);a.hideProperties();a.fireEvent(new mxEventObject(mxEvent.ESCAPE,"event",c))}}}mxDefaultKeyHandler.prototype.editor=null;mxDefaultKeyHandler.prototype.handler=null; +mxDefaultKeyHandler.prototype.bindAction=function(a,b,c){var d=mxUtils.bind(this,function(){this.editor.execute(b)});c?this.handler.bindControlKey(a,d):this.handler.bindKey(a,d)};mxDefaultKeyHandler.prototype.destroy=function(){this.handler.destroy();this.handler=null};function mxDefaultPopupMenu(a){this.config=a}mxDefaultPopupMenu.prototype.imageBasePath=null;mxDefaultPopupMenu.prototype.config=null; +mxDefaultPopupMenu.prototype.createMenu=function(a,b,c,d){if(null!=this.config){var e=this.createConditions(a,c,d);this.addItems(a,b,c,d,e,this.config.firstChild,null)}}; +mxDefaultPopupMenu.prototype.addItems=function(a,b,c,d,e,f,g){for(var k=!1;null!=f;){if("add"==f.nodeName){var l=f.getAttribute("if");if(null==l||e[l]){var l=f.getAttribute("as"),l=mxResources.get(l)||l,m=mxUtils.eval(mxUtils.getTextContent(f)),n=f.getAttribute("action"),p=f.getAttribute("icon"),q=f.getAttribute("iconCls"),r=f.getAttribute("enabled-if"),r=null==r||e[r];k&&(b.addSeparator(g),k=!1);null!=p&&this.imageBasePath&&(p=this.imageBasePath+p);l=this.addAction(b,a,l,p,m,n,c,g,q,r);this.addItems(a, +b,c,d,e,f.firstChild,l)}}else"separator"==f.nodeName&&(k=!0);f=f.nextSibling}};mxDefaultPopupMenu.prototype.addAction=function(a,b,c,d,e,f,g,k,l,m){return a.addItem(c,d,function(a){"function"==typeof e&&e.call(b,b,g,a);null!=f&&b.execute(f,g,a)},k,l,m)}; +mxDefaultPopupMenu.prototype.createConditions=function(a,b,c){var d=a.graph.getModel(),e=d.getChildCount(b),f=[];f.nocell=null==b;f.ncells=1 "+b.convertValueToString(c)+a),c=b.getModel().getParent(c);return this.getRootTitle()+a};mxEditor.prototype.getRootTitle=function(){var a=this.graph.getModel().getRoot();return this.graph.convertValueToString(a)};mxEditor.prototype.undo=function(){this.undoManager.undo()};mxEditor.prototype.redo=function(){this.undoManager.redo()}; +mxEditor.prototype.groupCells=function(){var a=null!=this.groupBorderSize?this.groupBorderSize:this.graph.gridSize;return this.graph.groupCells(this.createGroup(),a)};mxEditor.prototype.createGroup=function(){return this.graph.getModel().cloneCell(this.defaultGroup)};mxEditor.prototype.open=function(a){if(null!=a){var b=mxUtils.load(a).getXml();this.readGraphModel(b.documentElement);this.filename=a;this.fireEvent(new mxEventObject(mxEvent.OPEN,"filename",a))}}; +mxEditor.prototype.readGraphModel=function(a){(new mxCodec(a.ownerDocument)).decode(a,this.graph.getModel());this.resetHistory()};mxEditor.prototype.save=function(a,b){a=a||this.getUrlPost();if(null!=a&&0n&&(c-=c+k-n);n=l.y+l.height;d+m>n&&(d-=d+m-n)}}else null!=k&&(c-=k.x*f,d-=k.y*f)}}g=g.clone();g.x=this.graph.snap(c/f-this.graph.getView().translate.x-this.graph.gridSize/2);g.y=this.graph.snap(d/f-this.graph.getView().translate.y-this.graph.gridSize/2);b.setGeometry(g);null==a&&(a=this.graph.getDefaultParent());this.cycleAttribute(b);this.fireEvent(new mxEventObject(mxEvent.BEFORE_ADD_VERTEX,"vertex",b,"parent",a));e.beginUpdate();try{b=this.graph.addCell(b, +a),null!=b&&(this.graph.constrainChild(b),this.fireEvent(new mxEventObject(mxEvent.ADD_VERTEX,"vertex",b)))}finally{e.endUpdate()}null!=b&&(this.graph.setSelectionCell(b),this.graph.scrollCellToVisible(b),this.fireEvent(new mxEventObject(mxEvent.AFTER_ADD_VERTEX,"vertex",b)));return b}; +mxEditor.prototype.destroy=function(){this.destroyed||(this.destroyed=!0,null!=this.tasks&&this.tasks.destroy(),null!=this.outline&&this.outline.destroy(),null!=this.properties&&this.properties.destroy(),null!=this.keyHandler&&this.keyHandler.destroy(),null!=this.rubberband&&this.rubberband.destroy(),null!=this.toolbar&&this.toolbar.destroy(),null!=this.graph&&this.graph.destroy(),this.templates=this.status=null)}; +var mxCodecRegistry={codecs:[],aliases:[],register:function(a){if(null!=a){var b=a.getName();mxCodecRegistry.codecs[b]=a;var c=mxUtils.getFunctionName(a.template.constructor);c!=b&&mxCodecRegistry.addAlias(c,b)}return a},addAlias:function(a,b){mxCodecRegistry.aliases[a]=b},getCodec:function(a){var b=null;if(null!=a){var b=mxUtils.getFunctionName(a),c=mxCodecRegistry.aliases[b];null!=c&&(b=c);b=mxCodecRegistry.codecs[b];if(null==b)try{b=new mxObjectCodec(new a),mxCodecRegistry.register(b)}catch(d){}}return b}}; +function mxCodec(a){this.document=a||mxUtils.createXmlDocument();this.objects=[]}mxCodec.prototype.document=null;mxCodec.prototype.objects=null;mxCodec.prototype.elements=null;mxCodec.prototype.encodeDefaults=!1;mxCodec.prototype.putObject=function(a,b){return this.objects[a]=b};mxCodec.prototype.getObject=function(a){var b=null;null!=a&&(b=this.objects[a],null==b&&(b=this.lookup(a),null==b&&(a=this.getElementById(a),null!=a&&(b=this.decode(a)))));return b};mxCodec.prototype.lookup=function(a){return null}; +mxCodec.prototype.getElementById=function(a){if(null==this.elements){if(null==this.document.documentElement)throw Error("mxCodec constructor needs document parameter");this.elements={};this.addElement(this.document.documentElement)}return this.elements[a]};mxCodec.prototype.addElement=function(a){if(a.nodeType==mxConstants.NODETYPE_ELEMENT){var b=a.getAttribute("id");null!=b&&null==this.elements[b]&&(this.elements[b]=a)}for(a=a.firstChild;null!=a;)this.addElement(a),a=a.nextSibling}; +mxCodec.prototype.getId=function(a){var b=null;null!=a&&(b=this.reference(a),null==b&&a instanceof mxCell&&(b=a.getId(),null==b&&(b=mxCellPath.create(a),0==b.length&&(b="root"))));return b};mxCodec.prototype.reference=function(a){return null};mxCodec.prototype.encode=function(a){var b=null;if(null!=a&&null!=a.constructor){var c=mxCodecRegistry.getCodec(a.constructor);null!=c?b=c.encode(this,a):mxUtils.isNode(a)?b=mxUtils.importNode(this.document,a,!0):mxLog.warn("mxCodec.encode: No codec for "+mxUtils.getFunctionName(a.constructor))}return b}; +mxCodec.prototype.decode=function(a,b){var c=null;if(null!=a&&a.nodeType==mxConstants.NODETYPE_ELEMENT){c=null;try{c=window[a.nodeName]}catch(d){}c=mxCodecRegistry.getCodec(c);null!=c?c=c.decode(this,a,b):(c=a.cloneNode(!0),c.removeAttribute("as"))}return c};mxCodec.prototype.encodeCell=function(a,b,c){b.appendChild(this.encode(a));if(null==c||c){c=a.getChildCount();for(var d=0;d +function edit_booking(pk){ + var csrf = $('input[name="csrfmiddlewaretoken"]').val(); + $.ajax({ + type: "POST", + url: "/", + data: { "target": pk, "create": 0, "csrfmiddlewaretoken": csrf}, + beforeSend: function(request) { + request.setRequestHeader("X-CSFRToken", csrf); + } + }).done(function(){ + window.location.replace("/wf/"); + }).fail(function(){}) +} + +

Bookings I Own

+ {% for booking in bookings %} +
+
    +
  • id: {{booking.id}}
  • +
  • lab: {{booking.resource.template.lab.lab_user.username}}
  • +
  • resource: {{booking.resource.template.name}}
  • +
  • start: {{booking.start}}
  • +
  • end: {{booking.end}}
  • +
  • purpose: {{booking.purpose}}
  • +
+
+ + +
+
+ {% endfor %} +

Bookings I Collaborate On

+ {% for booking in collab_bookings %} +
+
    +
  • id: {{booking.id}}
  • +
  • lab: {{booking.lab}}
  • +
  • resource: {{booking.resource_name}}
  • +
  • start: {{booking.start}}
  • +
  • end: {{booking.end}}
  • +
  • purpose: {{booking.purpose}}
  • +
+
+ + +
+
+ {% endfor %} +{% endblock %} diff --git a/src/templates/account/configuration_list.html b/src/templates/account/configuration_list.html new file mode 100644 index 0000000..ee61e97 --- /dev/null +++ b/src/templates/account/configuration_list.html @@ -0,0 +1,28 @@ +{% extends "base.html" %} +{% block content %} + + {% for config in configurations %} +
+
    +
  • id: {{config.id}}
  • +
  • name: {{config.name}}
  • +
  • description: {{config.description}}
  • +
+ +
+ {% endfor %} +{% endblock %} diff --git a/src/templates/account/details.html b/src/templates/account/details.html new file mode 100644 index 0000000..5641064 --- /dev/null +++ b/src/templates/account/details.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} +{% load staticfiles %} +{% block content %} +

Account Details

+ + + + +{% endblock content %} diff --git a/src/templates/account/image_list.html b/src/templates/account/image_list.html new file mode 100644 index 0000000..fb436df --- /dev/null +++ b/src/templates/account/image_list.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} +{% block content %} +

Images I Own

+ {% for image in images %} +
+
    +
  • id: {{image.id}}
  • +
  • lab: {{image.from_lab.name}}
  • +
  • name: {{image.name}}
  • +
  • description: {{image.description}}
  • +
  • host profile: {{image.host_type.name}}
  • +
+
+ {% endfor %} +

Public Images

+ {% for image in public_images %} +
+
    +
  • id: {{image.id}}
  • +
  • lab: {{image.from_lab.name}}
  • +
  • name: {{image.name}}
  • +
  • description: {{image.description}}
  • +
  • host profile: {{image.host_type.name}}
  • +
+
+ {% endfor %} +{% endblock %} diff --git a/src/templates/account/resource_list.html b/src/templates/account/resource_list.html new file mode 100644 index 0000000..482a000 --- /dev/null +++ b/src/templates/account/resource_list.html @@ -0,0 +1,28 @@ +{% extends "base.html" %} +{% block content %} + + {% for resource in resources %} +
+
    +
  • id: {{resource.id}}
  • +
  • name: {{resource.name}}
  • +
  • description: {{resource.description}}
  • +
+ +
+ {% endfor %} +{% endblock %} diff --git a/src/templates/account/user_list.html b/src/templates/account/user_list.html index 58ddda6..e564524 100644 --- a/src/templates/account/user_list.html +++ b/src/templates/account/user_list.html @@ -47,7 +47,7 @@ + + +{% endblock %} {% block basecontent %}
@@ -54,36 +120,61 @@ @@ -93,14 +184,15 @@
+ {% if title %}

{{ title }}

- - {% bootstrap_messages %} + {% endif %} +
{% bootstrap_messages %}
{% block content %} diff --git a/src/templates/booking/booking_calendar.html b/src/templates/booking/booking_calendar.html index b60db3c..349cb0a 100644 --- a/src/templates/booking/booking_calendar.html +++ b/src/templates/booking/booking_calendar.html @@ -4,6 +4,8 @@ {% load bootstrap3 %} {% block extrahead %} + {{ block.super }} + + {% bootstrap_field form.opsys %} {% bootstrap_field form.purpose %} {% bootstrap_field form.installer %} {% bootstrap_field form.scenario %} @@ -95,6 +98,15 @@ {% endbuttons %}
+ {% else %}

Please @@ -132,8 +144,61 @@ var booking_detail_prefix = "{% url 'booking:detail_prefix' %}"; var booking_delete_prefix = "{% url 'booking:delete_prefix' %}"; var user_timezone = "{{ request.user.userprofile.timezone }}" + {% autoescape off %} + var sup_installer_dict = {{ installer_filter }} + var sup_scenario_dict = {{ scenario_filter }} + {% endautoescape %} + + - -

- Resource: - - {{ booking.resource.name }} - -

-

- User: {{ booking.user.username }} -

-

- Start: {{ booking.start }} -

-

- End: {{ booking.end }} -

-

- Purpose: {{ booking.purpose }} -

-

- Operating System: {{ booking.opsys }} -

-

- Installer: {{ booking.installer }} -

-

- Scenario: {{ booking.scenario }} -

-

- Extensions Remaining: {{ booking.ext_count }} -

- -{% if user.is_authenticated %} -{% if user.get_username == booking.user.username %} -

- - Edit Booking - - - Delete Booking - -