view to submitting jobs
authorMaciej Tronowski <mtro@man.poznan.pl>
Fri, 27 Feb 2015 16:07:35 +0000 (17:07 +0100)
committerMaciej Tronowski <mtro@man.poznan.pl>
Fri, 27 Feb 2015 16:07:35 +0000 (17:07 +0100)
plgng/settings.py
qcg/fields.py [new file with mode: 0644]
qcg/forms.py
qcg/static/qcg/main.css
qcg/templates/qcg/base.html
qcg/templates/qcg/job_new.html [new file with mode: 0644]
qcg/urls.py
qcg/views.py

index 24f298b..6679cb1 100644 (file)
@@ -132,7 +132,7 @@ SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
 # 3-rd party settings
 
 BOOTSTRAP3 = {
-    'horizontal_label_class': 'col-md-4',
-    'horizontal_field_class': 'col-md-6',
+    'horizontal_label_class': 'col-sm-3 col-md-4',
+    'horizontal_field_class': 'col-sm-9 col-md-6',
     'set_placeholder': False,
 }
diff --git a/qcg/fields.py b/qcg/fields.py
new file mode 100644 (file)
index 0000000..3702aad
--- /dev/null
@@ -0,0 +1,11 @@
+from django.forms import ChoiceField, MultipleChoiceField
+
+
+class PredefinedChoiceField(ChoiceField):
+    def valid_value(self, value):
+        # any value is valid
+        return True
+
+
+class MultiplePredefinedChoiceField(MultipleChoiceField, PredefinedChoiceField):
+    pass
index 8906e2d..1771327 100644 (file)
@@ -4,10 +4,15 @@ from django.core.validators import RegexValidator
 from django.template.defaultfilters import capfirst
 from pyqcg.utils import TaskStatus
 
+from qcg.fields import PredefinedChoiceField, MultiplePredefinedChoiceField
 from qcg.models import Task, Allocation
 
 
 date_range_validator = RegexValidator(r'[0-9]{2}\.[0-9]{2}\.[0-9]{4} - [0-9]{2}\.[0-9]{2}\.[0-9]{4}')
+nodes_validator = RegexValidator(r'^[0-9]{1,3}(:[0-9]{1,2}){0,2}$')
+env_name_validator = RegexValidator(r'^[a-zA-Z_][a-zA-Z0-9_]*$')
+
+CHOICES_PLACEHOLDER = (None, '')
 
 
 class FiltersForm(forms.Form):
@@ -54,3 +59,113 @@ class FiltersForm(forms.Form):
         self.fields['host'].choices = tuple(
             (host, capfirst(host.split('.', 1)[0]))
             for host in Allocation.objects.values_list('host_name', flat=True).order_by('host_name').distinct())
+
+
+class JobDescriptionForm(forms.Form):
+    class Host(object):
+        GALERA = 'galera.task.gda.pl'
+        HYDRA = 'hydra.icm.edu.pl'
+        INULA = 'inula.man.poznan.pl'
+        MOSS = 'moss.man.poznan.pl'
+        NOVA = 'nova.wcss.wroc.pl'
+        REEF = 'reef.man.poznan.pl'
+        ZEUS = 'zeus.cyfronet.pl'
+
+        CHOICES = (
+            CHOICES_PLACEHOLDER,
+            (GALERA, u'Galera'),
+            (HYDRA, u'Hydra'),
+            (INULA, u'Inula'),
+            (MOSS, u'Moss'),
+            (NOVA, u'Supernova'),
+            (REEF, u'Reef'),
+            (ZEUS, u'Zeus'),
+        )
+    APPLICATION_CHOICES = (
+        CHOICES_PLACEHOLDER,
+        ('bash', 'BASH'),
+        ('python', 'Python'),
+        ('matlab', 'MATLAB'),
+    )
+    QUEUE_CHOICES = (
+        CHOICES_PLACEHOLDER,
+        ('plgid', 'plgrid'),
+        ('plgid-long', 'plgrid-long'),
+        ('plgid-testing', 'plgrid-testing'),
+    )
+    MODULES_CHOICES = (
+        ('plgrid/apps/python', 'plgrid/apps/python'),
+        ('plgrid/apps/matlab', 'plgrid/apps/matlab'),
+    )
+    NOTIFY_CHOICES = (
+        (0, u'Brak'),
+        (1, u'E-mail'),
+        (2, u'XMPP'),
+    )
+    PROCESS_CHOICES = (
+        (0, u'Brak'),
+        (1, u'Polecenie'),
+        (2, u'Skrypt'),
+    )
+
+    application = forms.ChoiceField(choices=APPLICATION_CHOICES, label=u"Aplikacja", required=False)  # TODO choices
+    executable = forms.CharField(label=u"Polecenie", max_length=500, required=False)  # TODO grid ftp
+    arguments = forms.CharField(label=u"Argumenty", max_length=1000, required=False)
+
+    name = forms.CharField(label=u"Nazwa", max_length=100, required=False)
+    note = forms.CharField(label=u"Notatka", widget=forms.Textarea(attrs={'rows': 2, 'cols': 40}), required=False)
+    grant = forms.CharField(label=u"Grant", max_length=100, required=False)
+
+    host = forms.ChoiceField(label=u"Host", choices=Host.CHOICES, required=False)
+    queue = PredefinedChoiceField(choices=QUEUE_CHOICES, label=u"Kolejka", required=False)
+    procs = forms.IntegerField(label=u"Liczba procesów", min_value=0, required=False)
+    nodes = forms.CharField(label=u"Topologia węzłów", max_length=10, validators=[nodes_validator], required=False)
+    wall_time = forms.IntegerField(label=u"Wall time (s)", min_value=0, required=False)  # TODO duration field
+    memory = forms.IntegerField(label=u"Pamięć (MB)", min_value=0, required=False)
+    memory_per_slot = forms.IntegerField(label=u"Pamięci per proces (MB)", min_value=0, required=False)
+
+    # TODO grid ftp
+    input = forms.CharField(label=u"Standardowe wejście", max_length=500, required=False)
+    output = forms.CharField(label=u"Standardowe wyjście", max_length=500, required=False)
+    error = forms.CharField(label=u"Standardowe wyjście błędów", max_length=500, required=False)
+    stage_in = forms.CharField(label=u"Stage in", max_length=500, required=False)
+    stage_out = forms.CharField(label=u"Stage out", max_length=500, required=False)
+
+    properties = MultiplePredefinedChoiceField(label=u"Właściwości węzłów", required=False)
+    modules = forms.MultipleChoiceField(label=u"Moduły", choices=MODULES_CHOICES, required=False)  # TODO choices
+
+    not_after = forms.DateTimeField(label=u"Nie później niż", required=False)
+    not_before = forms.DateTimeField(label=u"Nie wcześniej niż", required=False)
+    deadline = forms.IntegerField(label=u"Deadline (s)", min_value=0, required=False)  # TODO duration field
+    reservation = forms.CharField(label=u"Rezerwacja", max_length=100, required=False)
+
+    monitoring = forms.BooleanField(label=u"Portal QCG-Monitoring", required=False)
+    notify_type = forms.ChoiceField(label=u"Monitorowanie stanu", choices=NOTIFY_CHOICES, required=False, initial=0,
+                                    widget=forms.RadioSelect)
+    notify_address = forms.EmailField(label=u"Adres", required=False)
+    watch_output_type = forms.ChoiceField(label=u"Monitorowanie wyjścia", choices=NOTIFY_CHOICES, required=False,
+                                          initial=0, widget=forms.RadioSelect)
+    watch_output_address = forms.EmailField(label=u"Adres", required=False)
+    watch_output_pattern = forms.CharField(label=u"Wzorzec", max_length=500, required=False)
+
+    preprocess_type = forms.ChoiceField(label=u"Preprocessing", choices=PROCESS_CHOICES, required=False, initial=0,
+                                        widget=forms.RadioSelect)
+    preprocess_cmd = forms.CharField(label=u"Polecenie", max_length=1000, required=False)
+    preprocess_script = forms.CharField(label=u"Skrypt", max_length=500, required=False)  # TODO grid ftp
+    postprocess_type = forms.ChoiceField(label=u"Postprocessing", choices=PROCESS_CHOICES, required=False, initial=0,
+                                         widget=forms.RadioSelect)
+    postprocess_cmd = forms.CharField(label=u"Polecenie", max_length=1000, required=False)
+    postprocess_script = forms.CharField(label=u"Skrypt", max_length=500, required=False)  # TODO grid ftp
+    native = MultiplePredefinedChoiceField(label=u"Parametry natywne", required=False)
+    persistent = forms.BooleanField(label=u"Trwałe", required=False)
+    use_scratch = forms.BooleanField(label=u"Scratch", required=False)
+
+
+class EnvForm(forms.Form):
+    name = forms.CharField(label=u"Nazwa", max_length=100, validators=[env_name_validator],
+                           widget=forms.TextInput(attrs={'placeholder': u'Nazwa'}))
+    value = forms.CharField(label=u"Wartość", max_length=500,
+                            widget=forms.TextInput(attrs={'placeholder': u'Wartość'}))
+
+
+EnvFormSet = forms.formset_factory(EnvForm, can_delete=True, extra=0)
index 57a2b84..4edc075 100644 (file)
@@ -16,3 +16,39 @@ footer.navbar-fixed-bottom {
 .page-header {
     margin-top: 0;
 }
+
+textarea {
+    resize: vertical;
+}
+
+
+.modal-body > *:last-child {
+    margin-bottom: 0;
+}
+
+
+#env-controls .form-group {
+    display: inline-block;
+    margin: 0;
+    vertical-align: middle;
+}
+
+
+#env-controls > div {
+    margin-bottom: 7px;
+}
+
+
+#env-controls > div:last-of-type {
+    margin: 0;
+}
+
+
+#env-controls input[type="text"] {
+    width: 160px;
+}
+
+
+#add-env-form {
+    padding-top: 7px;
+}
index fcf6291..19841b4 100644 (file)
@@ -31,6 +31,9 @@
                         <li{% ifequal request.resolver_match.url_name 'jobs' %} class="active"{% endifequal %}>
                             <a href="{% url 'jobs' %}">Zadania</a>
                         </li>
+                        <li{% ifequal request.resolver_match.url_name 'job_new' %} class="active"{% endifequal %}>
+                            <a href="{% url 'job_new' %}">Zleć zadanie</a>
+                        </li>
                     </ul>
                 {% endif %}
 
diff --git a/qcg/templates/qcg/job_new.html b/qcg/templates/qcg/job_new.html
new file mode 100644 (file)
index 0000000..d1e5de6
--- /dev/null
@@ -0,0 +1,291 @@
+{% extends 'qcg/base.html' %}
+{% load staticfiles bootstrap3 qcg_utils %}
+
+{% block extra_css %}
+    <link href="{% static 'qcg/selectize/selectize.bootstrap3.css' %}" rel="stylesheet">
+    <link href="{% static 'qcg/datetimepicker/bootstrap-datetimepicker.min.css' %}" rel="stylesheet">
+{% endblock %}
+
+{% block extra_js %}
+    <script src="{% static 'qcg/selectize/selectize.min.js' %}"></script>
+    <script src="{% static 'qcg/formset/jquery.formset.js' %}"></script>
+    <script src="{% static 'qcg/moment/moment.min.js' %}"></script>
+    <script src="{% static 'qcg/moment/pl.js' %}"></script>
+    <script src="{% static 'qcg/datetimepicker/bootstrap-datetimepicker.min.js' %}"></script>
+
+    <script>
+        $(function() {
+            $('#id_application, #id_host').selectize();
+            $('#id_arguments,#id_properties,#id_native').selectize({
+                plugins: ['remove_button'],
+                create: true,
+                render: {
+                    option_create: function(data, escape) {
+                        return '<div class="create">Dodaj <strong>' + escape(data.input) + '</strong>&hellip;</div>';
+                    }
+                }
+            });
+            $('#id_queue').selectize({
+                create: true,
+                render: {
+                    option_create: function(data, escape) {
+                        return '<div class="create">Wybierz <strong>' + escape(data.input) + '</strong>&hellip;</div>';
+                    }
+                }
+            });
+            $('#id_modules').selectize({
+                plugins: ['remove_button']
+            });
+
+            // hide delete checkbox in empty env form
+            $('#env-form-empty').find('div.form-group:last-of-type').hide();
+            $('#env-controls').find('> div').formset({
+                formTemplate: '#env-form-empty',
+                parent: '#env-controls',
+                addLinkParent: '#add-env-form',
+                addText: 'dodaj »',
+                addCssClass: 'add-btn',
+                deleteText: 'Usuń',
+                deleteCssClass: 'delete-btn btn btn-xs btn-danger'
+            });
+
+            $('#id_not_before,#id_not_after').datetimepicker({
+                locale: 'pl',
+                minDate: moment()
+            });
+
+            $('input[name="notify_type"],input[name="watch_output_type"],input[name="preprocess_type"],input[name="postprocess_type"]').on('change', function () {
+                $(this).tab('show');
+            }).each(function() {
+                $(this).parent().toggleClass('active', this.checked);
+
+                if (this.checked)
+                    $(this).tab('show');
+            });
+
+            $('#toggle-advanced').click(function(e) {
+                e.preventDefault();
+                $('form .collapse').collapse('toggle');
+                $(this).find('span').text(this.text == "Pokaż zaawansowane" ? "Ukryj zaawansowane" : "Pokaż zaawansowane");
+            });
+
+            $('#new-env').click(function(e) {
+                e.preventDefault();
+                $('#env-modal').modal('show');
+            });
+
+            $('#env-form').submit(function(e) {
+                e.preventDefault();
+                $('#env-modal').modal('hide');
+
+                $('#id_environment')[0].selectize.createItem($('#id_env_name').val() + '=' + $('#id_env_value').val());
+            });
+        });
+    </script>
+{% endblock %}
+
+{% block container %}
+    <h1 class="page-header">{% block title %}Zleć zadanie{% endblock %}</h1>
+
+    <form action="." method="post" class="form-horizontal">
+        {% csrf_token %}
+
+        <!-- Nav tabs -->
+        <ul class="nav nav-tabs" style="margin-bottom: 20px">
+            <li role="presentation" class="active"><a href="#basic" data-toggle="tab">Podstawowe</a></li>
+            <li role="presentation"><a href="#resources" data-toggle="tab">Zasoby</a></li>
+            <li role="presentation"><a href="#files" data-toggle="tab">Pliki</a></li>
+            <li role="presentation"><a href="#environment" data-toggle="tab">Środowisko</a></li>
+            <li role="presentation"><a href="#reservation" data-toggle="tab">Rezerwacja</a></li>
+            <li role="presentation"><a href="#monitoring" data-toggle="tab">Powiadomienia</a></li>
+            <li role="presentation"><a href="#other" data-toggle="tab">Inne</a></li>
+        </ul>
+
+        <div class="tab-content">
+            <fieldset id="basic" class="tab-pane active" role="tabpanel">
+                {% bootstrap_field form.application layout="horizontal" %}
+                <div class="row" style="margin-top: -15px; margin-bottom: 15px">
+                    <div class="col-sm-offset-3 col-md-offset-4 col-sm-9 col-md-6">
+                        {# TODO grid ftp #}
+                        <a id="add-main-file" href="#">wybierz plik główny &raquo;</a>
+                    </div>
+                </div>
+
+                {% bootstrap_field form.executable layout="horizontal" form_group_class="form-group collapse"  %}
+                {% bootstrap_field form.arguments layout="horizontal" %}
+                {% bootstrap_field form.name layout="horizontal" %}
+                {% bootstrap_field form.note layout="horizontal" %}
+                {% bootstrap_field form.grant layout="horizontal" %}
+            </fieldset>
+
+            <fieldset id="resources" class="tab-pane" role="tabpanel">
+                {% bootstrap_field form.host layout="horizontal" %}
+                {% bootstrap_field form.queue layout="horizontal" %}
+                {% bootstrap_field form.procs layout="horizontal" %}
+                {% bootstrap_field form.nodes layout="horizontal" form_group_class="form-group collapse" %}
+                {% bootstrap_field form.wall_time layout="horizontal" %}
+                {% bootstrap_field form.memory layout="horizontal" %}
+                {% bootstrap_field form.memory_per_slot layout="horizontal" %}
+            </fieldset>
+
+            <fieldset id="files" class="tab-pane" role="tabpanel">
+                {% bootstrap_field form.input layout="horizontal" %}
+                {% bootstrap_field form.output layout="horizontal" %}
+                {% bootstrap_field form.error layout="horizontal" %}
+                {% bootstrap_field form.stage_in layout="horizontal" %}
+                {% bootstrap_field form.stage_out layout="horizontal" %}
+            </fieldset>
+
+            <fieldset id="environment" class="tab-pane" role="tabpanel">
+                {% bootstrap_field form.properties layout="horizontal" %}
+                {{ env_formset.management_form }}
+
+                <div class="form-group">
+                    <label class="col-sm-3 col-md-4 control-label">Zmienne środowiskowe</label>
+                    <div class="col-sm-9 col-md-6">
+                        <div id="env-controls">
+                            {% for env_form in env_formset %}
+                                <div>{% bootstrap_form env_form layout='inline' %}</div>
+                            {% endfor %}
+                        </div>
+                        <div id="add-env-form"></div>
+                        <div id="env-form-empty" style="display: none">
+                            {% bootstrap_form env_formset.empty_form layout='inline' %}
+                        </div>
+                    </div>
+                </div>
+
+                {% bootstrap_field form.modules layout="horizontal" %}
+            </fieldset>
+
+            <fieldset id="reservation" class="tab-pane" role="tabpanel">
+                {% bootstrap_field form.not_before layout="horizontal" %}
+                {% bootstrap_field form.not_after layout="horizontal" %}
+                {% bootstrap_field form.deadline layout="horizontal" %}
+                {% bootstrap_field form.reservation layout="horizontal" form_group_class="form-group collapse" %}
+            </fieldset>
+
+            <fieldset id="monitoring" class="tab-pane" role="tabpanel">
+                {% bootstrap_checkbox form.monitoring %}
+
+                <div class="form-group">
+                    <label class="col-sm-3 col-md-4 control-label">Monitorowanie stanu</label>
+                    <div class="col-sm-9 col-md-6">
+                        <div class="btn-group" data-toggle="buttons">
+                            {% for option in form.notify_type %}
+                                <label class="btn btn-default">
+                                    <input type="radio" autocomplete="off" name="{{ option.name }}" value="{{ option.choice_value }}"
+                                            {% if option.is_checked %}checked{% endif %}
+                                            data-target=".notify-type-{{ option.choice_value }}"> {{ option.choice_label }}
+                                </label>
+                            {% endfor %}
+                        </div>
+                    </div>
+                </div>
+
+                <div class="tab-content">
+                    <div class="tab-pane notify-type-0"></div>
+
+                    <div class="tab-pane notify-type-1 notify-type-2" style="margin-top: 15px">
+                        {% bootstrap_field form.notify_address layout="horizontal" %}
+                    </div>
+                </div>
+
+                <div class="form-group">
+                    <label class="col-sm-3 col-md-4 control-label">Monitorowanie wyjścia</label>
+                    <div class="col-sm-9 col-md-6">
+                        <div class="btn-group" data-toggle="buttons">
+                            {% for option in form.watch_output_type %}
+                                <label class="btn btn-default">
+                                    <input type="radio" autocomplete="off" name="{{ option.name }}" value="{{ option.choice_value }}"
+                                            {% if option.is_checked %}checked{% endif %}
+                                            data-target=".watch-output-type-{{ option.choice_value }}"> {{ option.choice_label }}
+                                </label>
+                            {% endfor %}
+                        </div>
+                    </div>
+                </div>
+
+                <div class="tab-content">
+                    <div class="tab-pane watch-output-type-0"></div>
+
+                    <div class="tab-pane watch-output-type-1 watch-output-type-2" style="margin-top: 15px">
+                        {% bootstrap_field form.watch_output_address layout="horizontal" %}
+                        {% bootstrap_field form.watch_output_pattern layout="horizontal" %}
+                    </div>
+                </div>
+            </fieldset>
+
+            <fieldset id="other" class="tab-pane" role="tabpanel">
+                <div class="form-group">
+                    <label class="col-sm-3 col-md-4 control-label">Preprocessing</label>
+                    <div class="col-sm-9 col-md-6">
+                        <div class="btn-group" data-toggle="buttons">
+                            {% for option in form.preprocess_type %}
+                                <label class="btn btn-default">
+                                    <input type="radio" autocomplete="off" name="{{ option.name }}" value="{{ option.choice_value }}"
+                                            {% if option.is_checked %}checked{% endif %}
+                                            data-target=".preprocess-type-{{ option.choice_value }}"> {{ option.choice_label }}
+                                </label>
+                            {% endfor %}
+                        </div>
+                    </div>
+                </div>
+
+                <div class="tab-content">
+                    <div class="tab-pane preprocess-type-0"></div>
+
+                    <div class="tab-pane preprocess-type-1" style="margin-top: 15px">
+                        {% bootstrap_field form.preprocess_cmd layout="horizontal" %}
+                    </div>
+
+                    <div class="tab-pane preprocess-type-2" style="margin-top: 15px">
+                        {% bootstrap_field form.preprocess_script layout="horizontal" %}
+                    </div>
+                </div>
+
+                <div class="form-group">
+                    <label class="col-sm-3 col-md-4 control-label">Postprocessing</label>
+                    <div class="col-sm-9 col-md-6">
+                        <div class="btn-group" data-toggle="buttons">
+                            {% for option in form.postprocess_type %}
+                                <label class="btn btn-default">
+                                    <input type="radio" autocomplete="off" name="{{ option.name }}" value="{{ option.choice_value }}"
+                                            {% if option.is_checked %}checked{% endif %}
+                                            data-target=".postprocess-type-{{ option.choice_value }}"> {{ option.choice_label }}
+                                </label>
+                            {% endfor %}
+                        </div>
+                    </div>
+                </div>
+
+                <div class="tab-content">
+                    <div class="tab-pane postprocess-type-0"></div>
+
+                    <div class="tab-pane postprocess-type-1" style="margin-top: 15px">
+                        {% bootstrap_field form.postprocess_cmd layout="horizontal" %}
+                    </div>
+
+                    <div class="tab-pane postprocess-type-2" style="margin-top: 15px">
+                        {% bootstrap_field form.postprocess_script layout="horizontal" %}
+                    </div>
+                </div>
+
+                {% bootstrap_field form.native layout="horizontal" %}
+                {% bootstrap_checkbox form.persistent %}
+                {% bootstrap_checkbox form.use_scratch %}
+            </fieldset>
+        </div>
+
+        <hr>
+
+        <div class="row">
+            <div class="col-sm-offset-3 col-md-offset-4 col-sm-9 col-md-6">
+                <button type="submit" class="btn btn-primary">Zleć zadanie</button>
+                <button type="reset" class="btn btn-warning">Resetuj</button>
+                <a id="toggle-advanced" href="#"><span class="text-muted">Pokaż zaawansowane</span></a>
+            </div>
+        </div>
+    </form>
+
+{% endblock %}
index f265b0f..085bfd3 100644 (file)
@@ -9,6 +9,7 @@ urlpatterns = patterns('',
 
     url(r'^$', views.index, name='index'),
     url(r'^jobs/$', views.jobs_list, name='jobs'),
+    url(r'^job/new/$', views.job_new, name='job_new'),
     url(r'^job/(?P<job_id>[\w]+)/?$', views.job_details, name='job'),
     url(r'^job/(?P<job_id>[\w]+)/(?P<task_id>[\w]+)/?$', views.task_details, name='task'),
 )
index e996da0..f0de4dc 100644 (file)
@@ -13,7 +13,7 @@ from django_openid_auth.views import make_consumer
 from openid.extensions import ax
 from pyqcg import QCG
 
-from qcg.forms import FiltersForm
+from qcg.forms import FiltersForm, JobDescriptionForm, EnvFormSet
 from qcg.utils import update_user_data, paginator_context
 
 
@@ -133,3 +133,7 @@ def task_details(request, job_id, task_id):
                              job__job_id=job_id, task_id=task_id)
 
     return render(request, 'qcg/task.html', {'task': task})
+
+
+def job_new(request):
+    return render(request, 'qcg/job_new.html', {'form': JobDescriptionForm(), 'env_formset': EnvFormSet()})