Skip to content
This repository has been archived by the owner on Dec 5, 2019. It is now read-only.

Bug 1309227 - Improve form error handling and other UX things #37

Merged
merged 23 commits into from
Oct 17, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
4c2bb4f
Bug 1309227 - Port cluster forms to improved error handling
jezdez Oct 12, 2016
c9ee255
Fix bug 1309536 - Sort clusters by date
jezdez Oct 12, 2016
0e9be85
Fix cluster tests.
jezdez Oct 12, 2016
2d85b3a
Improved jobs app
jezdez Oct 13, 2016
d4283a8
Flatten the atmo package a bit
jezdez Oct 13, 2016
36d80e7
Add MPL statement to new files
jezdez Oct 13, 2016
c91aed0
Remove old form instance from dashboard view
jezdez Oct 13, 2016
5718681
Refactor schedule registry slightly
jezdez Oct 13, 2016
25dabc6
Add datetimepicker to jobs new and edit form
jezdez Oct 13, 2016
ab79c1f
Use 24h based datepicket and prevent dates in the past
jezdez Oct 13, 2016
1846547
Remove lower date limitaton
jezdez Oct 14, 2016
b017c24
Spark job frontend fixes
jezdez Oct 14, 2016
ec8b591
Refactor spark job forms to be DRY
jezdez Oct 14, 2016
7151e23
Fix bug 1309545 - Horizontal dashboard sections
jezdez Oct 14, 2016
2fa5c65
Fix bug 1309549 - Link to docs
jezdez Oct 14, 2016
9a077ca
Fix bug 1309554 - Grey out terminate and edit button for clusters
jezdez Oct 14, 2016
6e2e53d
Fix bug 1309572 - add note about times being in UTC to footer
jezdez Oct 14, 2016
1d1d5b1
Fix bug 1309543 - Add filters for clusters in dashboard
jezdez Oct 14, 2016
5f1f962
Different grid
jezdez Oct 14, 2016
a7de05d
Fix bug 1309855 - Add accounting tags to AWS calls
jezdez Oct 14, 2016
c60207d
Add JS to disable for submission on submit
jezdez Oct 17, 2016
cf91a0f
Apply review fixes
jezdez Oct 17, 2016
af0cbab
Add EditorConfig
jezdez Oct 17, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Top-most EditorConfig file ?
root = true

# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

# Indentiation
[*.py]
indent_style = space
indent_size = 4

[*.{css,js,json,html}]
indent_style = space
indent_size = 2
3 changes: 3 additions & 0 deletions atmo/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, you can obtain one at http://mozilla.org/MPL/2.0/.
default_app_config = 'atmo.apps.AtmoAppConfig'
42 changes: 39 additions & 3 deletions atmo/apps.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,47 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, you can obtain one at http://mozilla.org/MPL/2.0/.
from django.apps import AppConfig
from django.conf import settings
from django.utils.module_loading import import_string

import django_rq
import session_csrf

DEFAULT_JOB_TIMEOUT = 15

job_schedule = {
'delete_clusters': {
'cron_string': '* * * * *',
'func': 'atmo.clusters.jobs.delete_clusters',
'timeout': 5
},
'update_clusters_info': {
'cron_string': '* * * * *',
'func': 'atmo.clusters.jobs.update_clusters_info',
'timeout': 5
},
'launch_jobs': {
'cron_string': '* * * * *',
'func': 'atmo.jobs.jobs.launch_jobs',
'timeout': 5
},
}


def register_job_schedule():
scheduler = django_rq.get_scheduler()
for job_id, params in job_schedule.items():
scheduler.cron(
params['cron_string'],
id=job_id,
func=import_string(params['func']),
timeout=params.get('timeout', DEFAULT_JOB_TIMEOUT)
)
for job in scheduler.get_jobs():
if job.id not in job_schedule:
scheduler.cancel(job)


class AtmoAppConfig(AppConfig):
name = 'atmo'
Expand All @@ -21,8 +60,5 @@ def ready(self):
# Under some circumstances (e.g. when calling collectstatic)
# REDIS_URL is not available and we can skip the job schedule registration.
if settings.REDIS_URL:
# This module contains references to some orm models, so it's
# safer to import it here.
from .schedule import register_job_schedule
# Register rq scheduled jobs
register_job_schedule()
3 changes: 3 additions & 0 deletions atmo/clusters/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, you can obtain one at http://mozilla.org/MPL/2.0/.
140 changes: 59 additions & 81 deletions atmo/clusters/forms.py
Original file line number Diff line number Diff line change
@@ -1,136 +1,114 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, you can obtain one at http://mozilla.org/MPL/2.0/.
from django import forms

from . import models
from ..utils.fields import PublicKeyFileField
from ..forms import CreatedByFormMixin, PublicKeyFileField


class NewClusterForm(forms.ModelForm):
class NewClusterForm(CreatedByFormMixin, forms.ModelForm):

identifier = forms.RegexField(
label="Cluster identifier",
required=True,
regex="^[\w-]{1,100}$",
widget=forms.TextInput(attrs={
'class': 'form-control',
'pattern': r'[\w-]+',
'data-toggle': 'popover',
'data-trigger': 'focus',
'data-placement': 'top',
'data-container': 'body',
'data-content': 'A brief description of the cluster\'s purpose, '
'visible in the AWS management console.',
'data-validation-pattern-message': 'Valid cluster names are strings of alphanumeric '
'characters, \'_\', and \'-\'.',
})
}),
help_text='A brief description of the cluster\'s purpose, '
'visible in the AWS management console.',
)
size = forms.IntegerField(
label="Cluster size",
required=True,
min_value=1, max_value=20,
widget=forms.NumberInput(attrs={
'class': 'form-control',
'required': 'required',
'min': '1', 'max': '20',
'data-toggle': 'popover',
'data-trigger': 'focus',
'data-placement': 'top',
'data-container': 'body',
'data-content': 'Number of workers to use in the cluster '
'(1 is recommended for testing or development).',
})
'min': '1',
'max': '20',
}),
help_text='Number of workers to use in the cluster '
'(1 is recommended for testing or development).'
)
public_key = PublicKeyFileField(
label="Public SSH key",
required=True,
widget=forms.FileInput(attrs={'required': 'required'})
widget=forms.FileInput(attrs={'class': 'form-control', 'required': 'required'}),
help_text="""\
Upload your SSH <strong>public key</strong>, not private key!
This will generally be found in places like <code>~/.ssh/id_rsa.pub</code>.
"""
)
emr_release = forms.ChoiceField(
choices=models.Cluster.EMR_RELEASES_CHOICES,
widget=forms.Select(
attrs={'class': 'form-control', 'required': 'required'}
),
label='EMR release version',
initial=models.Cluster.EMR_RELEASES_CHOICES_DEFAULT,
)

def __init__(self, user, *args, **kwargs):
self.created_by = user
super(NewClusterForm, self).__init__(*args, **kwargs)

def save(self):
def save(self, commit=False):
# create the model without committing, since we haven't
# set the required created_by field yet
new_cluster = super(NewClusterForm, self).save(commit=False)
cluster = super(NewClusterForm, self).save(commit=commit)

# set the field to the user that created the cluster
new_cluster.created_by = self.created_by
cluster.created_by = self.created_by

# actually start the real cluster, and return the model object
new_cluster.save()
return new_cluster
cluster.save()
return cluster

class Meta:
model = models.Cluster
fields = ['identifier', 'size', 'public_key', 'emr_release']


class EditClusterForm(forms.ModelForm):
cluster = forms.ModelChoiceField(
queryset=models.Cluster.objects.all(),
required=True,
widget=forms.HiddenInput(attrs={
# fields with the `selected-cluster` class get their value automatically
# set to the cluster ID of the selected cluster
'class': 'selected-cluster',
})
)

class EditClusterForm(CreatedByFormMixin, forms.ModelForm):
identifier = forms.RegexField(
required=True,
regex="^[\w-]{1,100}$",
regex=r'^[\w-]{1,100}$',
widget=forms.TextInput(attrs={
'class': 'form-control',
'pattern': r'[\w-]+',
'data-toggle': 'popover',
'data-trigger': 'focus',
'data-placement': 'top',
'data-container': 'body',
'data-content': 'A brief description of the cluster\'s purpose, '
'visible in the AWS management console.',
'data-validation-pattern-message': 'Valid cluster names are strings of alphanumeric '
'characters, \'_\', and \'-\'.',
})
}),
help_text='A brief description of the cluster\'s purpose, '
'visible in the AWS management console.',
)

def __init__(self, user, *args, **kwargs):
self.created_by = user
super(EditClusterForm, self).__init__(*args, **kwargs)

def save(self):
cleaned_data = super(EditClusterForm, self).clean()
cluster = cleaned_data["cluster"]
if self.created_by != cluster.created_by: # only allow editing clusters that one created
raise ValueError("Disallowed attempt to edit another user's cluster")
cluster.identifier = cleaned_data["identifier"]
def save(self, commit=True):
cluster = super(EditClusterForm, self).save(commit=False)
cluster.update_identifier()
cluster.save()
if commit:
cluster.save()
self.save_m2m()
return cluster

class Meta:
model = models.Cluster
fields = ['identifier']


class DeleteClusterForm(forms.ModelForm):
cluster = forms.ModelChoiceField(
queryset=models.Cluster.objects.all(),
class TerminateClusterForm(CreatedByFormMixin, forms.ModelForm):
confirmation = forms.RegexField(
required=True,
widget=forms.HiddenInput(attrs={
# fields with the `selected-cluster` class get their value automatically
# set to the cluster ID of the selected cluster
'class': 'selected-cluster',
})
label='Confirm termination with cluster identifier',
regex=r'^[\w-]{1,100}$',
widget=forms.TextInput(attrs={
'class': 'form-control',
}),
)

def __init__(self, user, *args, **kwargs):
self.created_by = user
super(DeleteClusterForm, self).__init__(*args, **kwargs)

def save(self):
cleaned_data = super(DeleteClusterForm, self).clean()
cluster = cleaned_data["cluster"]
if self.created_by != cluster.created_by: # only allow deleting clusters that one created
raise ValueError("Disallowed attempt to delete another user's cluster")
cluster.deactivate()
return cluster
def clean_confirmation(self):
confirmation = self.cleaned_data.get('confirmation')
if confirmation != self.instance.identifier:
raise forms.ValidationError(
"Entered cluster identifier doesn't match"
)
return confirmation

class Meta:
model = models.Cluster
Expand Down
4 changes: 2 additions & 2 deletions atmo/clusters/jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
from django.utils import timezone
import newrelic.agent

from atmo.clusters.models import Cluster
from atmo.utils import email
from .models import Cluster
from .. import email


@newrelic.agent.background_task(group='RQ')
Expand Down
4 changes: 3 additions & 1 deletion atmo/clusters/management/commands/delete_clusters.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, you can obtain one at http://mozilla.org/MPL/2.0/.
from django.core.management.base import BaseCommand
from atmo.clusters.jobs import delete_clusters
from ...jobs import delete_clusters


class Command(BaseCommand):
help = 'Go through expired clusters to deactivate or warn about ones that are expiring'

def handle(self, *args, **options):
self.stdout.write('Deleting expired clusters...', ending='')
delete_clusters()
self.stdout.write('done.')
4 changes: 3 additions & 1 deletion atmo/clusters/management/commands/update_clusters_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, you can obtain one at http://mozilla.org/MPL/2.0/.
from django.core.management.base import BaseCommand
from atmo.clusters.jobs import update_clusters_info
from ...jobs import update_clusters_info


class Command(BaseCommand):
help = 'Go through active clusters and update their status'

def handle(self, *args, **options):
self.stdout.write('Updating cluster info...', ending='')
update_clusters_info()
self.stdout.write('done.')
3 changes: 3 additions & 0 deletions atmo/clusters/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, you can obtain one at http://mozilla.org/MPL/2.0/.
# -*- coding: utf-8 -*-
# Generated by Django 1.9.1 on 2016-07-27 15:50
from __future__ import unicode_literals
Expand Down
3 changes: 3 additions & 0 deletions atmo/clusters/migrations/0002_cluster_emr_release.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, you can obtain one at http://mozilla.org/MPL/2.0/.
# -*- coding: utf-8 -*-
# Generated by Django 1.9.1 on 2016-09-15 12:40
from __future__ import unicode_literals
Expand Down
3 changes: 3 additions & 0 deletions atmo/clusters/migrations/0003_cluster_master_address.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, you can obtain one at http://mozilla.org/MPL/2.0/.
# -*- coding: utf-8 -*-
# Generated by Django 1.9.1 on 2016-09-27 22:29
from __future__ import unicode_literals
Expand Down
3 changes: 3 additions & 0 deletions atmo/clusters/migrations/0004_auto_20161002_1841.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, you can obtain one at http://mozilla.org/MPL/2.0/.
# -*- coding: utf-8 -*-
# Generated by Django 1.9.1 on 2016-10-02 18:41
from __future__ import unicode_literals
Expand Down
3 changes: 3 additions & 0 deletions atmo/clusters/migrations/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, you can obtain one at http://mozilla.org/MPL/2.0/.
Loading