Guided job execution page for Sahara
Adding a guide for job executions in data processing. This is meant to weave the existing panels and forms together in a way that can be understood by all users. Change-Id: Ib6e1c86ae18d18a1e2dbac6a172e70d61c79fff4 Partial-Implements: blueprint data-processing-rework-ui
This commit is contained in:
parent
22bdf25e23
commit
6ce3206a77
@ -36,6 +36,12 @@ class ClustersFilterAction(tables.FilterAction):
|
||||
('status', _("Status"), True))
|
||||
|
||||
|
||||
class ClusterGuide(tables.LinkAction):
|
||||
name = "cluster_guide"
|
||||
verbose_name = _("Cluster Creation Guide")
|
||||
url = "horizon:project:data_processing.wizard:cluster_guide"
|
||||
|
||||
|
||||
class CreateCluster(tables.LinkAction):
|
||||
name = "create"
|
||||
verbose_name = _("Launch Cluster")
|
||||
@ -156,7 +162,8 @@ class ClustersTable(tables.DataTable):
|
||||
row_class = UpdateRow
|
||||
cell_class = RichErrorCell
|
||||
status_columns = ["status"]
|
||||
table_actions = (CreateCluster,
|
||||
table_actions = (ClusterGuide,
|
||||
CreateCluster,
|
||||
ConfigureCluster,
|
||||
DeleteCluster,
|
||||
ClustersFilterAction)
|
||||
|
@ -20,6 +20,8 @@ from horizon import forms
|
||||
from horizon import workflows
|
||||
|
||||
from openstack_dashboard.api import sahara as saharaclient
|
||||
from openstack_dashboard.dashboards.project.data_processing \
|
||||
.utils import helpers
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
@ -106,6 +108,13 @@ class CreateDataSource(workflows.Workflow):
|
||||
context["source_url"],
|
||||
context.get("general_data_source_credential_user", None),
|
||||
context.get("general_data_source_credential_pass", None))
|
||||
|
||||
hlps = helpers.Helpers(request)
|
||||
if hlps.is_from_guide():
|
||||
request.session["guide_datasource_id"] = self.object.id
|
||||
request.session["guide_datasource_name"] = self.object.name
|
||||
self.success_url = (
|
||||
"horizon:project:data_processing.wizard:jobex_guide")
|
||||
return True
|
||||
except Exception:
|
||||
exceptions.handle(request)
|
||||
|
@ -16,7 +16,7 @@
|
||||
{% include "horizon/common/_form_fields.html" %}
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="form-help-block">
|
||||
<div class="form-help-block right">
|
||||
{{ form.get_help_text }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -25,6 +25,8 @@ from horizon import tables
|
||||
from horizon import tabs
|
||||
|
||||
from openstack_dashboard.api import sahara as saharaclient
|
||||
from openstack_dashboard.dashboards.project.data_processing.utils \
|
||||
import helpers
|
||||
|
||||
import openstack_dashboard.dashboards.project.data_processing. \
|
||||
job_binaries.forms as job_binary_forms
|
||||
@ -60,6 +62,13 @@ class CreateJobBinaryView(forms.ModalFormView):
|
||||
template_name = "project/data_processing.job_binaries/create.html"
|
||||
page_title = _("Create Job Binary")
|
||||
|
||||
def get_success_url(self):
|
||||
hlps = helpers.Helpers(self.request)
|
||||
if hlps.is_from_guide():
|
||||
self.success_url = reverse_lazy(
|
||||
"horizon:project:data_processing.wizard:jobex_guide")
|
||||
return self.success_url
|
||||
|
||||
|
||||
class JobBinaryDetailsView(tabs.TabView):
|
||||
tab_group_class = _tabs.JobBinaryDetailsTabs
|
||||
|
@ -40,6 +40,12 @@ class JobExecutionsFilterAction(tables.FilterAction):
|
||||
('status', _("Status"), True))
|
||||
|
||||
|
||||
class JobExecutionGuide(tables.LinkAction):
|
||||
name = "jobex_guide"
|
||||
verbose_name = _("Job Execution Guide")
|
||||
url = "horizon:project:data_processing.wizard:jobex_guide"
|
||||
|
||||
|
||||
class DeleteJobExecution(tables.DeleteAction):
|
||||
@staticmethod
|
||||
def action_present(count):
|
||||
@ -186,7 +192,8 @@ class JobExecutionsTable(tables.DataTable):
|
||||
row_class = UpdateRow
|
||||
status_columns = ["status"]
|
||||
verbose_name = _("Job Executions")
|
||||
table_actions = [DeleteJobExecution,
|
||||
table_actions = [JobExecutionGuide,
|
||||
DeleteJobExecution,
|
||||
JobExecutionsFilterAction]
|
||||
row_actions = [DeleteJobExecution,
|
||||
ReLaunchJobExistingCluster,
|
||||
|
@ -185,7 +185,7 @@
|
||||
var job_type;
|
||||
|
||||
$.ajax({
|
||||
url: "launch-job",
|
||||
url: "/project/data_processing/jobs/launch-job",
|
||||
data: {"job_id": $("#id_job").val(),
|
||||
"json": true },
|
||||
success: function (data, textStatus, jqXHR) {
|
||||
|
@ -27,6 +27,10 @@ urlpatterns = patterns('',
|
||||
url(r'^create-job$',
|
||||
views.CreateJobView.as_view(),
|
||||
name='create-job'),
|
||||
url(r'^create-job/'
|
||||
'(?P<guide_job_type>[^/]+)/$',
|
||||
views.CreateJobView.as_view(),
|
||||
name='create-job'),
|
||||
url(r'^launch-job$',
|
||||
views.LaunchJobView.as_view(),
|
||||
name='launch-job'),
|
||||
|
@ -14,6 +14,7 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
from django.core import urlresolvers
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from horizon import exceptions
|
||||
@ -21,7 +22,8 @@ from horizon import forms
|
||||
from horizon.forms import fields
|
||||
from horizon import workflows
|
||||
|
||||
|
||||
from openstack_dashboard.dashboards.project.data_processing \
|
||||
.utils import helpers
|
||||
from openstack_dashboard.api import sahara as saharaclient
|
||||
|
||||
|
||||
@ -85,6 +87,14 @@ class GeneralConfigAction(workflows.Action):
|
||||
required=False,
|
||||
widget=forms.Textarea(attrs={'rows': 4}))
|
||||
|
||||
def __init__(self, request, context, *args, **kwargs):
|
||||
super(GeneralConfigAction,
|
||||
self).__init__(request, context, *args, **kwargs)
|
||||
resolver_match = urlresolvers.resolve(request.path)
|
||||
if "guide_job_type" in resolver_match.kwargs:
|
||||
self.fields["job_type"].initial = (
|
||||
resolver_match.kwargs["guide_job_type"].lower())
|
||||
|
||||
def populate_job_type_choices(self, request, context):
|
||||
choices = [("pig", _("Pig")), ("hive", _("Hive")),
|
||||
("spark", _("Spark")),
|
||||
@ -119,19 +129,11 @@ class GeneralConfigAction(workflows.Action):
|
||||
class GeneralConfig(workflows.Step):
|
||||
action_class = GeneralConfigAction
|
||||
contributes = ("job_name", "job_type", "job_description", "main_binary")
|
||||
# Map needed because switchable fields need lower case
|
||||
# and our server is expecting upper case
|
||||
JOB_TYPE_MAP = {"pig": "Pig",
|
||||
"hive": "Hive",
|
||||
"spark": "Spark",
|
||||
"mapreduce": "MapReduce",
|
||||
"mapreduce.streaming": "MapReduce.Streaming",
|
||||
"java": "Java"}
|
||||
|
||||
def contribute(self, data, context):
|
||||
for k, v in data.items():
|
||||
if k == "job_type":
|
||||
context[k] = self.JOB_TYPE_MAP[v]
|
||||
context[k] = helpers.JOB_TYPE_MAP[v][1]
|
||||
else:
|
||||
context[k] = v
|
||||
return context
|
||||
@ -169,13 +171,21 @@ class CreateJob(workflows.Workflow):
|
||||
main_locations.append(context["main_binary"])
|
||||
|
||||
try:
|
||||
saharaclient.job_create(
|
||||
job = saharaclient.job_create(
|
||||
request,
|
||||
context["job_name"],
|
||||
context["job_type"],
|
||||
main_locations,
|
||||
lib_locations,
|
||||
context["job_description"])
|
||||
|
||||
hlps = helpers.Helpers(request)
|
||||
if hlps.is_from_guide():
|
||||
request.session["guide_job_id"] = job.id
|
||||
request.session["guide_job_type"] = context["job_type"]
|
||||
request.session["guide_job_name"] = context["job_name"]
|
||||
self.success_url = (
|
||||
"horizon:project:data_processing.wizard:jobex_guide")
|
||||
return True
|
||||
except Exception:
|
||||
exceptions.handle(request)
|
||||
|
@ -11,6 +11,8 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
import openstack_dashboard.dashboards.project.data_processing. \
|
||||
utils.workflow_helpers as work_helpers
|
||||
|
||||
@ -104,3 +106,27 @@ class Helpers(object):
|
||||
except Exception:
|
||||
return False
|
||||
return True
|
||||
|
||||
def reset_job_guide(self):
|
||||
try:
|
||||
self.request.session.update(
|
||||
{"guide_job_type": None,
|
||||
"guide_job_name": None,
|
||||
"guide_job_id": None,
|
||||
"guide_datasource_id": None,
|
||||
"guide_datasource_name": None, })
|
||||
except Exception:
|
||||
return False
|
||||
return True
|
||||
|
||||
# Map needed because switchable fields need lower case
|
||||
# and our server is expecting upper case. We will be
|
||||
# using the 0 index as the display name and the 1 index
|
||||
# as the value to pass to the server.
|
||||
JOB_TYPE_MAP = {"pig": [_("Pig"), "Pig"],
|
||||
"hive": [_("Hive"), "Hive"],
|
||||
"spark": [_("Spark"), "Spark"],
|
||||
"mapreduce": [_("MapReduce"), "MapReduce"],
|
||||
"mapreduce.streaming": [_("Streaming MapReduce"),
|
||||
"MapReduce.Streaming"],
|
||||
"java": [_("Java"), "Java"]}
|
||||
|
@ -84,3 +84,38 @@ class ChoosePluginForm(forms.SelfHandlingForm):
|
||||
|
||||
class Meta(object):
|
||||
name = _("Choose plugin type and version")
|
||||
|
||||
|
||||
class ChooseJobTypeForm(forms.SelfHandlingForm):
|
||||
guide_job_type = forms.ChoiceField(
|
||||
label=_("Job Type"),
|
||||
widget=forms.Select())
|
||||
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
super(ChooseJobTypeForm, self).__init__(request, *args, **kwargs)
|
||||
self.help_text_template = ("project/data_processing.wizard/"
|
||||
"_job_type_select_help.html")
|
||||
|
||||
self.fields["guide_job_type"].choices = \
|
||||
self.populate_guide_job_type_choices()
|
||||
|
||||
def populate_guide_job_type_choices(self):
|
||||
choices = [(x, helpers.JOB_TYPE_MAP[x][0])
|
||||
for x in helpers.JOB_TYPE_MAP]
|
||||
return choices
|
||||
|
||||
def handle(self, request, context):
|
||||
try:
|
||||
hlps = helpers.Helpers(request)
|
||||
job_type = context["guide_job_type"]
|
||||
if force_text(request.session.get("guide_job_type")) != (
|
||||
force_text(helpers.JOB_TYPE_MAP[job_type][0])):
|
||||
hlps.reset_job_guide()
|
||||
request.session["guide_job_type"] = (
|
||||
helpers.JOB_TYPE_MAP[job_type][0])
|
||||
messages.success(request, "Job type chosen")
|
||||
return True
|
||||
except Exception:
|
||||
exceptions.handle(request,
|
||||
_("Unable to set job type"))
|
||||
return False
|
||||
|
@ -0,0 +1,31 @@
|
||||
{% extends "horizon/common/_modal_form.html" %}
|
||||
|
||||
{% load url from future %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block form_id %}create-job-binary{% endblock %}
|
||||
{% block form_action %}
|
||||
{% url 'horizon:project:data_processing.wizard:job_type_select' %}
|
||||
{% endblock %}
|
||||
{% block form_attrs %}enctype="multipart/form-data"{% endblock %}
|
||||
|
||||
{% block modal-header %}{% trans "Choose job type" %}{% endblock %}
|
||||
|
||||
{% block modal-body %}
|
||||
<div class="left">
|
||||
<fieldset>
|
||||
{% include "horizon/common/_form_fields.html" %}
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="form-help-block right">
|
||||
{{ form.get_help_text }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<input class="btn btn-primary pull-right" id="plugin_select_btn"
|
||||
type="submit" value="{% trans "Select" %}"/>
|
||||
<a href="{% url 'horizon:project:data_processing.wizard:jobex_guide' %}"
|
||||
class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
|
||||
{% endblock %}
|
@ -0,0 +1,7 @@
|
||||
{% load i18n horizon %}
|
||||
<p class="well">
|
||||
{% blocktrans %}Select which type of job that you want to run.
|
||||
This choice will dictate which steps are required to successfully
|
||||
execute your job.
|
||||
{% endblocktrans %}
|
||||
</p>
|
@ -0,0 +1,11 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Choose job type" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title=_("Choose job type") %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'project/data_processing.wizard/_job_type_select.html' %}
|
||||
{% endblock %}
|
@ -0,0 +1,119 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% load url from future %}
|
||||
{% block title %}{% trans "Data Processing" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title=_("Guided Job Execution") %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
<div class="guide">
|
||||
<ol>
|
||||
<li>
|
||||
<div>
|
||||
<div>
|
||||
{% blocktrans %}First, select which type of job that
|
||||
you want to run. This choice will determine which
|
||||
other steps are required
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
<div>
|
||||
<a class="btn btn-default btn-small btn-create btn-inline ajax-modal"
|
||||
href="{% url 'horizon:project:data_processing.wizard:job_type_select' %}">
|
||||
<span class="fa fa-plus"></span> {% trans "Select type" %}
|
||||
</a>
|
||||
</div>
|
||||
<div>{% trans "Current type:" %}
|
||||
{% if request.session.guide_job_type %}
|
||||
<span class="text-success">
|
||||
{{ request.session.guide_job_type}}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="text-danger">
|
||||
{% trans "No type chosen" %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<hr/>
|
||||
{% if request.session.guide_job_type %}
|
||||
{% if view.show_data_sources %}
|
||||
<li>
|
||||
<div>
|
||||
<div>{% blocktrans %}Data Sources are what your
|
||||
job uses for input and output. Depending on the type
|
||||
of job you will be running, you may need to define one
|
||||
or more data sources. You can create multiple data
|
||||
sources by repeating this step.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
<div>
|
||||
<a class="btn btn-default btn-small btn-create btn-inline ajax-modal"
|
||||
href="{% url 'horizon:project:data_processing.data_sources:create-data-source' %}">
|
||||
<span class="fa fa-plus"></span> {% trans "Create a data source" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<hr/>
|
||||
{% endif %}
|
||||
<li>
|
||||
<div>
|
||||
<div>{% blocktrans %}Define your Job Template.
|
||||
This is where you choose the type of job that you
|
||||
want to run (Pig, Java Action, Spark, etc) and choose
|
||||
or upload the files necessary to run it. The inputs
|
||||
and outputs will be defined later.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
<div>
|
||||
<a class="btn btn-default btn-small btn-create btn-inline ajax-modal{% if not request.session.guide_job_type %} disabled{% endif %}"
|
||||
href="{% url 'horizon:project:data_processing.jobs:create-job' request.session.guide_job_type %}">
|
||||
<span class="fa fa-plus"></span> {% trans "Create a job template" %}</a>
|
||||
</div>
|
||||
<div>{% trans "Job template:" %}
|
||||
{% if request.session.guide_job_name %}
|
||||
<span class="text-success">
|
||||
{{ request.session.guide_job_name }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="text-danger">
|
||||
{% trans "No job template created" %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<hr/>
|
||||
<li>
|
||||
<div>
|
||||
<div>{% blocktrans %}Launch your job. When
|
||||
launching, you may need to choose your input and
|
||||
output data sources. This is where you would also
|
||||
add any special configuration values, parameters,
|
||||
or arguments that you need to pass along
|
||||
to your job.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
<div>
|
||||
<a class="btn btn-default btn-small btn-create btn-inline ajax-modal {% if not request.session.guide_job_id %} disabled{% endif %}"
|
||||
href="{% url 'horizon:project:data_processing.jobs:launch-job' %}?job_id={{ request.session.guide_job_id }}">{% trans "Launch job" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ol>
|
||||
<div class="guide">
|
||||
<a id="reset_cluster_guide_btn"
|
||||
class=" btn btn-default btn-small btn-create btn-inline btn-danger"
|
||||
title="{% trans "Reset Job Execution Guide" %}"
|
||||
href="{% url 'horizon:project:data_processing.wizard:reset_jobex_guide' True %}">
|
||||
{% trans "Reset Job Execution Guide" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
@ -33,6 +33,29 @@
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<li>
|
||||
<div>
|
||||
<div>{% blocktrans %}
|
||||
In order to run a Data Processing job, you need to make
|
||||
the files for your program available to the
|
||||
Data Processing system, define where the input and output
|
||||
need to go and create a Job Template that describes
|
||||
how to run your job. Each of those steps can be done
|
||||
manually or you can follow this guide to help take you
|
||||
through the steps to run a job on an existing cluster.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
<div>
|
||||
<a id="jobex_guide_btn"
|
||||
class=" btn btn-default btn-small btn-create btn-inline"
|
||||
title="{% trans "Job Execution Guide" %}"
|
||||
href="/project/data_processing/wizard/jobex_guide">
|
||||
{% trans "Job Execution Guide" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
@ -22,6 +22,11 @@ CLUSTER_GUIDE_URL = reverse(
|
||||
CLUSTER_GUIDE_RESET_URL = reverse(
|
||||
'horizon:project:data_processing.wizard:reset_cluster_guide',
|
||||
kwargs={"reset_cluster_guide": "true"})
|
||||
JOB_GUIDE_URL = reverse(
|
||||
'horizon:project:data_processing.wizard:jobex_guide')
|
||||
JOB_GUIDE_RESET_URL = reverse(
|
||||
'horizon:project:data_processing.wizard:reset_jobex_guide',
|
||||
kwargs={"reset_jobex_guide": "true"})
|
||||
|
||||
|
||||
class DataProcessingClusterGuideTests(test.TestCase):
|
||||
@ -42,3 +47,13 @@ class DataProcessingClusterGuideTests(test.TestCase):
|
||||
def test_cluster_guide_reset(self):
|
||||
res = self.client.get(CLUSTER_GUIDE_RESET_URL)
|
||||
self.assertRedirectsNoFollow(res, CLUSTER_GUIDE_URL)
|
||||
|
||||
def test_jobex_guide(self):
|
||||
res = self.client.get(JOB_GUIDE_URL)
|
||||
self.assertTemplateUsed(
|
||||
res, 'project/data_processing.wizard/jobex_guide.html')
|
||||
self.assertContains(res, 'Guided Job Execution')
|
||||
|
||||
def test_jobex_guide_reset(self):
|
||||
res = self.client.get(JOB_GUIDE_RESET_URL)
|
||||
self.assertRedirectsNoFollow(res, JOB_GUIDE_URL)
|
||||
|
@ -26,7 +26,16 @@ urlpatterns = patterns('',
|
||||
url(r'^cluster_guide/(?P<reset_cluster_guide>[^/]+)/$',
|
||||
views.ResetClusterGuideView.as_view(),
|
||||
name='reset_cluster_guide'),
|
||||
url(r'^jobex_guide$',
|
||||
views.JobExecutionGuideView.as_view(),
|
||||
name='jobex_guide'),
|
||||
url(r'^jobex_guide/(?P<reset_jobex_guide>[^/]+)/$',
|
||||
views.ResetJobExGuideView.as_view(),
|
||||
name='reset_jobex_guide'),
|
||||
url(r'^plugin_select$',
|
||||
views.PluginSelectView.as_view(),
|
||||
name='plugin_select'),
|
||||
url(r'^job_type_select$',
|
||||
views.JobTypeSelectView.as_view(),
|
||||
name='job_type_select'),
|
||||
)
|
||||
|
@ -56,9 +56,39 @@ class ResetClusterGuideView(generic.RedirectView):
|
||||
return http.HttpResponseRedirect(reverse_lazy(self.pattern_name))
|
||||
|
||||
|
||||
class JobExecutionGuideView(horizon_views.APIView):
|
||||
template_name = 'project/data_processing.wizard/jobex_guide.html'
|
||||
|
||||
def show_data_sources(self):
|
||||
try:
|
||||
if self.request.session["guide_job_type"] in ["Spark", "Java"]:
|
||||
return False
|
||||
return True
|
||||
except Exception:
|
||||
return True
|
||||
|
||||
|
||||
class ResetJobExGuideView(generic.RedirectView):
|
||||
pattern_name = 'horizon:project:data_processing.wizard:jobex_guide'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if kwargs["reset_jobex_guide"]:
|
||||
hlps = helpers.Helpers(request)
|
||||
hlps.reset_job_guide()
|
||||
return http.HttpResponseRedirect(reverse_lazy(self.pattern_name))
|
||||
|
||||
|
||||
class PluginSelectView(forms.ModalFormView):
|
||||
form_class = wizforms.ChoosePluginForm
|
||||
success_url = reverse_lazy(
|
||||
'horizon:project:data_processing.wizard:cluster_guide')
|
||||
classes = ("ajax-modal")
|
||||
template_name = "project/data_processing.wizard/plugin_select.html"
|
||||
|
||||
|
||||
class JobTypeSelectView(forms.ModalFormView):
|
||||
form_class = wizforms.ChooseJobTypeForm
|
||||
success_url = reverse_lazy(
|
||||
'horizon:project:data_processing.wizard:jobex_guide')
|
||||
classes = ("ajax-modal")
|
||||
template_name = "project/data_processing.wizard/job_type_select.html"
|
||||
|
Loading…
Reference in New Issue
Block a user