diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..45531ed4 --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/atmo/__init__.py b/atmo/__init__.py index 5aa0232a..db957215 100644 --- a/atmo/__init__.py +++ b/atmo/__init__.py @@ -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' diff --git a/atmo/apps.py b/atmo/apps.py index 4a9ceb03..eb2138f4 100644 --- a/atmo/apps.py +++ b/atmo/apps.py @@ -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' @@ -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() diff --git a/atmo/clusters/__init__.py b/atmo/clusters/__init__.py index e69de29b..564e6203 100644 --- a/atmo/clusters/__init__.py +++ b/atmo/clusters/__init__.py @@ -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/. diff --git a/atmo/clusters/forms.py b/atmo/clusters/forms.py index fd744845..d6e617b1 100644 --- a/atmo/clusters/forms.py +++ b/atmo/clusters/forms.py @@ -1,107 +1,90 @@ +# 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 public key, not private key! +This will generally be found in places like ~/.ssh/id_rsa.pub. +""" + ) + 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: @@ -109,28 +92,23 @@ class Meta: 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 diff --git a/atmo/clusters/jobs.py b/atmo/clusters/jobs.py index afb442a6..1c0fca84 100644 --- a/atmo/clusters/jobs.py +++ b/atmo/clusters/jobs.py @@ -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') diff --git a/atmo/clusters/management/commands/delete_clusters.py b/atmo/clusters/management/commands/delete_clusters.py index 89b63b41..34229ca9 100644 --- a/atmo/clusters/management/commands/delete_clusters.py +++ b/atmo/clusters/management/commands/delete_clusters.py @@ -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.') diff --git a/atmo/clusters/management/commands/update_clusters_info.py b/atmo/clusters/management/commands/update_clusters_info.py index 7a4e16fa..c3ac106b 100644 --- a/atmo/clusters/management/commands/update_clusters_info.py +++ b/atmo/clusters/management/commands/update_clusters_info.py @@ -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.') diff --git a/atmo/clusters/migrations/0001_initial.py b/atmo/clusters/migrations/0001_initial.py index 23393c3b..e29a9ba8 100644 --- a/atmo/clusters/migrations/0001_initial.py +++ b/atmo/clusters/migrations/0001_initial.py @@ -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 diff --git a/atmo/clusters/migrations/0002_cluster_emr_release.py b/atmo/clusters/migrations/0002_cluster_emr_release.py index 56eb6135..95b21d7b 100644 --- a/atmo/clusters/migrations/0002_cluster_emr_release.py +++ b/atmo/clusters/migrations/0002_cluster_emr_release.py @@ -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 diff --git a/atmo/clusters/migrations/0003_cluster_master_address.py b/atmo/clusters/migrations/0003_cluster_master_address.py index 816e94a0..6625734f 100644 --- a/atmo/clusters/migrations/0003_cluster_master_address.py +++ b/atmo/clusters/migrations/0003_cluster_master_address.py @@ -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 diff --git a/atmo/clusters/migrations/0004_auto_20161002_1841.py b/atmo/clusters/migrations/0004_auto_20161002_1841.py index 070135a5..7f94084c 100644 --- a/atmo/clusters/migrations/0004_auto_20161002_1841.py +++ b/atmo/clusters/migrations/0004_auto_20161002_1841.py @@ -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 diff --git a/atmo/clusters/migrations/__init__.py b/atmo/clusters/migrations/__init__.py index e69de29b..564e6203 100644 --- a/atmo/clusters/migrations/__init__.py +++ b/atmo/clusters/migrations/__init__.py @@ -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/. diff --git a/atmo/clusters/models.py b/atmo/clusters/models.py index 8f86e1f9..850bd31c 100644 --- a/atmo/clusters/models.py +++ b/atmo/clusters/models.py @@ -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/. from datetime import timedelta from django.core.urlresolvers import reverse @@ -5,15 +8,18 @@ from django.contrib.auth.models import User from django.utils import timezone -from ..utils import provisioning - - -# Default release is the last item. -EMR_RELEASES = ('5.0.0', '4.5.0') +from .. import provisioning class Cluster(models.Model): FINAL_STATUS_LIST = ('COMPLETED', 'TERMINATED', 'FAILED') + # Default release is the first item, order should be from latest to oldest + EMR_RELEASES = ( + '5.0.0', + '4.5.0', + ) + EMR_RELEASES_CHOICES = list(zip(*(EMR_RELEASES,) * 2)) + EMR_RELEASES_CHOICES_DEFAULT = EMR_RELEASES[0] identifier = models.CharField( max_length=100, @@ -45,7 +51,7 @@ class Cluster(models.Model): ) emr_release = models.CharField( - max_length=50, choices=list(zip(*(EMR_RELEASES,) * 2)), default=EMR_RELEASES[-1], + max_length=50, choices=EMR_RELEASES_CHOICES, default=EMR_RELEASES_CHOICES_DEFAULT, help_text=('Different EMR versions have different versions ' 'of software like Hadoop, Spark, etc') ) @@ -69,10 +75,6 @@ def __repr__(self): def get_info(self): return provisioning.cluster_info(self.jobflow_id) - def is_expiring_soon(self): - """Returns true if the cluster is expiring in the next hour.""" - return self.end_date <= timezone.now() + timedelta(hours=1) - def update_status(self): """Should be called to update latest cluster status in `self.most_recent_status`.""" info = self.get_info() @@ -120,9 +122,18 @@ def deactivate(self): def is_active(self): return self.most_recent_status not in self.FINAL_STATUS_LIST + @property + def is_terminating(self): + return self.most_recent_status == 'TERMINATING' + @property def is_ready(self): return self.most_recent_status == 'WAITING' + @property + def is_expiring_soon(self): + """Returns true if the cluster is expiring in the next hour.""" + return self.end_date <= timezone.now() + timedelta(hours=1) + def get_absolute_url(self): return reverse('clusters-detail', kwargs={'id': self.id}) diff --git a/atmo/clusters/tests.py b/atmo/clusters/tests.py index 2b1bc246..ca46eda2 100644 --- a/atmo/clusters/tests.py +++ b/atmo/clusters/tests.py @@ -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/. import io import mock from datetime import timedelta @@ -10,8 +13,8 @@ class TestCreateCluster(TestCase): - @mock.patch('atmo.utils.provisioning.cluster_start', return_value=u'12345') - @mock.patch('atmo.utils.provisioning.cluster_info', return_value={ + @mock.patch('atmo.provisioning.cluster_start', return_value=u'12345') + @mock.patch('atmo.provisioning.cluster_info', return_value={ 'start_time': timezone.now(), 'state': 'BOOTSTRAPPING', 'public_dns': 'master.public.dns.name', @@ -22,12 +25,13 @@ def setUp(self, cluster_info, cluster_start): self.client.force_login(self.test_user) # request that a new cluster be created - self.response = self.client.post(reverse('clusters-new'), { - 'identifier': 'test-cluster', - 'size': 5, - 'public_key': io.BytesIO('ssh-rsa AAAAB3'), - 'emr_release': models.EMR_RELEASES[-1] - }, follow=True) + self.response = self.client.post( + reverse('clusters-new'), { + 'new-identifier': 'test-cluster', + 'new-size': 5, + 'new-public_key': io.BytesIO('ssh-rsa AAAAB3'), + 'new-emr_release': models.Cluster.EMR_RELEASES_CHOICES_DEFAULT + }, follow=True) self.cluster_start = cluster_start self.cluster = models.Cluster.objects.get(jobflow_id=u'12345') @@ -43,7 +47,7 @@ def test_that_cluster_is_correctly_provisioned(self): self.assertEqual(identifier, 'test-cluster') self.assertEqual(size, 5) self.assertEqual(public_key, 'ssh-rsa AAAAB3') - self.assertEqual(emr_release, models.EMR_RELEASES[-1]) + self.assertEqual(emr_release, models.Cluster.EMR_RELEASES_CHOICES_DEFAULT) def test_that_the_model_was_created_correctly(self): cluster = models.Cluster.objects.get(jobflow_id=u'12345') @@ -55,23 +59,24 @@ def test_that_the_model_was_created_correctly(self): self.start_date <= cluster.start_date <= self.start_date + timedelta(seconds=10) ) self.assertEqual(cluster.created_by, self.test_user) - self.assertEqual(cluster.emr_release, models.EMR_RELEASES[-1]) + self.assertEqual(cluster.emr_release, models.Cluster.EMR_RELEASES_CHOICES_DEFAULT) self.assertTrue(User.objects.filter(username='john.smith').exists()) - @mock.patch('atmo.utils.provisioning.cluster_start', return_value=u'67890') + @mock.patch('atmo.provisioning.cluster_start', return_value=u'67890') @mock.patch( - 'atmo.utils.provisioning.cluster_info', return_value={ + 'atmo.provisioning.cluster_info', return_value={ 'start_time': timezone.now(), 'state': 'BOOTSTRAPPING', 'public_dns': None, }) def test_empty_public_dns(self, cluster_info, cluster_start): - self.client.post(reverse('clusters-new'), { - 'identifier': 'test-cluster', - 'size': 5, - 'public_key': io.BytesIO('ssh-rsa AAAAB3'), - 'emr_release': models.EMR_RELEASES[-1] - }, follow=True) + self.client.post( + reverse('clusters-new'), { + 'new-identifier': 'test-cluster', + 'new-size': 5, + 'new-public_key': io.BytesIO('ssh-rsa AAAAB3'), + 'new-emr_release': models.Cluster.EMR_RELEASES_CHOICES_DEFAULT + }, follow=True) self.assertEqual(cluster_start.call_count, 1) cluster = models.Cluster.objects.get(jobflow_id=u'67890') self.assertEqual(cluster_info.call_count, 1) @@ -79,13 +84,13 @@ def test_empty_public_dns(self, cluster_info, cluster_start): class TestEditCluster(TestCase): - @mock.patch('atmo.utils.provisioning.cluster_start', return_value=u'12345') - @mock.patch('atmo.utils.provisioning.cluster_info', return_value={ + @mock.patch('atmo.provisioning.cluster_start', return_value=u'12345') + @mock.patch('atmo.provisioning.cluster_info', return_value={ 'start_time': timezone.now(), 'state': 'BOOTSTRAPPING', 'public_dns': 'master.public.dns.name', }) - @mock.patch('atmo.utils.provisioning.cluster_rename', return_value=None) + @mock.patch('atmo.provisioning.cluster_rename', return_value=None) def setUp(self, cluster_rename, cluster_info, cluster_start): self.start_date = timezone.now() @@ -101,10 +106,13 @@ def setUp(self, cluster_rename, cluster_info, cluster_start): # request that the test cluster be edited self.client.force_login(self.test_user) - self.response = self.client.post(reverse('clusters-edit'), { - 'cluster': cluster.id, - 'identifier': 'new-cluster-name', - }, follow=True) + self.response = self.client.post( + reverse('clusters-edit', kwargs={ + 'id': cluster.id, + }), { + 'edit-cluster': cluster.id, + 'edit-identifier': 'new-cluster-name', + }, follow=True) self.cluster_rename = cluster_rename self.cluster = cluster @@ -131,10 +139,10 @@ def test_that_the_model_was_edited_correctly(self): self.assertEqual(cluster.created_by, self.test_user) -class TestDeleteCluster(TestCase): - @mock.patch('atmo.utils.provisioning.cluster_stop', return_value=None) - @mock.patch('atmo.utils.provisioning.cluster_start', return_value=u'12345') - @mock.patch('atmo.utils.provisioning.cluster_info', return_value={ +class TestTerminateCluster(TestCase): + @mock.patch('atmo.provisioning.cluster_stop', return_value=None) + @mock.patch('atmo.provisioning.cluster_start', return_value=u'12345') + @mock.patch('atmo.provisioning.cluster_info', return_value={ 'start_time': timezone.now(), 'state': 'BOOTSTRAPPING', 'public_dns': 'master.public.dns.name', @@ -153,9 +161,13 @@ def setUp(self, cluster_info, cluster_start, cluster_stop): cluster.save() # request that the test cluster be deleted - self.response = self.client.post(reverse('clusters-delete'), { - 'cluster': cluster.id, - }, follow=True) + self.response = self.client.post( + reverse('clusters-terminate', kwargs={ + 'id': cluster.id, + }), { + 'terminate-cluster': cluster.id, + 'terminate-confirmation': cluster.identifier, + }, follow=True) self.cluster = cluster self.cluster_stop = cluster_stop self.cluster_info = cluster_info diff --git a/atmo/clusters/urls.py b/atmo/clusters/urls.py index 3e4fa149..6fb80450 100644 --- a/atmo/clusters/urls.py +++ b/atmo/clusters/urls.py @@ -1,9 +1,12 @@ +# 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.conf.urls import url from . import views urlpatterns = [ url(r'^new/$', views.new_cluster, name='clusters-new'), - url(r'^edit/$', views.edit_cluster, name='clusters-edit'), - url(r'^delete/$', views.delete_cluster, name='clusters-delete'), - url(r'^(?P[0-9]+)/$', views.detail_cluster, name='clusters-detail'), + url(r'^(?P\d+)/edit/$', views.edit_cluster, name='clusters-edit'), + url(r'^(?P\d+)/terminate/$', views.terminate_cluster, name='clusters-terminate'), + url(r'^(?P\d+)/$', views.detail_cluster, name='clusters-detail'), ] diff --git a/atmo/clusters/views.py b/atmo/clusters/views.py index 9f48ff95..840fb5a8 100644 --- a/atmo/clusters/views.py +++ b/atmo/clusters/views.py @@ -1,48 +1,112 @@ +# 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 django.shortcuts import redirect, get_object_or_404, render -from django.views.decorators.http import require_POST from django.contrib.auth.decorators import login_required -from django.http import HttpResponseBadRequest -from session_csrf import anonymous_csrf - -from . import forms +from .forms import NewClusterForm, EditClusterForm, TerminateClusterForm from .models import Cluster @login_required -@anonymous_csrf -@require_POST def new_cluster(request): - form = forms.NewClusterForm(request.user, request.POST, request.FILES) - if not form.is_valid(): - return HttpResponseBadRequest(form.errors.as_json(escape_html=True)) - cluster = form.save() # this will also magically spawn the cluster for us - return redirect(cluster) + username = request.user.email.split("@")[0] + initial = { + "identifier": "{}-telemetry-analysis".format(username), + "size": 1, + } + if request.method == 'POST': + form = NewClusterForm( + request.user, + data=request.POST, + files=request.FILES, + initial=initial, + prefix='new', + ) + if form.is_valid(): + cluster = form.save() # this will also magically spawn the cluster for us + return redirect(cluster) + else: + form = NewClusterForm( + request.user, + initial=initial, + prefix='new', + ) + context = { + 'form': form, + } + return render(request, 'atmo/cluster-new.html', context) @login_required -@anonymous_csrf -@require_POST -def edit_cluster(request): - form = forms.EditClusterForm(request.user, request.POST) - if not form.is_valid(): - return HttpResponseBadRequest(form.errors.as_json(escape_html=True)) - cluster = form.save() # this will also update the cluster for us - return redirect(cluster) +def edit_cluster(request, id): + cluster = get_object_or_404(Cluster, created_by=request.user, pk=id) + if not cluster.is_active: + return redirect(cluster) + if request.method == 'POST': + form = EditClusterForm( + request.user, + data=request.POST, + files=request.FILES, + instance=cluster, + prefix='edit', + ) + if form.is_valid(): + cluster = form.save() # this will also magically spawn the cluster for us + return redirect(cluster) + else: + form = EditClusterForm( + request.user, + instance=cluster, + prefix='edit', + ) + context = { + 'form': form, + } + return render(request, 'atmo/cluster-edit.html', context) @login_required -@anonymous_csrf -@require_POST -def delete_cluster(request): - form = forms.DeleteClusterForm(request.user, request.POST) - if not form.is_valid(): - return HttpResponseBadRequest(form.errors.as_json(escape_html=True)) - cluster = form.save() # this will also terminate the cluster for us - return redirect(cluster) +def terminate_cluster(request, id): + cluster = get_object_or_404(Cluster, created_by=request.user, pk=id) + if not cluster.is_active: + return redirect(cluster) + if request.method == 'POST': + form = TerminateClusterForm( + request.user, + prefix='terminate', + data=request.POST, + instance=cluster, + ) + if form.is_valid(): + cluster.deactivate() + return redirect(cluster) + else: + form = TerminateClusterForm( + request.user, + prefix='terminate', + instance=cluster, + ) + context = { + 'cluster': cluster, + 'form': form, + } + return render(request, 'atmo/cluster-terminate.html', context=context) @login_required def detail_cluster(request, id): cluster = get_object_or_404(Cluster, created_by=request.user, pk=id) - return render(request, 'atmo/detail-cluster.html', context={'cluster': cluster}) + terminate_form = TerminateClusterForm( + request.user, + prefix='terminate', + instance=cluster, + ) + # hiding the confirmation input on the detail page + terminate_form.fields['confirmation'].widget = forms.HiddenInput() + context = { + 'cluster': cluster, + 'terminate_form': terminate_form, + } + return render(request, 'atmo/cluster-detail.html', context=context) diff --git a/atmo/context_processors.py b/atmo/context_processors.py new file mode 100644 index 00000000..2de0e8bc --- /dev/null +++ b/atmo/context_processors.py @@ -0,0 +1,11 @@ +# 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.conf import settings as django_settings + + +def settings(request): + """ + Adds static-related context variables to the context. + """ + return {'settings': django_settings} diff --git a/atmo/utils/email.py b/atmo/email.py similarity index 70% rename from atmo/utils/email.py rename to atmo/email.py index 0d4cd1e7..b2723d12 100644 --- a/atmo/utils/email.py +++ b/atmo/email.py @@ -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/. from django.conf import settings import boto3 diff --git a/atmo/forms.py b/atmo/forms.py new file mode 100644 index 00000000..ffb08e98 --- /dev/null +++ b/atmo/forms.py @@ -0,0 +1,49 @@ +# 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 django.core.exceptions import ValidationError +from django.template.defaultfilters import filesizeformat + + +class PublicKeyFileField(forms.FileField): + """ + Custom Django for file field that only accepts SSH public keys. + + The cleaned data is the public key as a string. + """ + def clean(self, data, initial=None): + uploaded_file = super(PublicKeyFileField, self).clean(data, initial) + if uploaded_file.size > 100000: + raise ValidationError( + 'File size must be at most 100kB, actual size is {}'.format( + filesizeformat(uploaded_file.size) + ) + ) + contents = uploaded_file.read() + if not contents.startswith('ssh-rsa AAAAB3'): + raise ValidationError( + 'Invalid public key (a public key should start with \'ssh-rsa AAAAB3\')' + ) + return contents + + +class CreatedByFormMixin(object): + """ + Custom Django form mixin that takes a user object and if the provided + model form instance has a primary key checks if the given user + matches the "created_by" field. + """ + def __init__(self, user, *args, **kwargs): + self.created_by = user + super(CreatedByFormMixin, self).__init__(*args, **kwargs) + + def clean(self): + """ + only allow deleting clusters that one created + """ + super(CreatedByFormMixin, self).clean() + if self.instance.id and self.created_by != self.instance.created_by: + raise forms.ValidationError( + 'Access denied to the data of another user' + ) diff --git a/atmo/jobs/__init__.py b/atmo/jobs/__init__.py index e69de29b..564e6203 100644 --- a/atmo/jobs/__init__.py +++ b/atmo/jobs/__init__.py @@ -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/. diff --git a/atmo/jobs/forms.py b/atmo/jobs/forms.py index c69c3bec..e617545e 100644 --- a/atmo/jobs/forms.py +++ b/atmo/jobs/forms.py @@ -1,272 +1,146 @@ +# 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 ..forms import CreatedByFormMixin -class NewSparkJobForm(forms.ModelForm): +class BaseSparkJobForm(CreatedByFormMixin, forms.ModelForm): identifier = forms.RegexField( required=True, - regex="^[\w-]{1,100}$", + label='Job identifier', + 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 scheduled Spark job\'s purpose, ' - 'visible in the AWS management console.', - 'data-validation-pattern-message': 'Valid job names are strings of alphanumeric ' - 'characters, \'_\', and \'-\'.', - }) - ) - notebook = forms.FileField( - required=True, - widget=forms.FileInput(attrs={'required': 'required'}) + }), + help_text='A brief description of the scheduled Spark job\'s purpose, ' + 'visible in the AWS management console.' ) result_visibility = forms.ChoiceField( - choices=[ - ('private', 'Private: results output to an S3 bucket, viewable with AWS credentials'), - ('public', 'Public: results output to a public S3 bucket, viewable by anyone'), - ], + choices=models.SparkJob.RESULT_VISIBILITY_CHOICES, widget=forms.Select( attrs={'class': 'form-control', 'required': 'required'} - ) + ), + label='Job result visibility' ) size = forms.IntegerField( required=True, - min_value=1, max_value=20, + min_value=1, + max_value=20, + label='Job cluster size', 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 when running the Spark job ' - '(1 is recommended for testing or development).', - }) + 'min': '1', + 'max': '20', + }), + help_text='Number of workers to use when running the Spark job ' + '(1 is recommended for testing or development).' ) interval_in_hours = forms.ChoiceField( - choices=[ - (24, "Daily"), - (24 * 7, "Weekly"), - (24 * 30, "Monthly"), - ], + choices=models.SparkJob.INTERVAL_CHOICES, widget=forms.Select( - attrs={'class': 'form-control', 'required': 'required'} - ) + attrs={ + 'class': 'form-control', + 'required': 'required', + } + ), + label='Job interval', + help_text='Interval at which the Spark job should be run', ) job_timeout = forms.IntegerField( required=True, - min_value=1, max_value=24, + min_value=1, + max_value=24, + label='Job timeout', widget=forms.NumberInput(attrs={ 'class': 'form-control', 'required': 'required', - 'min': '1', 'max': '24', - 'data-toggle': 'popover', - 'data-trigger': 'focus', - 'data-placement': 'top', - 'data-container': 'body', - 'data-content': 'Number of hours that a single run of the job can run ' - 'for before timing out and being terminated.', - }) + 'min': '1', + 'max': '24', + }), + help_text='Number of hours that a single run of the job can run ' + 'for before timing out and being terminated.' ) start_date = forms.DateTimeField( required=True, - widget=forms.DateTimeInput(attrs={ - 'class': 'form-control datetimepicker', - 'data-toggle': 'popover', - 'data-trigger': 'focus', - 'data-placement': 'top', - 'data-container': 'body', - 'data-content': 'Date/time on which to enable the scheduled Spark job.', - }) + widget=forms.DateTimeInput( + attrs={ + 'class': 'form-control datetimepicker', + }), + label='Job start date', + help_text='Date and time on which to enable the scheduled Spark job.', ) end_date = forms.DateTimeField( required=False, widget=forms.DateTimeInput(attrs={ 'class': 'form-control datetimepicker', - 'data-toggle': 'popover', - 'data-trigger': 'focus', - 'data-placement': 'top', - 'data-container': 'body', - 'data-content': 'Date/time on which to disable the scheduled Spark job ' - '- leave this blank if the job should not be disabled.', - }) + }), + label='Job end date (optional)', + help_text='Date and time on which to disable the scheduled Spark job ' + '- leave this blank if the job should not be disabled.', ) - def __init__(self, user, *args, **kwargs): - self.created_by = user - super(NewSparkJobForm, self).__init__(*args, **kwargs) + class Meta: + model = models.SparkJob + fields = [] + + +class NewSparkJobForm(BaseSparkJobForm): + notebook = forms.FileField( + required=True, + widget=forms.FileInput(attrs={'class': 'form-control', 'required': 'required'}), + label='Analysis Jupyter Notebook', + help_text='A Jupyter (formally IPython) Notebook has the file extension .ipynb' + ) def save(self): # create the model without committing, since we haven't # set the required created_by field yet - new_spark_job = super(NewSparkJobForm, self).save(commit=False) + spark_job = super(NewSparkJobForm, self).save(commit=False) # set the field to the user that created the scheduled Spark job - new_spark_job.created_by = self.created_by + spark_job.created_by = self.created_by # actually save the scheduled Spark job, and return the model object - new_spark_job.save(self.cleaned_data["notebook"]) + spark_job.save(self.cleaned_data['notebook']) + return spark_job - class Meta: - model = models.SparkJob + class Meta(BaseSparkJobForm.Meta): fields = [ - 'identifier', 'result_visibility', 'size', 'interval_in_hours', - 'job_timeout', 'start_date', 'end_date' + 'identifier', 'notebook', 'result_visibility', 'size', + 'interval_in_hours', 'job_timeout', 'start_date', 'end_date' ] -class EditSparkJobForm(forms.ModelForm): - job = forms.ModelChoiceField( - queryset=models.SparkJob.objects.all(), - required=True, - widget=forms.HiddenInput(attrs={ - # fields with the `selected-spark-job` class get their value - # automatically set to the job ID of the selected scheduled Spark job - 'class': 'selected-spark-job', - }) - ) +class EditSparkJobForm(BaseSparkJobForm): - identifier = forms.RegexField( - 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 scheduled Spark job\'s purpose, ' - 'visible in the AWS management console.', - 'data-validation-pattern-message': 'Valid job names are strings of alphanumeric ' - 'characters, \'_\', and \'-\'.', - }) - ) - result_visibility = forms.ChoiceField( - choices=[ - ('private', 'Private: results output to an S3 bucket, viewable with AWS credentials'), - ('public', 'Public: results output to a public S3 bucket, viewable by anyone'), - ], - widget=forms.Select( - attrs={'class': 'form-control', 'required': 'required'} - ) - ) - size = forms.IntegerField( - 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 when running the Spark job ' - '(1 is recommended for testing or development).', - }) - ) - interval_in_hours = forms.ChoiceField( - choices=[ - (24, "Daily"), - (24 * 7, "Weekly"), - (24 * 30, "Monthly"), - ], - widget=forms.Select( - attrs={'class': 'form-control', 'required': 'required'} - ) - ) - job_timeout = forms.IntegerField( - required=True, - min_value=1, max_value=24, - widget=forms.NumberInput(attrs={ - 'class': 'form-control', - 'required': 'required', - 'min': '1', 'max': '24', - 'data-toggle': 'popover', - 'data-trigger': 'focus', - 'data-placement': 'top', - 'data-container': 'body', - 'data-content': 'Number of hours that a single run of the job can run ' - 'for before timing out and being terminated.', - }) - ) - start_date = forms.DateTimeField( - required=True, - widget=forms.DateTimeInput(attrs={ - 'class': 'form-control datetimepicker', - 'data-toggle': 'popover', - 'data-trigger': 'focus', - 'data-placement': 'top', - 'data-container': 'body', - 'data-content': 'Date/time on which to enable the scheduled Spark job.', - }) - ) - end_date = forms.DateTimeField( - required=False, - widget=forms.DateTimeInput(attrs={ - 'class': 'form-control datetimepicker', - 'data-toggle': 'popover', - 'data-trigger': 'focus', - 'data-placement': 'top', - 'data-container': 'body', - 'data-content': 'Date/time on which to disable the scheduled Spark job ' - '- leave this blank if the job should not be disabled.', - }) - ) - - def __init__(self, user, *args, **kwargs): - self.created_by = user - super(EditSparkJobForm, self).__init__(*args, **kwargs) - - def save(self): - cleaned_data = super(EditSparkJobForm, self).clean() - job = cleaned_data["job"] - if self.created_by != job.created_by: # only allow editing jobs that one creates - raise ValueError("Disallowed attempt to edit another user's scheduled job") - job.identifier = cleaned_data["identifier"] - job.result_visibility = cleaned_data["result_visibility"] - job.size = cleaned_data["size"] - job.interval_in_hours = cleaned_data["interval_in_hours"] - job.job_timeout = cleaned_data["job_timeout"] - job.start_date = cleaned_data["start_date"] - job.end_date = cleaned_data["end_date"] - job.save() - - class Meta: - model = models.SparkJob + class Meta(BaseSparkJobForm.Meta): fields = [ 'identifier', 'result_visibility', 'size', 'interval_in_hours', 'job_timeout', 'start_date', 'end_date' ] -class DeleteSparkJobForm(forms.ModelForm): - job = forms.ModelChoiceField( - queryset=models.SparkJob.objects.all(), +class DeleteSparkJobForm(CreatedByFormMixin, forms.ModelForm): + confirmation = forms.RegexField( required=True, - widget=forms.HiddenInput(attrs={ - # fields with the `selected-spark-job` class get their value - # automatically set to the job ID of the selected scheduled Spark job - 'class': 'selected-spark-job', - }) + label='Confirm termination with Spark job identifier', + regex=r'^[\w-]{1,100}$', + widget=forms.TextInput(attrs={ + 'class': 'form-control', + }), ) - def __init__(self, user, *args, **kwargs): - self.created_by = user - super(DeleteSparkJobForm, self).__init__(*args, **kwargs) - - def save(self): - cleaned_data = super(DeleteSparkJobForm, self).clean() - job = cleaned_data["job"] - if self.created_by != job.created_by: # only allow deleting jobs that one creates - raise ValueError("Disallowed attempt to delete another user's scheduled job") - job.delete() + def clean_confirmation(self): + confirmation = self.cleaned_data.get('confirmation') + if confirmation != self.instance.identifier: + raise forms.ValidationError( + "Entered Spark job identifier doesn't match" + ) + return confirmation class Meta: model = models.SparkJob diff --git a/atmo/jobs/jobs.py b/atmo/jobs/jobs.py index 03dc3ef2..d2a79d78 100644 --- a/atmo/jobs/jobs.py +++ b/atmo/jobs/jobs.py @@ -2,7 +2,7 @@ # 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/. import newrelic.agent -from atmo.jobs.models import SparkJob +from .models import SparkJob @newrelic.agent.background_task(group='RQ') diff --git a/atmo/jobs/management/commands/launch_jobs.py b/atmo/jobs/management/commands/launch_jobs.py index c091d6aa..f46afc86 100644 --- a/atmo/jobs/management/commands/launch_jobs.py +++ b/atmo/jobs/management/commands/launch_jobs.py @@ -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.jobs.jobs import launch_jobs +from ...jobs import launch_jobs class Command(BaseCommand): help = 'Launch scheduled jobs if necessary' def handle(self, *args, **options): + self.stdout.write('Launching scheduled jobs...', ending='') launch_jobs() + self.stdout.write('done.') diff --git a/atmo/jobs/migrations/0001_initial.py b/atmo/jobs/migrations/0001_initial.py index 1f387901..8572619d 100644 --- a/atmo/jobs/migrations/0001_initial.py +++ b/atmo/jobs/migrations/0001_initial.py @@ -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 diff --git a/atmo/jobs/migrations/__init__.py b/atmo/jobs/migrations/__init__.py index e69de29b..564e6203 100644 --- a/atmo/jobs/migrations/__init__.py +++ b/atmo/jobs/migrations/__init__.py @@ -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/. diff --git a/atmo/jobs/models.py b/atmo/jobs/models.py index f7286b04..01e52f98 100644 --- a/atmo/jobs/models.py +++ b/atmo/jobs/models.py @@ -1,14 +1,31 @@ +# 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 datetime import timedelta from django.core.urlresolvers import reverse from django.db import models from django.contrib.auth.models import User from django.utils import timezone +from django.utils.functional import cached_property -from ..utils import provisioning, scheduling +from .. import provisioning, scheduling class SparkJob(models.Model): + WEEKLY = 24 * 7 + INTERVAL_CHOICES = [ + (24, "Daily"), + (WEEKLY, "Weekly"), + (24 * 30, "Monthly"), + ] + INTERVAL_CHOICES_DEFAULT = INTERVAL_CHOICES[0][0] + RESULT_VISIBILITY_CHOICES = [ + ('private', 'Private: results output to an S3 bucket, viewable with AWS credentials'), + ('public', 'Public: results output to a public S3 bucket, viewable by anyone'), + ] + RESULT_VISIBILITY_CHOICES_DEFAULT = RESULT_VISIBILITY_CHOICES[0][0] + identifier = models.CharField( max_length=100, help_text="Job name, used to non-uniqely identify individual jobs." @@ -19,16 +36,20 @@ class SparkJob(models.Model): ) result_visibility = models.CharField( # can currently be "public" or "private" max_length=50, - help_text="Whether notebook results are uploaded to a public or private bucket" + help_text="Whether notebook results are uploaded to a public or private bucket", + choices=RESULT_VISIBILITY_CHOICES, + default=RESULT_VISIBILITY_CHOICES_DEFAULT, ) size = models.IntegerField( help_text="Number of computers to use to run the job." ) interval_in_hours = models.IntegerField( - help_text="Interval at which the job should run, in hours." + help_text="Interval at which the job should run, in hours.", + choices=INTERVAL_CHOICES, + default=INTERVAL_CHOICES_DEFAULT, ) job_timeout = models.IntegerField( - help_text="Number of hours before the job times out." + help_text="Number of hours before the job times out.", ) start_date = models.DateTimeField( help_text="Date/time that the job should start being scheduled to run." @@ -107,18 +128,31 @@ def run(self): self.created_by.email, self.identifier, self.notebook_s3_key, - self.result_visibility == "public", + self.is_public, self.size, self.job_timeout ) self.update_status() + @property + def is_public(self): + return self.result_visibility == 'public' + + @property + def notebook_name(self): + return self.notebook_s3_key.rsplit('/', 1)[-1] + + @cached_property + def notebook_content(self): + if self.notebook_s3_key: + return scheduling.spark_job_get(self.notebook_s3_key) + def terminate(self): """Stop the currently running scheduled Spark job.""" if self.current_run_jobflow_id: provisioning.cluster_stop(self.current_run_jobflow_id) - def save(self, notebook_uploadedfile = None, *args, **kwargs): + def save(self, notebook_uploadedfile=None, *args, **kwargs): if notebook_uploadedfile is not None: # notebook specified, replace current notebook self.notebook_s3_key = scheduling.spark_job_add( self.identifier, @@ -129,7 +163,6 @@ def save(self, notebook_uploadedfile = None, *args, **kwargs): def delete(self, *args, **kwargs): self.terminate() # make sure to shut down the cluster if it's currently running scheduling.spark_job_remove(self.notebook_s3_key) - super(SparkJob, self).delete(*args, **kwargs) @classmethod diff --git a/atmo/jobs/tests.py b/atmo/jobs/tests.py index 896b857d..49360838 100644 --- a/atmo/jobs/tests.py +++ b/atmo/jobs/tests.py @@ -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/. import io import mock from datetime import datetime, timedelta @@ -10,13 +13,14 @@ class TestCreateSparkJob(TestCase): - @mock.patch('atmo.utils.scheduling.spark_job_run', return_value=u'12345') - def setUp(self, spark_job_run): + @mock.patch('atmo.scheduling.spark_job_run', return_value=u'12345') + @mock.patch('atmo.scheduling.spark_job_get', return_value=u'content') + def setUp(self, spark_job_get, spark_job_run): self.test_user = User.objects.create_user('john.smith', 'john@smith.com', 'hunter2') self.client.force_login(self.test_user) # request that a new scheduled Spark job be created - with mock.patch('atmo.utils.scheduling.spark_job_add') as mocked: + with mock.patch('atmo.scheduling.spark_job_add') as mocked: def spark_job_add(identifier, notebook_uploadedfile): self.saved_notebook_contents = notebook_uploadedfile.read() return u's3://test/test-notebook.ipynb' @@ -24,18 +28,20 @@ def spark_job_add(identifier, notebook_uploadedfile): self.spark_job_add = mocked self.response = self.client.post(reverse('jobs-new'), { - 'identifier': 'test-spark-job', - 'notebook': io.BytesIO('{}'), - 'result_visibility': 'private', - 'size': 5, - 'interval_in_hours': 24, - 'job_timeout': 12, - 'start_date': '2016-04-05 13:25:47', + 'new-identifier': 'test-spark-job', + 'new-notebook': io.BytesIO('{}'), + 'new-result_visibility': 'private', + 'new-size': 5, + 'new-interval_in_hours': 24, + 'new-job_timeout': 12, + 'new-start_date': '2016-04-05 13:25:47', }, follow=True) + self.spark_job = models.SparkJob.objects.get(identifier='test-spark-job') def test_that_request_succeeded(self): self.assertEqual(self.response.status_code, 200) - self.assertEqual(self.response.redirect_chain[-1], ('/', 302)) + self.assertEqual(self.response.redirect_chain[-1], + (self.spark_job.get_absolute_url(), 302)) def test_that_the_notebook_was_uploaded_correctly(self): self.assertEqual(self.spark_job_add.call_count, 1) @@ -60,8 +66,9 @@ def test_that_the_model_was_created_correctly(self): class TestEditSparkJob(TestCase): - @mock.patch('atmo.utils.scheduling.spark_job_run', return_value=u'12345') - def setUp(self, spark_job_run): + @mock.patch('atmo.scheduling.spark_job_run', return_value=u'12345') + @mock.patch('atmo.scheduling.spark_job_get', return_value=u'content') + def setUp(self, spark_job_get, spark_job_run): self.test_user = User.objects.create_user('john.smith', 'john@smith.com', 'hunter2') self.client.force_login(self.test_user) @@ -78,19 +85,24 @@ def setUp(self, spark_job_run): spark_job.save() # request that a new scheduled Spark job be created - self.response = self.client.post(reverse('jobs-edit'), { - 'job': spark_job.id, - 'identifier': 'new-spark-job-name', - 'result_visibility': 'public', - 'size': 3, - 'interval_in_hours': 24 * 7, - 'job_timeout': 10, - 'start_date': '2016-03-08 11:17:35', - }, follow=True) + self.response = self.client.post( + reverse('jobs-edit', kwargs={ + 'id': spark_job.id, + }), { + 'edit-job': spark_job.id, + 'edit-identifier': 'new-spark-job-name', + 'edit-result_visibility': 'public', + 'edit-size': 3, + 'edit-interval_in_hours': 24 * 7, + 'edit-job_timeout': 10, + 'edit-start_date': '2016-03-08 11:17:35', + }, follow=True) + self.spark_job = spark_job def test_that_request_succeeded(self): self.assertEqual(self.response.status_code, 200) - self.assertEqual(self.response.redirect_chain[-1], ('/', 302)) + self.assertEqual(self.response.redirect_chain[-1], + (self.spark_job.get_absolute_url(), 302)) def test_that_the_model_was_edited_correctly(self): spark_job = models.SparkJob.objects.get(identifier=u'new-spark-job-name') @@ -109,8 +121,9 @@ def test_that_the_model_was_edited_correctly(self): class TestDeleteSparkJob(TestCase): - @mock.patch('atmo.utils.scheduling.spark_job_remove', return_value=None) - def setUp(self, spark_job_remove): + @mock.patch('atmo.scheduling.spark_job_remove', return_value=None) + @mock.patch('atmo.scheduling.spark_job_get', return_value=u'content') + def setUp(self, spark_job_get, spark_job_remove): self.test_user = User.objects.create_user('john.smith', 'john@smith.com', 'hunter2') self.client.force_login(self.test_user) @@ -127,9 +140,13 @@ def setUp(self, spark_job_remove): spark_job.save() # request that the test job be deleted - self.response = self.client.post(reverse('jobs-delete'), { - 'job': spark_job.id, - }, follow=True) + self.response = self.client.post( + reverse('jobs-delete', kwargs={ + 'id': spark_job.id, + }), { + 'delete-job': spark_job.id, + 'delete-confirmation': spark_job.identifier, + }, follow=True) self.spark_job_remove = spark_job_remove diff --git a/atmo/jobs/urls.py b/atmo/jobs/urls.py index c5c3e89b..72f70383 100644 --- a/atmo/jobs/urls.py +++ b/atmo/jobs/urls.py @@ -1,9 +1,12 @@ +# 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.conf.urls import url from . import views urlpatterns = [ url(r'^new/', views.new_spark_job, name='jobs-new'), - url(r'^edit/', views.edit_spark_job, name='jobs-edit'), - url(r'^delete/', views.delete_spark_job, name='jobs-delete'), - url(r'^(?P[0-9]+)/$', views.detail_spark_job, name='jobs-detail'), + url(r'^(?P\d+)/edit/', views.edit_spark_job, name='jobs-edit'), + url(r'^(?P\d+)/delete/', views.delete_spark_job, name='jobs-delete'), + url(r'^(?P\d+)/$', views.detail_spark_job, name='jobs-detail'), ] diff --git a/atmo/jobs/views.py b/atmo/jobs/views.py index 4a7bc94a..bf00a83a 100644 --- a/atmo/jobs/views.py +++ b/atmo/jobs/views.py @@ -1,52 +1,118 @@ +# 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/. import logging -from django.views.decorators.http import require_POST +from django import forms from django.contrib.auth.decorators import login_required -from django.http import HttpResponseBadRequest from django.shortcuts import redirect, get_object_or_404, render +from django.utils import timezone -from session_csrf import anonymous_csrf - +from .forms import NewSparkJobForm, EditSparkJobForm, DeleteSparkJobForm from .models import SparkJob -from . import forms logger = logging.getLogger("django") @login_required -@anonymous_csrf -@require_POST def new_spark_job(request): - form = forms.NewSparkJobForm(request.user, request.POST, request.FILES) - if not form.is_valid(): - return HttpResponseBadRequest(form.errors.as_json(escape_html=True)) - form.save() # this will also magically create the job for us - return redirect("/") + username = request.user.email.split("@")[0] + initial = { + "identifier": "{}-telemetry-scheduled-task".format(username), + "size": 1, + "interval_in_hours": SparkJob.WEEKLY, + "job_timeout": 24, + "start_date": timezone.now(), + } + if request.method == 'POST': + form = NewSparkJobForm( + request.user, + data=request.POST, + files=request.FILES, + initial=initial, + prefix='new', + ) + if form.is_valid(): + # this will also magically create the spark job for us + spark_job = form.save() + return redirect(spark_job) + else: + form = NewSparkJobForm( + request.user, + initial=initial, + prefix='new', + ) + context = { + 'form': form, + } + return render(request, 'atmo/spark-job-new.html', context) @login_required -@anonymous_csrf -@require_POST -def edit_spark_job(request): - form = forms.EditSparkJobForm(request.user, request.POST, request.FILES) - if not form.is_valid(): - return HttpResponseBadRequest(form.errors.as_json(escape_html=True)) - form.save() # this will also update the job for us - return redirect("/") +def edit_spark_job(request, id): + spark_job = get_object_or_404(SparkJob, created_by=request.user, pk=id) + if request.method == 'POST': + form = EditSparkJobForm( + request.user, + data=request.POST, + files=request.FILES, + instance=spark_job, + prefix='edit', + ) + if form.is_valid(): + # this will also update the job for us + spark_job = form.save() + return redirect(spark_job) + else: + form = EditSparkJobForm( + request.user, + instance=spark_job, + prefix='edit', + ) + context = { + 'form': form, + } + return render(request, 'atmo/spark-job-edit.html', context) @login_required -@anonymous_csrf -@require_POST -def delete_spark_job(request): - form = forms.DeleteSparkJobForm(request.user, request.POST) - if not form.is_valid(): - return HttpResponseBadRequest(form.errors.as_json(escape_html=True)) - form.save() # this will also delete the job for us - return redirect("/") +def delete_spark_job(request, id): + job = get_object_or_404(SparkJob, created_by=request.user, pk=id) + if request.method == 'POST': + form = DeleteSparkJobForm( + request.user, + prefix='delete', + data=request.POST, + instance=job, + ) + if form.is_valid(): + job.delete() + return redirect('dashboard') + else: + form = DeleteSparkJobForm( + request.user, + prefix='delete', + instance=job, + ) + context = { + 'job': job, + 'form': form, + } + return render(request, 'atmo/spark-job-delete.html', context=context) @login_required def detail_spark_job(request, id): job = get_object_or_404(SparkJob, created_by=request.user, pk=id) - return render(request, 'atmo/detail-spark-job.html', context={'job': job}) + delete_form = DeleteSparkJobForm( + request.user, + prefix='delete', + instance=job, + ) + # hiding the confirmation input on the detail page + delete_form.fields['confirmation'].widget = forms.HiddenInput() + context = { + 'job': job, + 'delete_form': delete_form, + } + return render(request, 'atmo/spark-job-detail.html', context=context) diff --git a/atmo/utils/middleware.py b/atmo/middleware.py similarity index 70% rename from atmo/utils/middleware.py rename to atmo/middleware.py index bf7be81a..2271a360 100644 --- a/atmo/utils/middleware.py +++ b/atmo/middleware.py @@ -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/. import time import os import newrelic.agent diff --git a/atmo/utils/provisioning.py b/atmo/provisioning.py similarity index 90% rename from atmo/utils/provisioning.py rename to atmo/provisioning.py index be1979ae..2976ec2b 100644 --- a/atmo/utils/provisioning.py +++ b/atmo/provisioning.py @@ -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/. from uuid import uuid4 from django.conf import settings @@ -74,6 +77,8 @@ def cluster_start(user_email, identifier, size, public_key, emr_release): {'Key': 'Owner', 'Value': user_email}, {'Key': 'Name', 'Value': identifier}, {'Key': 'Application', 'Value': settings.AWS_CONFIG['INSTANCE_APP_TAG']}, + {'Key': 'App', 'Value': settings.AWS_CONFIG['ACCOUNTING_APP_TAG']}, + {'Key': 'Type', 'Value': settings.AWS_CONFIG['ACCOUNTING_TYPE_TAG']}, ], VisibleToAllUsers=True ) diff --git a/atmo/schedule.py b/atmo/schedule.py deleted file mode 100644 index d40f47bf..00000000 --- a/atmo/schedule.py +++ /dev/null @@ -1,37 +0,0 @@ -# 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/. -import django_rq -from atmo.clusters.jobs import delete_clusters, update_clusters_info -from atmo.jobs.jobs import launch_jobs - - -default_job_timeout = 15 - -job_schedule = { - 'delete_clusters': { - 'cron_string': '* * * * *', - 'func': delete_clusters, - 'timeout': 5 - }, - 'update_clusters_info': { - 'cron_string': '* * * * *', - 'func': update_clusters_info, - 'timeout': 5 - }, - 'launch_jobs': { - 'cron_string': '* * * * *', - 'func': 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=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) diff --git a/atmo/utils/scheduling.py b/atmo/scheduling.py similarity index 78% rename from atmo/utils/scheduling.py rename to atmo/scheduling.py index 7c87895d..4c5e58ba 100644 --- a/atmo/utils/scheduling.py +++ b/atmo/scheduling.py @@ -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/. from uuid import uuid4 from django.conf import settings @@ -10,20 +13,30 @@ def spark_job_add(identifier, notebook_uploadedfile): - # upload the notebook file to S3 + """ + Upload the notebook file to S3 + """ key = 'jobs/{}/{}'.format(identifier, notebook_uploadedfile.name) s3.put_object( - Bucket = settings.AWS_CONFIG['CODE_BUCKET'], - Key = key, - Body = notebook_uploadedfile + Bucket=settings.AWS_CONFIG['CODE_BUCKET'], + Key=key, + Body=notebook_uploadedfile ) return key +def spark_job_get(notebook_s3_key): + obj = s3.get_object( + Bucket=settings.AWS_CONFIG['CODE_BUCKET'], + Key=notebook_s3_key, + ) + return obj['Body'].read() + + def spark_job_remove(notebook_s3_key): s3.delete_object( - Bucket = settings.AWS_CONFIG['CODE_BUCKET'], - Key = notebook_s3_key, + Bucket=settings.AWS_CONFIG['CODE_BUCKET'], + Key=notebook_s3_key, ) @@ -79,6 +92,8 @@ def spark_job_run(user_email, identifier, notebook_uri, result_is_public, size, {'Key': 'Owner', 'Value': user_email}, {'Key': 'Name', 'Value': identifier}, {'Key': 'Application', 'Value': settings.AWS_CONFIG['INSTANCE_APP_TAG']}, + {'Key': 'App', 'Value': settings.AWS_CONFIG['ACCOUNTING_APP_TAG']}, + {'Key': 'Type', 'Value': settings.AWS_CONFIG['ACCOUNTING_TYPE_TAG']}, ] ) return cluster['JobFlowId'] diff --git a/atmo/settings.py b/atmo/settings.py index 892411c3..883d7245 100644 --- a/atmo/settings.py +++ b/atmo/settings.py @@ -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/. """ Django settings for atmo project. @@ -64,7 +67,7 @@ MIDDLEWARE_CLASSES = ( 'django.middleware.security.SecurityMiddleware', - 'atmo.utils.middleware.NewRelicPapertrailMiddleware', + 'atmo.middleware.NewRelicPapertrailMiddleware', 'whitenoise.middleware.WhiteNoiseMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', @@ -98,6 +101,10 @@ 'INSTANCE_APP_TAG': 'telemetry-analysis-worker-instance', 'EMAIL_SOURCE': 'telemetry-alerts@mozilla.com', + # Tags for accounting purposes + 'ACCOUNTING_APP_TAG': 'telemetry-analysis', + 'ACCOUNTING_TYPE_TAG': 'worker', + # Buckets for storing S3 data 'CODE_BUCKET': 'telemetry-analysis-code-2', 'PUBLIC_DATA_BUCKET': 'telemetry-public-analysis-2', @@ -177,9 +184,10 @@ # https://docs.djangoproject.com/en/1.9/topics/i18n/ LANGUAGE_CODE = config('LANGUAGE_CODE', default='en-us') TIME_ZONE = config('TIME_ZONE', default='UTC') -USE_I18N = config('USE_I18N', default=True, cast=bool) -USE_L10N = config('USE_L10N', default=True, cast=bool) +USE_I18N = False +USE_L10N = False USE_TZ = config('USE_TZ', default=True, cast=bool) +DATETIME_FORMAT = 'Y-m-d H:i' # simplified ISO format since we assume UTC STATIC_ROOT = os.path.join(BASE_DIR, 'static') STATIC_URL = '/static/' @@ -228,7 +236,7 @@ 'django.template.context_processors.request', 'django.contrib.messages.context_processors.messages', 'session_csrf.context_processor', - 'atmo.utils.context_processors.settings', + 'atmo.context_processors.settings', ], 'loaders': [ 'django.template.loaders.filesystem.Loader', diff --git a/atmo/static/css/base.css b/atmo/static/css/base.css index 58e31db0..e6039545 100644 --- a/atmo/static/css/base.css +++ b/atmo/static/css/base.css @@ -1,15 +1,31 @@ /* 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/. */ +/* don't let the top navigation bar cover any content */ +body { + padding-top: 30px; + padding-bottom: 90px; +} -.no-select { - -webkit-touch-callout: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; +nav.navbar-dev { + background-color: #00539F; + -webkit-background-size: 40px 40px; + -moz-background-size: 40px 40px; + background-size: 40px 40px; + background-image: + linear-gradient(-45deg, rgba(255, 255, 255, .1) 25%, transparent 25%, + transparent 50%, rgba(255, 255, 255, .1) 50%, rgba(255, 255, 255, .1) 75%, + transparent 75%, transparent); +} +.navbar-dev a, +.navbar-inverse .navbar-nav > li > a, +.navbar-inverse .navbar-brand, +.navbar-inverse .navbar-text, +.navbar-inverse .navbar-text { + color: white; } -/* don't let the top navigation bar cover any content */ -body { padding: 70px; } +.alert-dragons { + margin-top: 40px; + margin-bottom: 0; +} diff --git a/atmo/static/css/dashboard.css b/atmo/static/css/dashboard.css deleted file mode 100644 index 00e0578f..00000000 --- a/atmo/static/css/dashboard.css +++ /dev/null @@ -1,11 +0,0 @@ -/* 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/. */ - -.lead img { - margin: 0.5em; -} - -.editable-table .selected { - background: rgb(217, 237, 247) !important; -} diff --git a/atmo/static/js/csrf.js b/atmo/static/js/csrf.js new file mode 100644 index 00000000..e54b62be --- /dev/null +++ b/atmo/static/js/csrf.js @@ -0,0 +1,12 @@ +(function() { + // Ensure that all AJAX requests sent with jQuery have CSRF tokens + var csrfToken = jQuery("input[name=csrfmiddlewaretoken]").val(); + $.ajaxSetup({ + beforeSend: function(xhr, settings) { + // non-CSRF-safe method that isn't cross domain + if (["GET", "HEAD", "OPTIONS", "TRACE"].indexOf(settings.Type) < 0 && !this.crossDomain) { + xhr.setRequestHeader("X-CSRFToken", csrfToken); + } + } + }); +})(); diff --git a/atmo/static/js/dashboard.js b/atmo/static/js/dashboard.js deleted file mode 100644 index f2291d50..00000000 --- a/atmo/static/js/dashboard.js +++ /dev/null @@ -1,48 +0,0 @@ -$(function() { - // set up form element popovers - $('[data-toggle="popover"]').popover(); - - // apply validation for form controls - $('input, select, textarea').not('[type=submit]').jqBootstrapValidation(); - - // apply datetimepicker initialization - $('.datetimepicker').datetimepicker({ - sideBySide: true, // show the time picker and date picker at the same time - useCurrent: false, // don't automatically set the date when opening the dialog - widgetPositioning: {vertical: 'bottom'}, // make sure the picker shows up below the control - format: 'YYYY-MM-DD h:mm', - }); - - $(".editable-table").each(function(i, e) { // select the first row of each editable table - $(e).find("tr:has(td)").first().addClass("selected"); - updateSelectedIdClasses($(e)); - }); - $(".editable-table tr:has(td)").click(function() { - // allow selecting individual rows - var parentTable = $(this).parents("table").first(); // the table containing the clicked row - parentTable.find("tr").removeClass("selected"); - $(this).addClass("selected"); // select the clicked row - updateSelectedIdClasses(parentTable); - }); -}); - -// given a jQuery table object, update selected object IDs based on which table it is -// for example, an input with the `selected-cluster` class should always contain the ID -// of the selected row in the cluster table -function updateSelectedIdClasses(editableTable) { - // the first two columns of the tables should be row IDs, and row names - var selectedId = editableTable.find("tr.selected td:first").text(); - var selectedName = editableTable.find("tr.selected td:nth-child(2)").text(); - - // update objects as necessary - switch (editableTable.attr("id")) { - case "cluster-table": - $(".selected-cluster").val(selectedId); - $(".selected-cluster-name").text(selectedName); - break; - case "spark-job-table": - $(".selected-spark-job").val(selectedId); - $(".selected-spark-job-name").text(selectedName); - break; - } -} diff --git a/atmo/static/js/forms.js b/atmo/static/js/forms.js new file mode 100644 index 00000000..8b18b72f --- /dev/null +++ b/atmo/static/js/forms.js @@ -0,0 +1,41 @@ +$(function() { + // apply datetimepicker initialization + $('.datetimepicker').datetimepicker({ + sideBySide: true, // show the time picker and date picker at the same time + useCurrent: false, // don't automatically set the date when opening the dialog + format: 'YYYY-MM-DD HH:mm', + stepping: 5, + toolbarPlacement: 'top', + showTodayButton: true, + showClear: true, + showClose: true + }); + + $('form').on('submit', function(event){ + var $form = $(this); + var $submit = $form.find('button[type=submit]'); + var $cancel = $form.find("a:contains('Cancel')"); + var $reset = $form.find('button[type=reset]'); + var submit_label = $submit.text(); + var wait_label = 'Please wait…'; + + // disable submit button and change label + $submit.addClass('disabled').find('.submit-button').text(wait_label); + + // hide cancel button + $cancel.addClass('hidden'); + + var reset_callback = function(event) { + // re-enable submit button + $submit.removeClass('disabled') + .find('.submit-button') + .text(submit_label); + // show cancel button again + $cancel.removeClass('hidden'); + // hide reset button + $reset.addClass('hidden'); + }; + // show reset button to be able to reset form + $reset.removeClass('hidden').click(reset_callback); + }); +}); diff --git a/atmo/static/js/jobs.js b/atmo/static/js/jobs.js new file mode 100644 index 00000000..263398c0 --- /dev/null +++ b/atmo/static/js/jobs.js @@ -0,0 +1,14 @@ +$(function() { + var root = this; + var $holder = $("#notebook-holder"); + var content = $("#notebook-content").val(); + + if (content) { + var parsed = JSON.parse(content); + console.log("rendering"); + var notebook = root.notebook = nb.parse(parsed); + $holder.empty() + $holder.append(notebook.render()); + Prism.highlightAll(); + } +}); diff --git a/atmo/static/lib/ansi_up.js b/atmo/static/lib/ansi_up.js new file mode 100644 index 00000000..74f541a4 --- /dev/null +++ b/atmo/static/lib/ansi_up.js @@ -0,0 +1,327 @@ +// ansi_up.js +// version : 1.3.0 +// author : Dru Nelson +// license : MIT +// http://github.com/drudru/ansi_up + +(function (Date, undefined) { + + var ansi_up, + VERSION = "1.3.0", + + // check for nodeJS + hasModule = (typeof module !== 'undefined'), + + // Normal and then Bright + ANSI_COLORS = [ + [ + { color: "0, 0, 0", 'class': "ansi-black" }, + { color: "187, 0, 0", 'class': "ansi-red" }, + { color: "0, 187, 0", 'class': "ansi-green" }, + { color: "187, 187, 0", 'class': "ansi-yellow" }, + { color: "0, 0, 187", 'class': "ansi-blue" }, + { color: "187, 0, 187", 'class': "ansi-magenta" }, + { color: "0, 187, 187", 'class': "ansi-cyan" }, + { color: "255,255,255", 'class': "ansi-white" } + ], + [ + { color: "85, 85, 85", 'class': "ansi-bright-black" }, + { color: "255, 85, 85", 'class': "ansi-bright-red" }, + { color: "0, 255, 0", 'class': "ansi-bright-green" }, + { color: "255, 255, 85", 'class': "ansi-bright-yellow" }, + { color: "85, 85, 255", 'class': "ansi-bright-blue" }, + { color: "255, 85, 255", 'class': "ansi-bright-magenta" }, + { color: "85, 255, 255", 'class': "ansi-bright-cyan" }, + { color: "255, 255, 255", 'class': "ansi-bright-white" } + ] + ], + + // 256 Colors Palette + PALETTE_COLORS; + + function Ansi_Up() { + this.fg = this.bg = this.fg_truecolor = this.bg_truecolor = null; + this.bright = 0; + } + + Ansi_Up.prototype.setup_palette = function() { + PALETTE_COLORS = []; + // Index 0..15 : System color + (function() { + var i, j; + for (i = 0; i < 2; ++i) { + for (j = 0; j < 8; ++j) { + PALETTE_COLORS.push(ANSI_COLORS[i][j]['color']); + } + } + })(); + + // Index 16..231 : RGB 6x6x6 + // https://gist.github.com/jasonm23/2868981#file-xterm-256color-yaml + (function() { + var levels = [0, 95, 135, 175, 215, 255]; + var format = function (r, g, b) { return levels[r] + ', ' + levels[g] + ', ' + levels[b] }; + var r, g, b; + for (r = 0; r < 6; ++r) { + for (g = 0; g < 6; ++g) { + for (b = 0; b < 6; ++b) { + PALETTE_COLORS.push(format.call(this, r, g, b)); + } + } + } + })(); + + // Index 232..255 : Grayscale + (function() { + var level = 8; + var format = function(level) { return level + ', ' + level + ', ' + level }; + var i; + for (i = 0; i < 24; ++i, level += 10) { + PALETTE_COLORS.push(format.call(this, level)); + } + })(); + }; + + Ansi_Up.prototype.escape_for_html = function (txt) { + return txt.replace(/[&<>]/gm, function(str) { + if (str == "&") return "&"; + if (str == "<") return "<"; + if (str == ">") return ">"; + }); + }; + + Ansi_Up.prototype.linkify = function (txt) { + return txt.replace(/(https?:\/\/[^\s]+)/gm, function(str) { + return "" + str + ""; + }); + }; + + Ansi_Up.prototype.ansi_to_html = function (txt, options) { + return this.process(txt, options, true); + }; + + Ansi_Up.prototype.ansi_to_text = function (txt) { + var options = {}; + return this.process(txt, options, false); + }; + + Ansi_Up.prototype.process = function (txt, options, markup) { + var self = this; + var raw_text_chunks = txt.split(/\033\[/); + var first_chunk = raw_text_chunks.shift(); // the first chunk is not the result of the split + + var color_chunks = raw_text_chunks.map(function (chunk) { + return self.process_chunk(chunk, options, markup); + }); + + color_chunks.unshift(first_chunk); + + return color_chunks.join(''); + }; + + Ansi_Up.prototype.process_chunk = function (text, options, markup) { + + // Are we using classes or styles? + options = typeof options == 'undefined' ? {} : options; + var use_classes = typeof options.use_classes != 'undefined' && options.use_classes; + var key = use_classes ? 'class' : 'color'; + + // Each 'chunk' is the text after the CSI (ESC + '[') and before the next CSI/EOF. + // + // This regex matches four groups within a chunk. + // + // The first and third groups match code type. + // We supported only SGR command. It has empty first group and 'm' in third. + // + // The second group matches all of the number+semicolon command sequences + // before the 'm' (or other trailing) character. + // These are the graphics or SGR commands. + // + // The last group is the text (including newlines) that is colored by + // the other group's commands. + var matches = text.match(/^([!\x3c-\x3f]*)([\d;]*)([\x20-\x2c]*[\x40-\x7e])([\s\S]*)/m); + + if (!matches) return text; + + var orig_txt = matches[4]; + var nums = matches[2].split(';'); + + // We currently support only "SGR" (Select Graphic Rendition) + // Simply ignore if not a SGR command. + if (matches[1] !== '' || matches[3] !== 'm') { + return orig_txt; + } + + if (!markup) { + return orig_txt; + } + + var self = this; + + while (nums.length > 0) { + var num_str = nums.shift(); + var num = parseInt(num_str); + + if (isNaN(num) || num === 0) { + self.fg = self.bg = null; + self.bright = 0; + } else if (num === 1) { + self.bright = 1; + } else if (num == 39) { + self.fg = null; + } else if (num == 49) { + self.bg = null; + } else if ((num >= 30) && (num < 38)) { + self.fg = ANSI_COLORS[self.bright][(num % 10)][key]; + } else if ((num >= 90) && (num < 98)) { + self.fg = ANSI_COLORS[1][(num % 10)][key]; + } else if ((num >= 40) && (num < 48)) { + self.bg = ANSI_COLORS[0][(num % 10)][key]; + } else if ((num >= 100) && (num < 108)) { + self.bg = ANSI_COLORS[1][(num % 10)][key]; + } else if (num === 38 || num === 48) { // extend color (38=fg, 48=bg) + (function() { + var is_foreground = (num === 38); + if (nums.length >= 1) { + var mode = nums.shift(); + if (mode === '5' && nums.length >= 1) { // palette color + var palette_index = parseInt(nums.shift()); + if (palette_index >= 0 && palette_index <= 255) { + if (!use_classes) { + if (!PALETTE_COLORS) { + self.setup_palette.call(self); + } + if (is_foreground) { + self.fg = PALETTE_COLORS[palette_index]; + } else { + self.bg = PALETTE_COLORS[palette_index]; + } + } else { + var klass = (palette_index >= 16) + ? ('ansi-palette-' + palette_index) + : ANSI_COLORS[palette_index > 7 ? 1 : 0][palette_index % 8]['class']; + if (is_foreground) { + self.fg = klass; + } else { + self.bg = klass; + } + } + } + } else if(mode === '2' && nums.length >= 3) { // true color + var r = parseInt(nums.shift()); + var g = parseInt(nums.shift()); + var b = parseInt(nums.shift()); + if ((r >= 0 && r <= 255) && (g >= 0 && g <= 255) && (b >= 0 && b <= 255)) { + var color = r + ', ' + g + ', ' + b; + if (!use_classes) { + if (is_foreground) { + self.fg = color; + } else { + self.bg = color; + } + } else { + if (is_foreground) { + self.fg = 'ansi-truecolor'; + self.fg_truecolor = color; + } else { + self.bg = 'ansi-truecolor'; + self.bg_truecolor = color; + } + } + } + } + } + })(); + } + } + + if ((self.fg === null) && (self.bg === null)) { + return orig_txt; + } else { + var styles = []; + var classes = []; + var data = {}; + var render_data = function (data) { + var fragments = []; + var key; + for (key in data) { + if (data.hasOwnProperty(key)) { + fragments.push('data-' + key + '="' + this.escape_for_html(data[key]) + '"'); + } + } + return fragments.length > 0 ? ' ' + fragments.join(' ') : ''; + }; + + if (self.fg) { + if (use_classes) { + classes.push(self.fg + "-fg"); + if (self.fg_truecolor !== null) { + data['ansi-truecolor-fg'] = self.fg_truecolor; + self.fg_truecolor = null; + } + } else { + styles.push("color:rgb(" + self.fg + ")"); + } + } + if (self.bg) { + if (use_classes) { + classes.push(self.bg + "-bg"); + if (self.bg_truecolor !== null) { + data['ansi-truecolor-bg'] = self.bg_truecolor; + self.bg_truecolor = null; + } + } else { + styles.push("background-color:rgb(" + self.bg + ")"); + } + } + if (use_classes) { + return '' + orig_txt + ''; + } else { + return '' + orig_txt + ''; + } + } + }; + + // Module exports + ansi_up = { + + escape_for_html: function (txt) { + var a2h = new Ansi_Up(); + return a2h.escape_for_html(txt); + }, + + linkify: function (txt) { + var a2h = new Ansi_Up(); + return a2h.linkify(txt); + }, + + ansi_to_html: function (txt, options) { + var a2h = new Ansi_Up(); + return a2h.ansi_to_html(txt, options); + }, + + ansi_to_text: function (txt) { + var a2h = new Ansi_Up(); + return a2h.ansi_to_text(txt); + }, + + ansi_to_html_obj: function () { + return new Ansi_Up(); + } + }; + + // CommonJS module is defined + if (hasModule) { + module.exports = ansi_up; + } + /*global ender:false */ + if (typeof window !== 'undefined' && typeof ender === 'undefined') { + window.ansi_up = ansi_up; + } + /*global define:false */ + if (typeof define === "function" && define.amd) { + define("ansi_up", [], function () { + return ansi_up; + }); + } +})(Date); diff --git a/atmo/static/lib/es5-shim.min.js b/atmo/static/lib/es5-shim.min.js new file mode 100644 index 00000000..14b24f15 --- /dev/null +++ b/atmo/static/lib/es5-shim.min.js @@ -0,0 +1,7 @@ +/*! + * https://github.com/es-shims/es5-shim + * @license es5-shim Copyright 2009-2015 by contributors, MIT License + * see https://github.com/es-shims/es5-shim/blob/v4.5.9/LICENSE + */ +(function(t,r){"use strict";if(typeof define==="function"&&define.amd){define(r)}else if(typeof exports==="object"){module.exports=r()}else{t.returnExports=r()}})(this,function(){var t=Array;var r=t.prototype;var e=Object;var n=e.prototype;var i=Function;var a=i.prototype;var o=String;var f=o.prototype;var u=Number;var l=u.prototype;var s=r.slice;var c=r.splice;var v=r.push;var h=r.unshift;var p=r.concat;var y=r.join;var d=a.call;var g=a.apply;var w=Math.max;var b=Math.min;var T=n.toString;var m=typeof Symbol==="function"&&typeof Symbol.toStringTag==="symbol";var D;var S=Function.prototype.toString,x=/^\s*class /,O=function isES6ClassFn(t){try{var r=S.call(t);var e=r.replace(/\/\/.*\n/g,"");var n=e.replace(/\/\*[.\s\S]*\*\//g,"");var i=n.replace(/\n/gm," ").replace(/ {2}/g," ");return x.test(i)}catch(a){return false}},j=function tryFunctionObject(t){try{if(O(t)){return false}S.call(t);return true}catch(r){return false}},E="[object Function]",I="[object GeneratorFunction]",D=function isCallable(t){if(!t){return false}if(typeof t!=="function"&&typeof t!=="object"){return false}if(m){return j(t)}if(O(t)){return false}var r=T.call(t);return r===E||r===I};var M;var U=RegExp.prototype.exec,F=function tryRegexExec(t){try{U.call(t);return true}catch(r){return false}},N="[object RegExp]";M=function isRegex(t){if(typeof t!=="object"){return false}return m?F(t):T.call(t)===N};var C;var k=String.prototype.valueOf,A=function tryStringObject(t){try{k.call(t);return true}catch(r){return false}},R="[object String]";C=function isString(t){if(typeof t==="string"){return true}if(typeof t!=="object"){return false}return m?A(t):T.call(t)===R};var P=e.defineProperty&&function(){try{var t={};e.defineProperty(t,"x",{enumerable:false,value:t});for(var r in t){return false}return t.x===t}catch(n){return false}}();var $=function(t){var r;if(P){r=function(t,r,n,i){if(!i&&r in t){return}e.defineProperty(t,r,{configurable:true,enumerable:false,writable:true,value:n})}}else{r=function(t,r,e,n){if(!n&&r in t){return}t[r]=e}}return function defineProperties(e,n,i){for(var a in n){if(t.call(n,a)){r(e,a,n[a],i)}}}}(n.hasOwnProperty);var J=function isPrimitive(t){var r=typeof t;return t===null||r!=="object"&&r!=="function"};var Y=u.isNaN||function isActualNaN(t){return t!==t};var Z={ToInteger:function ToInteger(t){var r=+t;if(Y(r)){r=0}else if(r!==0&&r!==1/0&&r!==-(1/0)){r=(r>0||-1)*Math.floor(Math.abs(r))}return r},ToPrimitive:function ToPrimitive(t){var r,e,n;if(J(t)){return t}e=t.valueOf;if(D(e)){r=e.call(t);if(J(r)){return r}}n=t.toString;if(D(n)){r=n.call(t);if(J(r)){return r}}throw new TypeError},ToObject:function(t){if(t==null){throw new TypeError("can't convert "+t+" to object")}return e(t)},ToUint32:function ToUint32(t){return t>>>0}};var z=function Empty(){};$(a,{bind:function bind(t){var r=this;if(!D(r)){throw new TypeError("Function.prototype.bind called on incompatible "+r)}var n=s.call(arguments,1);var a;var o=function(){if(this instanceof a){var i=g.call(r,this,p.call(n,s.call(arguments)));if(e(i)===i){return i}return this}else{return g.call(r,t,p.call(n,s.call(arguments)))}};var f=w(0,r.length-n.length);var u=[];for(var l=0;l1){a=arguments[1]}if(!D(t)){throw new TypeError("Array.prototype.forEach callback must be a function")}while(++n1){o=arguments[1]}if(!D(r)){throw new TypeError("Array.prototype.map callback must be a function")}for(var f=0;f1){o=arguments[1]}if(!D(t)){throw new TypeError("Array.prototype.filter callback must be a function")}for(var f=0;f1){i=arguments[1]}if(!D(t)){throw new TypeError("Array.prototype.every callback must be a function")}for(var a=0;a1){i=arguments[1]}if(!D(t)){throw new TypeError("Array.prototype.some callback must be a function")}for(var a=0;a=2){a=arguments[1]}else{do{if(i in e){a=e[i++];break}if(++i>=n){throw new TypeError("reduce of empty array with no initial value")}}while(true)}for(;i=2){i=arguments[1]}else{do{if(a in e){i=e[a--];break}if(--a<0){throw new TypeError("reduceRight of empty array with no initial value")}}while(true)}if(a<0){return i}do{if(a in e){i=t(i,e[a],a,r)}}while(a--);return i}},!at);var ot=r.indexOf&&[0,1].indexOf(1,2)!==-1;$(r,{indexOf:function indexOf(t){var r=et&&C(this)?X(this,""):Z.ToObject(this);var e=Z.ToUint32(r.length);if(e===0){return-1}var n=0;if(arguments.length>1){n=Z.ToInteger(arguments[1])}n=n>=0?n:w(0,e+n);for(;n1){n=b(n,Z.ToInteger(arguments[1]))}n=n>=0?n:e-Math.abs(n);for(;n>=0;n--){if(n in r&&t===r[n]){return n}}return-1}},ft);var ut=function(){var t=[1,2];var r=t.splice();return t.length===2&&_(r)&&r.length===0}();$(r,{splice:function splice(t,r){if(arguments.length===0){return[]}else{return c.apply(this,arguments)}}},!ut);var lt=function(){var t={};r.splice.call(t,0,0,1);return t.length===1}();$(r,{splice:function splice(t,r){if(arguments.length===0){return[]}var e=arguments;this.length=w(Z.ToInteger(this.length),0);if(arguments.length>0&&typeof r!=="number"){e=H(arguments);if(e.length<2){K(e,this.length-t)}else{e[1]=Z.ToInteger(r)}}return c.apply(this,e)}},!lt);var st=function(){var r=new t(1e5);r[8]="x";r.splice(1,1);return r.indexOf("x")===7}();var ct=function(){var t=256;var r=[];r[t]="a";r.splice(t+1,0,"b");return r[t]==="a"}();$(r,{splice:function splice(t,r){var e=Z.ToObject(this);var n=[];var i=Z.ToUint32(e.length);var a=Z.ToInteger(t);var f=a<0?w(i+a,0):b(a,i);var u=b(w(Z.ToInteger(r),0),i-f);var l=0;var s;while(ly){delete e[l-1];l-=1}}else if(v>u){l=i-u;while(l>f){s=o(l+u-1);h=o(l+v-1);if(G(e,s)){e[h]=e[s]}else{delete e[h]}l-=1}}l=f;for(var d=0;d=0&&!_(t)&&D(t.callee)};var Ct=Ft(arguments)?Ft:Nt;$(e,{keys:function keys(t){var r=D(t);var e=Ct(t);var n=t!==null&&typeof t==="object";var i=n&&C(t);if(!n&&!r&&!e){throw new TypeError("Object.keys called on a non-object")}var a=[];var f=St&&r;if(i&&xt||e){for(var u=0;u11){return t+1}return t},getMonth:function getMonth(){if(!this||!(this instanceof Date)){throw new TypeError("this is not a Date object.")}var t=Bt(this);var r=Ht(this);if(t<0&&r>11){return 0}return r},getDate:function getDate(){if(!this||!(this instanceof Date)){throw new TypeError("this is not a Date object.")}var t=Bt(this);var r=Ht(this);var e=Wt(this);if(t<0&&r>11){if(r===12){return e}var n=nr(0,t+1);return n-e+1}return e},getUTCFullYear:function getUTCFullYear(){if(!this||!(this instanceof Date)){throw new TypeError("this is not a Date object.")}var t=Lt(this);if(t<0&&Xt(this)>11){return t+1}return t},getUTCMonth:function getUTCMonth(){if(!this||!(this instanceof Date)){throw new TypeError("this is not a Date object.")}var t=Lt(this);var r=Xt(this);if(t<0&&r>11){return 0}return r},getUTCDate:function getUTCDate(){if(!this||!(this instanceof Date)){throw new TypeError("this is not a Date object.")}var t=Lt(this);var r=Xt(this);var e=qt(this);if(t<0&&r>11){if(r===12){return e}var n=nr(0,t+1);return n-e+1}return e}},Pt);$(Date.prototype,{toUTCString:function toUTCString(){if(!this||!(this instanceof Date)){throw new TypeError("this is not a Date object.")}var t=Kt(this);var r=qt(this);var e=Xt(this);var n=Lt(this);var i=Qt(this);var a=Vt(this);var o=_t(this);return rr[t]+", "+(r<10?"0"+r:r)+" "+er[e]+" "+n+" "+(i<10?"0"+i:i)+":"+(a<10?"0"+a:a)+":"+(o<10?"0"+o:o)+" GMT"}},Pt||Yt);$(Date.prototype,{toDateString:function toDateString(){if(!this||!(this instanceof Date)){throw new TypeError("this is not a Date object.")}var t=this.getDay();var r=this.getDate();var e=this.getMonth();var n=this.getFullYear();return rr[t]+" "+er[e]+" "+(r<10?"0"+r:r)+" "+n}},Pt||Zt);if(Pt||zt){Date.prototype.toString=function toString(){if(!this||!(this instanceof Date)){throw new TypeError("this is not a Date object.")}var t=this.getDay();var r=this.getDate();var e=this.getMonth();var n=this.getFullYear();var i=this.getHours();var a=this.getMinutes();var o=this.getSeconds();var f=this.getTimezoneOffset();var u=Math.floor(Math.abs(f)/60);var l=Math.floor(Math.abs(f)%60);return rr[t]+" "+er[e]+" "+(r<10?"0"+r:r)+" "+n+" "+(i<10?"0"+i:i)+":"+(a<10?"0"+a:a)+":"+(o<10?"0"+o:o)+" GMT"+(f>0?"-":"+")+(u<10?"0"+u:u)+(l<10?"0"+l:l)};if(P){e.defineProperty(Date.prototype,"toString",{configurable:true,enumerable:false,writable:true})}}var ir=-621987552e5;var ar="-000001";var or=Date.prototype.toISOString&&new Date(ir).toISOString().indexOf(ar)===-1;var fr=Date.prototype.toISOString&&new Date(-1).toISOString()!=="1969-12-31T23:59:59.999Z";var ur=d.bind(Date.prototype.getTime);$(Date.prototype,{toISOString:function toISOString(){if(!isFinite(this)||!isFinite(ur(this))){throw new RangeError("Date.prototype.toISOString called on non-finite value.")}var t=Lt(this);var r=Xt(this);t+=Math.floor(r/12);r=(r%12+12)%12;var e=[r+1,qt(this),Qt(this),Vt(this),_t(this)];t=(t<0?"-":t>9999?"+":"")+L("00000"+Math.abs(t),0<=t&&t<=9999?-4:-6);for(var n=0;n=7&&l>hr){var p=Math.floor(l/hr)*hr;var y=Math.floor(p/1e3);v+=y;h-=y*1e3}c=s===1&&o(e)===e?new t(r.parse(e)):s>=7?new t(e,n,i,a,f,v,h):s>=6?new t(e,n,i,a,f,v):s>=5?new t(e,n,i,a,f):s>=4?new t(e,n,i,a):s>=3?new t(e,n,i):s>=2?new t(e,n):s>=1?new t(e instanceof t?+e:e):new t}else{c=t.apply(this,arguments)}if(!J(c)){$(c,{constructor:r},true)}return c};var e=new RegExp("^"+"(\\d{4}|[+-]\\d{6})"+"(?:-(\\d{2})"+"(?:-(\\d{2})"+"(?:"+"T(\\d{2})"+":(\\d{2})"+"(?:"+":(\\d{2})"+"(?:(\\.\\d{1,}))?"+")?"+"("+"Z|"+"(?:"+"([-+])"+"(\\d{2})"+":(\\d{2})"+")"+")?)?)?)?"+"$");var n=[0,31,59,90,120,151,181,212,243,273,304,334,365];var i=function dayFromMonth(t,r){var e=r>1?1:0;return n[r]+Math.floor((t-1969+e)/4)-Math.floor((t-1901+e)/100)+Math.floor((t-1601+e)/400)+365*(t-1970)};var a=function toUTC(r){var e=0;var n=r;if(pr&&n>hr){var i=Math.floor(n/hr)*hr;var a=Math.floor(i/1e3);e+=a;n-=a*1e3}return u(new t(1970,0,1,0,0,e,n))};for(var f in t){if(G(t,f)){r[f]=t[f]}}$(r,{now:t.now,UTC:t.UTC},true);r.prototype=t.prototype;$(r.prototype,{constructor:r},true);var l=function parse(r){var n=e.exec(r);if(n){var o=u(n[1]),f=u(n[2]||1)-1,l=u(n[3]||1)-1,s=u(n[4]||0),c=u(n[5]||0),v=u(n[6]||0),h=Math.floor(u(n[7]||0)*1e3),p=Boolean(n[4]&&!n[8]),y=n[9]==="-"?1:-1,d=u(n[10]||0),g=u(n[11]||0),w;var b=c>0||v>0||h>0;if(s<(b?24:25)&&c<60&&v<60&&h<1e3&&f>-1&&f<12&&d<24&&g<60&&l>-1&&l=0){e+=dr.data[r];dr.data[r]=Math.floor(e/t);e=e%t*dr.base}},numToString:function numToString(){var t=dr.size;var r="";while(--t>=0){if(r!==""||t===0||dr.data[t]!==0){var e=o(dr.data[t]);if(r===""){r=e}else{r+=L("0000000",0,7-e.length)+e}}}return r},pow:function pow(t,r,e){return r===0?e:r%2===1?pow(t,r-1,e*t):pow(t*t,r/2,e)},log:function log(t){var r=0;var e=t;while(e>=4096){r+=12;e/=4096}while(e>=2){r+=1;e/=2}return r}};var gr=function toFixed(t){var r,e,n,i,a,f,l,s;r=u(t);r=Y(r)?0:Math.floor(r);if(r<0||r>20){throw new RangeError("Number.toFixed called with invalid number of decimals")}e=u(this);if(Y(e)){return"NaN"}if(e<=-1e21||e>=1e21){return o(e)}n="";if(e<0){n="-";e=-e}i="0";if(e>1e-21){a=dr.log(e*dr.pow(2,69,1))-69;f=a<0?e*dr.pow(2,-a,1):e/dr.pow(2,a,1);f*=4503599627370496;a=52-a;if(a>0){dr.multiply(0,f);l=r;while(l>=7){dr.multiply(1e7,0);l-=7}dr.multiply(dr.pow(10,l,1),0);l=a-1;while(l>=23){dr.divide(1<<23);l-=23}dr.divide(1<0){s=i.length;if(s<=r){i=n+L("0.0000000000000000000",0,r-s+2)+i}else{i=n+L(i,0,s-r)+"."+L(i,s-r)}}else{i=n+i}return i};$(l,{toFixed:gr},yr);var wr=function(){try{return 1..toPrecision(undefined)==="1"}catch(t){return true}}();var br=l.toPrecision;$(l,{toPrecision:function toPrecision(t){return typeof t==="undefined"?br.call(this):br.call(this,t)}},wr);if("ab".split(/(?:ab)*/).length!==2||".".split(/(.?)(.?)/).length!==4||"tesst".split(/(s)*/)[1]==="t"||"test".split(/(?:)/,-1).length!==4||"".split(/.?/).length||".".split(/()()/).length>1){(function(){var t=typeof/()??/.exec("")[1]==="undefined";var r=Math.pow(2,32)-1;f.split=function(e,n){var i=String(this);if(typeof e==="undefined"&&n===0){return[]}if(!M(e)){return X(this,e,n)}var a=[];var o=(e.ignoreCase?"i":"")+(e.multiline?"m":"")+(e.unicode?"u":"")+(e.sticky?"y":""),f=0,u,l,s,c;var h=new RegExp(e.source,o+"g");if(!t){u=new RegExp("^"+h.source+"$(?!\\s)",o)}var p=typeof n==="undefined"?r:Z.ToUint32(n);l=h.exec(i);while(l){s=l.index+l[0].length;if(s>f){K(a,L(i,f,l.index));if(!t&&l.length>1){l[0].replace(u,function(){for(var t=1;t1&&l.index=p){break}}if(h.lastIndex===l.index){h.lastIndex++}l=h.exec(i)}if(f===i.length){if(c||!h.test("")){K(a,"")}}else{K(a,L(i,f))}return a.length>p?H(a,0,p):a}})()}else if("0".split(void 0,0).length){f.split=function split(t,r){if(typeof t==="undefined"&&r===0){return[]}return X(this,t,r)}}var Tr=f.replace;var mr=function(){var t=[];"x".replace(/x(.)?/g,function(r,e){K(t,e)});return t.length===1&&typeof t[0]==="undefined"}();if(!mr){f.replace=function replace(t,r){var e=D(r);var n=M(t)&&/\)[*?]/.test(t.source);if(!e||!n){return Tr.call(this,t,r)}else{var i=function(e){var n=arguments.length;var i=t.lastIndex;t.lastIndex=0;var a=t.exec(e)||[];t.lastIndex=i;K(a,arguments[n-2],arguments[n-1]);return r.apply(this,a)};return Tr.call(this,t,i)}}}var Dr=f.substr;var Sr="".substr&&"0b".substr(-1)!=="b";$(f,{substr:function substr(t,r){var e=t;if(t<0){e=w(this.length+t,0)}return Dr.call(this,e,r)}},Sr);var xr=" \n\x0B\f\r \xa0\u1680\u180e\u2000\u2001\u2002\u2003"+"\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028"+"\u2029\ufeff";var Or="\u200b";var jr="["+xr+"]";var Er=new RegExp("^"+jr+jr+"*");var Ir=new RegExp(jr+jr+"*$");var Mr=f.trim&&(xr.trim()||!Or.trim());$(f,{trim:function trim(){if(typeof this==="undefined"||this===null){throw new TypeError("can't convert "+this+" to object")}return o(this).replace(Er,"").replace(Ir,"")}},Mr);var Ur=d.bind(String.prototype.trim);var Fr=f.lastIndexOf&&"abc\u3042\u3044".lastIndexOf("\u3042\u3044",2)!==-1;$(f,{lastIndexOf:function lastIndexOf(t){if(typeof this==="undefined"||this===null){throw new TypeError("can't convert "+this+" to object")}var r=o(this);var e=o(t);var n=arguments.length>1?u(arguments[1]):NaN;var i=Y(n)?Infinity:Z.ToInteger(n);var a=b(w(i,0),r.length);var f=e.length;var l=a+f;while(l>0){l=w(0,l-f);var s=q(L(r,l,a+f),e);if(s!==-1){return l+s}}return-1}},Fr);var Nr=f.lastIndexOf;$(f,{lastIndexOf:function lastIndexOf(t){return Nr.apply(this,arguments)}},f.lastIndexOf.length!==1);if(parseInt(xr+"08")!==8||parseInt(xr+"0x16")!==22){parseInt=function(t){var r=/^[\-+]?0[xX]/;return function parseInt(e,n){var i=Ur(String(e));var a=u(n)||(r.test(i)?16:10);return t(i,a)}}(parseInt)}if(1/parseFloat("-0")!==-Infinity){parseFloat=function(t){return function parseFloat(r){var e=Ur(String(r));var n=t(e);return n===0&&L(e,0,1)==="-"?-0:n}}(parseFloat)}if(String(new RangeError("test"))!=="RangeError: test"){var Cr=function toString(){if(typeof this==="undefined"||this===null){throw new TypeError("can't convert "+this+" to object")}var t=this.name;if(typeof t==="undefined"){t="Error"}else if(typeof t!=="string"){t=o(t)}var r=this.message;if(typeof r==="undefined"){r=""}else if(typeof r!=="string"){r=o(r)}if(!t){return r}if(!r){return t}return t+": "+r};Error.prototype.toString=Cr}if(P){var kr=function(t,r){if(Q(t,r)){var e=Object.getOwnPropertyDescriptor(t,r);if(e.configurable){e.enumerable=false;Object.defineProperty(t,r,e)}}};kr(Error.prototype,"message");if(Error.prototype.message!==""){Error.prototype.message=""}kr(Error.prototype,"name")}if(String(/a/gim)!=="/a/gim"){var Ar=function toString(){var t="/"+this.source+"/";if(this.global){t+="g"}if(this.ignoreCase){t+="i"}if(this.multiline){t+="m"}return t};RegExp.prototype.toString=Ar}}); +//# sourceMappingURL=es5-shim.map diff --git a/atmo/static/lib/jqBootstrapValidation.min.js b/atmo/static/lib/jqBootstrapValidation.min.js deleted file mode 100644 index c5d52362..00000000 --- a/atmo/static/lib/jqBootstrapValidation.min.js +++ /dev/null @@ -1,4 +0,0 @@ -/*! jqBootstrapValidation - v1.3.7 - 2013-05-07 -* http://reactiveraven.github.com/jqBootstrapValidation -* Copyright (c) 2013 David Godfrey; Licensed MIT */ -(function(e){function s(e){return new RegExp("^"+e+"$")}function o(e,t){var n=Array.prototype.slice.call(arguments,2),r=e.split("."),i=r.pop();for(var s=0;s0});u.trigger("submit.validation"),i.trigger("validationLostFocus.validation"),s.each(function(t,n){var i=e(n);if(i.hasClass("warning")||i.hasClass("error"))i.removeClass("warning").addClass("error"),r++}),r?(o.options.preventSubmit&&(t.preventDefault(),t.stopImmediatePropagation()),n.addClass("error"),e.isFunction(o.options.submitError)&&o.options.submitError(n,t,u.jqBootstrapValidation("collectErrors",!0))):(n.removeClass("error"),e.isFunction(o.options.submitSuccess)&&o.options.submitSuccess(n,t))}),this.each(function(){var n=e(this),s=n.parents(".control-group").first(),u=s.find(".help-block").first(),a=n.parents("form").first(),f=[];!u.length&&o.options.autoAdd&&o.options.autoAdd.helpBlocks&&(u=e('
'),s.find(".controls").append(u),t.push(u[0]));if(o.options.sniffHtml){var l;n.data("validationPatternPattern")&&n.attr("pattern",n.data("validationPatternPattern")),n.attr("pattern")!==undefined&&(l="Not in the expected format",n.data("validationPatternMessage")&&(l=n.data("validationPatternMessage")),n.data("validationPatternMessage",l),n.data("validationPatternRegex",n.attr("pattern")));if(n.attr("max")!==undefined||n.attr("aria-valuemax")!==undefined){var c=n.attr("max")!==undefined?n.attr("max"):n.attr("aria-valuemax");l="Too high: Maximum of '"+c+"'",n.data("validationMaxMessage")&&(l=n.data("validationMaxMessage")),n.data("validationMaxMessage",l),n.data("validationMaxMax",c)}if(n.attr("min")!==undefined||n.attr("aria-valuemin")!==undefined){var h=n.attr("min")!==undefined?n.attr("min"):n.attr("aria-valuemin");l="Too low: Minimum of '"+h+"'",n.data("validationMinMessage")&&(l=n.data("validationMinMessage")),n.data("validationMinMessage",l),n.data("validationMinMin",h)}n.attr("maxlength")!==undefined&&(l="Too long: Maximum of '"+n.attr("maxlength")+"' characters",n.data("validationMaxlengthMessage")&&(l=n.data("validationMaxlengthMessage")),n.data("validationMaxlengthMessage",l),n.data("validationMaxlengthMaxlength",n.attr("maxlength"))),n.attr("minlength")!==undefined&&(l="Too short: Minimum of '"+n.attr("minlength")+"' characters",n.data("validationMinlengthMessage")&&(l=n.data("validationMinlengthMessage")),n.data("validationMinlengthMessage",l),n.data("validationMinlengthMinlength",n.attr("minlength")));if(n.attr("required")!==undefined||n.attr("aria-required")!==undefined)l=o.builtInValidators.required.message,n.data("validationRequiredMessage")&&(l=n.data("validationRequiredMessage")),n.data("validationRequiredMessage",l);if(n.attr("type")!==undefined&&n.attr("type").toLowerCase()==="number"){l=o.validatorTypes.number.message,n.data("validationNumberMessage")&&(l=n.data("validationNumberMessage")),n.data("validationNumberMessage",l);var p=o.validatorTypes.number.step;n.data("validationNumberStep")&&(p=n.data("validationNumberStep")),n.data("validationNumberStep",p);var d=o.validatorTypes.number.decimal;n.data("validationNumberDecimal")&&(d=n.data("validationNumberDecimal")),n.data("validationNumberDecimal",d)}n.attr("type")!==undefined&&n.attr("type").toLowerCase()==="email"&&(l="Not a valid email address",n.data("validationEmailMessage")&&(l=n.data("validationEmailMessage")),n.data("validationEmailMessage",l)),n.attr("minchecked")!==undefined&&(l="Not enough options checked; Minimum of '"+n.attr("minchecked")+"' required",n.data("validationMincheckedMessage")&&(l=n.data("validationMincheckedMessage")),n.data("validationMincheckedMessage",l),n.data("validationMincheckedMinchecked",n.attr("minchecked"))),n.attr("maxchecked")!==undefined&&(l="Too many options checked; Maximum of '"+n.attr("maxchecked")+"' required",n.data("validationMaxcheckedMessage")&&(l=n.data("validationMaxcheckedMessage")),n.data("validationMaxcheckedMessage",l),n.data("validationMaxcheckedMaxchecked",n.attr("maxchecked")))}n.data("validation")!==undefined&&(f=n.data("validation").split(",")),e.each(n.data(),function(e,t){var n=e.replace(/([A-Z])/g,",$1").split(",");n[0]==="validation"&&n[1]&&f.push(n[1])});var v=f,m=[],g=function(e,t){f[e]=r(t)},y=function(t,i){if(n.data("validation"+i+"Shortcut")!==undefined)e.each(n.data("validation"+i+"Shortcut").split(","),function(e,t){m.push(t)});else if(o.builtInValidators[i.toLowerCase()]){var s=o.builtInValidators[i.toLowerCase()];s.type.toLowerCase()==="shortcut"&&e.each(s.shortcut.split(","),function(e,t){t=r(t),m.push(t),f.push(t)})}};do e.each(f,g),f=e.unique(f),m=[],e.each(v,y),v=m;while(v.length>0);var b={};e.each(f,function(t,i){var s=n.data("validation"+i+"Message"),u=!!s,a=!1;s||(s="'"+i+"' validation failed "),e.each(o.validatorTypes,function(t,o){b[t]===undefined&&(b[t]=[]);if(!a&&n.data("validation"+i+r(o.name))!==undefined){var f=o.init(n,i);u&&(f.message=s),b[t].push(e.extend(!0,{name:r(o.name),message:s},f)),a=!0}});if(!a&&o.builtInValidators[i.toLowerCase()]){var f=e.extend(!0,{},o.builtInValidators[i.toLowerCase()]);u&&(f.message=s);var l=f.type.toLowerCase();l==="shortcut"?a=!0:e.each(o.validatorTypes,function(t,s){b[t]===undefined&&(b[t]=[]),!a&&l===t.toLowerCase()&&(n.data("validation"+i+r(s.name),f[s.name.toLowerCase()]),b[l].push(e.extend(f,s.init(n,i))),a=!0)})}a||e.error("Cannot find validation info for '"+i+"'")}),u.data("original-contents",u.data("original-contents")?u.data("original-contents"):u.html()),u.data("original-role",u.data("original-role")?u.data("original-role"):u.attr("role")),s.data("original-classes",s.data("original-clases")?s.data("original-classes"):s.attr("class")),n.data("original-aria-invalid",n.data("original-aria-invalid")?n.data("original-aria-invalid"):n.attr("aria-invalid")),n.bind("validation.validation",function(t,r){var s=i(n),u=[];return e.each(b,function(t,i){(s||s.length||r&&r.includeEmpty||!!o.validatorTypes[t].includeEmpty||!!o.validatorTypes[t].blockSubmit&&r&&!!r.submitting)&&e.each(i,function(e,r){o.validatorTypes[t].validate(n,s,r)&&u.push(r.message)})}),u}),n.bind("getValidators.validation",function(){return b});var w=0;e.each(b,function(e,t){w+=t.length}),n.bind("getValidatorCount.validation",function(){return w}),n.bind("submit.validation",function(){return n.triggerHandler("change.validation",{submitting:!0})}),n.bind((o.options.bindEvents.length>0?o.options.bindEvents:["keyup","focus","blur","click","keydown","keypress","change"]).concat(["revalidate"]).join(".validation ")+".validation",function(t,r){var f=i(n),l=[];r&&!!r.submitting?s.data("jqbvIsSubmitting",!0):t.type!=="revalidate"&&s.data("jqbvIsSubmitting",!1);var c=!!s.data("jqbvIsSubmitting");s.find("input,textarea,select").each(function(t,i){var s=l.length;e.each(e(i).triggerHandler("validation.validation",r),function(e,t){l.push(t)});if(l.length>s)e(i).attr("aria-invalid","true");else{var o=n.data("original-aria-invalid");e(i).attr("aria-invalid",o!==undefined?o:!1)}}),a.find("input,select,textarea").not(n).not('[name="'+n.attr("name")+'"]').trigger("validationLostFocus.validation"),l=e.unique(l.sort()),l.length?(s.removeClass("success error warning").addClass(c?"error":"warning"),o.options.semanticallyStrict&&l.length===1?u.html(l[0]+(o.options.prependExistingHelpBlock?u.data("original-contents"):"")):u.html('
  • '+l.join("
  • ")+"
"+(o.options.prependExistingHelpBlock?u.data("original-contents"):""))):(s.removeClass("warning error success"),f.length>0&&s.addClass("success"),u.html(u.data("original-contents"))),t.type==="blur"&&s.removeClass("success")}),n.bind("validationLostFocus.validation",function(){s.removeClass("success")})})},destroy:function(){return this.each(function(){var n=e(this),r=n.parents(".control-group").first(),i=r.find(".help-block").first(),s=n.parents("form").first();n.unbind(".validation"),s.unbind(".validationSubmit"),i.html(i.data("original-contents")),r.attr("class",r.data("original-classes")),n.attr("aria-invalid",n.data("original-aria-invalid")),i.attr("role",n.data("original-role")),e.inArray(i[0],t)>-1&&i.remove()})},collectErrors:function(t){var n={};return this.each(function(t,r){var i=e(r),s=i.attr("name"),o=i.triggerHandler("validation.validation",{includeEmpty:!0});n[s]=e.extend(!0,o,n[s])}),e.each(n,function(e,t){t.length===0&&delete n[e]}),n},hasErrors:function(){var t=[];return this.find("input,select,textarea").add(this).each(function(n,r){t=t.concat(e(r).triggerHandler("getValidators.validation")?e(r).triggerHandler("validation.validation",{submitting:!0}):[])}),t.length>0},override:function(t){n=e.extend(!0,n,t)}},validatorTypes:{callback:{name:"callback",init:function(e,t){var n={validatorName:t,callback:e.data("validation"+t+"Callback"),lastValue:e.val(),lastValid:!0,lastFinished:!0},r="Not valid";return e.data("validation"+t+"Message")&&(r=e.data("validation"+t+"Message")),n.message=r,n},validate:function(e,t,n){if(n.lastValue===t&&n.lastFinished)return!n.lastValid;if(n.lastFinished===!0){n.lastValue=t,n.lastValid=!0,n.lastFinished=!1;var r=n,i=e;o(n.callback,window,e,t,function(t){r.lastValue===t.value&&(r.lastValid=t.valid,t.message&&(r.message=t.message),r.lastFinished=!0,i.data("validation"+r.validatorName+"Message",r.message),setTimeout(function(){!e.is(":focus")&&e.parents("form").first().data("jqbvIsSubmitting")?i.trigger("blur.validation"):i.trigger("revalidate.validation")},1))})}return!1}},ajax:{name:"ajax",init:function(e,t){return{validatorName:t,url:e.data("validation"+t+"Ajax"),lastValue:e.val(),lastValid:!0,lastFinished:!0}},validate:function(t,n,r){return""+r.lastValue==""+n&&r.lastFinished===!0?r.lastValid===!1:(r.lastFinished===!0&&(r.lastValue=n,r.lastValid=!0,r.lastFinished=!1,e.ajax({url:r.url,data:"value="+encodeURIComponent(n)+"&field="+t.attr("name"),dataType:"json",success:function(e){""+r.lastValue==""+e.value&&(r.lastValid=!!e.valid,e.message&&(r.message=e.message),r.lastFinished=!0,t.data("validation"+r.validatorName+"Message",r.message),setTimeout(function(){t.trigger("revalidate.validation")},1))},failure:function(){r.lastValid=!0,r.message="ajax call failed",r.lastFinished=!0,t.data("validation"+r.validatorName+"Message",r.message),setTimeout(function(){t.trigger("revalidate.validation")},1)}})),!1)}},regex:{name:"regex",init:function(t,n){var r={},i=t.data("validation"+n+"Regex");r.regex=s(i),i===undefined&&e.error("Can't find regex for '"+n+"' validator on '"+t.attr("name")+"'");var o="Not in the expected format";return t.data("validation"+n+"Message")&&(o=t.data("validation"+n+"Message")),r.message=o,r.originalName=n,r},validate:function(e,t,n){return!n.regex.test(t)&&!n.negative||n.regex.test(t)&&n.negative}},email:{name:"email",init:function(e,t){var n={};n.regex=s("[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,4}");var r="Not a valid email address";return e.data("validation"+t+"Message")&&(r=e.data("validation"+t+"Message")),n.message=r,n.originalName=t,n},validate:function(e,t,n){return!n.regex.test(t)&&!n.negative||n.regex.test(t)&&n.negative}},required:{name:"required",init:function(e,t){var n="This is required";return e.data("validation"+t+"Message")&&(n=e.data("validation"+t+"Message")),{message:n,includeEmpty:!0}},validate:function(e,t,n){return!!(t.length===0&&!n.negative||t.length>0&&n.negative)},blockSubmit:!0},match:{name:"match",init:function(t,n){var r=t.data("validation"+n+"Match"),i=t.parents("form").first(),s=i.find('[name="'+r+'"]').first();s.bind("validation.validation",function(){t.trigger("revalidate.validation",{submitting:!0})});var o={};o.element=s,s.length===0&&e.error("Can't find field '"+r+"' to match '"+t.attr("name")+"' against in '"+n+"' validator");var u="Must match",a=null;return(a=i.find('label[for="'+r+'"]')).length?u+=" '"+a.text()+"'":(a=s.parents(".control-group").first().find("label")).length&&(u+=" '"+a.first().text()+"'"),t.data("validation"+n+"Message")&&(u=t.data("validation"+n+"Message")),o.message=u,o},validate:function(e,t,n){return t!==n.element.val()&&!n.negative||t===n.element.val()&&n.negative},blockSubmit:!0,includeEmpty:!0},max:{name:"max",init:function(e,t){var n={};return n.max=e.data("validation"+t+"Max"),n.message="Too high: Maximum of '"+n.max+"'",e.data("validation"+t+"Message")&&(n.message=e.data("validation"+t+"Message")),n},validate:function(e,t,n){return parseFloat(t,10)>parseFloat(n.max,10)&&!n.negative||parseFloat(t,10)<=parseFloat(n.max,10)&&n.negative}},min:{name:"min",init:function(e,t){var n={};return n.min=e.data("validation"+t+"Min"),n.message="Too low: Minimum of '"+n.min+"'",e.data("validation"+t+"Message")&&(n.message=e.data("validation"+t+"Message")),n},validate:function(e,t,n){return parseFloat(t)=parseFloat(n.min)&&n.negative}},maxlength:{name:"maxlength",init:function(e,t){var n={};return n.maxlength=e.data("validation"+t+"Maxlength"),n.message="Too long: Maximum of '"+n.maxlength+"' characters",e.data("validation"+t+"Message")&&(n.message=e.data("validation"+t+"Message")),n},validate:function(e,t,n){return t.length>n.maxlength&&!n.negative||t.length<=n.maxlength&&n.negative}},minlength:{name:"minlength",init:function(e,t){var n={};return n.minlength=e.data("validation"+t+"Minlength"),n.message="Too short: Minimum of '"+n.minlength+"' characters",e.data("validation"+t+"Message")&&(n.message=e.data("validation"+t+"Message")),n},validate:function(e,t,n){return t.length=n.minlength&&n.negative}},maxchecked:{name:"maxchecked",init:function(e,t){var n={},r=e.parents("form").first().find('[name="'+e.attr("name")+'"]');r.bind("change.validation click.validation",function(){e.trigger("revalidate.validation",{includeEmpty:!0})}),n.elements=r,n.maxchecked=e.data("validation"+t+"Maxchecked");var i="Too many: Max '"+n.maxchecked+"' checked";return e.data("validation"+t+"Message")&&(i=e.data("validation"+t+"Message")),n.message=i,n},validate:function(e,t,n){return n.elements.filter(":checked").length>n.maxchecked&&!n.negative||n.elements.filter(":checked").length<=n.maxchecked&&n.negative},blockSubmit:!0},minchecked:{name:"minchecked",init:function(e,t){var n={},r=e.parents("form").first().find('[name="'+e.attr("name")+'"]');r.bind("change.validation click.validation",function(){e.trigger("revalidate.validation",{includeEmpty:!0})}),n.elements=r,n.minchecked=e.data("validation"+t+"Minchecked");var i="Too few: Min '"+n.minchecked+"' checked";return e.data("validation"+t+"Message")&&(i=e.data("validation"+t+"Message")),n.message=i,n},validate:function(e,t,n){return n.elements.filter(":checked").length=n.minchecked&&n.negative},blockSubmit:!0,includeEmpty:!0},number:{name:"number",init:function(e,t){var n={};n.step=1,e.attr("step")&&(n.step=e.attr("step")),e.data("validation"+t+"Step")&&(n.step=e.data("validation"+t+"Step")),n.decimal=".",e.data("validation"+t+"Decimal")&&(n.decimal=e.data("validation"+t+"Decimal")),n.thousands="",e.data("validation"+t+"Thousands")&&(n.thousands=e.data("validation"+t+"Thousands")),n.regex=s("([+-]?\\d+(\\"+n.decimal+"\\d+)?)?"),n.message="Must be a number";var r=e.data("validation"+t+"Message");return r&&(n.message=r),n},validate:function(e,t,n){var r=t.replace(n.decimal,".").replace(n.thousands,""),i=parseFloat(r),s=parseFloat(n.step);while(s%1!==0)s=parseFloat(s.toPrecision(12))*10,i=parseFloat(i.toPrecision(12))*10;var o=n.regex.test(t),u=parseFloat(i)%parseFloat(s)===0,a=!isNaN(parseFloat(r))&&isFinite(r),f=!(o&&u&&a);return f},message:"Must be a number"}},builtInValidators:{email:{name:"Email",type:"email"},passwordagain:{name:"Passwordagain",type:"match",match:"password",message:"Does not match the given password"},positive:{name:"Positive",type:"shortcut",shortcut:"number,positivenumber"},negative:{name:"Negative",type:"shortcut",shortcut:"number,negativenumber"},integer:{name:"Integer",type:"regex",regex:"[+-]?\\d+",message:"No decimal places allowed"},positivenumber:{name:"Positivenumber",type:"min",min:0,message:"Must be a positive number"},negativenumber:{name:"Negativenumber",type:"max",max:0,message:"Must be a negative number"},required:{name:"Required",type:"required",message:"This is required"},checkone:{name:"Checkone",type:"minchecked",minchecked:1,message:"Check at least one option"},number:{name:"Number",type:"number",decimal:".",step:"1"},pattern:{name:"Pattern",type:"regex",message:"Not in expected format"}}},r=function(e){return e.toLowerCase().replace(/(^|\s)([a-z])/g,function(e,t,n){return t+n.toUpperCase()})},i=function(t){var n=null,r=t.attr("type");if(r==="checkbox"){n=t.is(":checked")?n:"";var i=t.parents("form").first()||t.parents(".control-group").first();i&&(n=i.find("input[name='"+t.attr("name")+"']:checked").map(function(t,n){return e(n).val()}).toArray().join(","))}else if(r==="radio"){n=e('input[name="'+t.attr("name")+'"]:checked').length>0?t.val():"";var s=t.parents("form").first()||t.parents(".control-group").first();s&&(n=s.find("input[name='"+t.attr("name")+"']:checked").map(function(t,n){return e(n).val()}).toArray().join(","))}else n=t.val();return n};e.fn.jqBootstrapValidation=function(t){return n.methods[t]?n.methods[t].apply(this,Array.prototype.slice.call(arguments,1)):typeof t=="object"||!t?n.methods.init.apply(this,arguments):(e.error("Method "+t+" does not exist on jQuery.jqBootstrapValidation"),null)},e.jqBootstrapValidation=function(t){e(":input").not("[type=image],[type=submit]").jqBootstrapValidation.apply(this,arguments)}})(jQuery); \ No newline at end of file diff --git a/atmo/static/lib/marked.min.js b/atmo/static/lib/marked.min.js new file mode 100644 index 00000000..4d2eb081 --- /dev/null +++ b/atmo/static/lib/marked.min.js @@ -0,0 +1,6 @@ +/** + * marked - a markdown parser + * Copyright (c) 2011-2014, Christopher Jeffrey. (MIT Licensed) + * https://github.com/chjj/marked + */ +(function(){var block={newline:/^\n+/,code:/^( {4}[^\n]+\n*)+/,fences:noop,hr:/^( *[-*_]){3,} *(?:\n+|$)/,heading:/^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/,nptable:noop,lheading:/^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/,blockquote:/^( *>[^\n]+(\n(?!def)[^\n]+)*\n*)+/,list:/^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/,html:/^ *(?:comment *(?:\n|\s*$)|closed *(?:\n{2,}|\s*$)|closing *(?:\n{2,}|\s*$))/,def:/^ *\[([^\]]+)\]: *]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/,table:noop,paragraph:/^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+)\n*/,text:/^[^\n]+/};block.bullet=/(?:[*+-]|\d+\.)/;block.item=/^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/;block.item=replace(block.item,"gm")(/bull/g,block.bullet)();block.list=replace(block.list)(/bull/g,block.bullet)("hr","\\n+(?=\\1?(?:[-*_] *){3,}(?:\\n+|$))")("def","\\n+(?="+block.def.source+")")();block.blockquote=replace(block.blockquote)("def",block.def)();block._tag="(?!(?:"+"a|em|strong|small|s|cite|q|dfn|abbr|data|time|code"+"|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo"+"|span|br|wbr|ins|del|img)\\b)\\w+(?!:/|[^\\w\\s@]*@)\\b";block.html=replace(block.html)("comment",//)("closed",/<(tag)[\s\S]+?<\/\1>/)("closing",/])*?>/)(/tag/g,block._tag)();block.paragraph=replace(block.paragraph)("hr",block.hr)("heading",block.heading)("lheading",block.lheading)("blockquote",block.blockquote)("tag","<"+block._tag)("def",block.def)();block.normal=merge({},block);block.gfm=merge({},block.normal,{fences:/^ *(`{3,}|~{3,})[ \.]*(\S+)? *\n([\s\S]*?)\s*\1 *(?:\n+|$)/,paragraph:/^/,heading:/^ *(#{1,6}) +([^\n]+?) *#* *(?:\n+|$)/});block.gfm.paragraph=replace(block.paragraph)("(?!","(?!"+block.gfm.fences.source.replace("\\1","\\2")+"|"+block.list.source.replace("\\1","\\3")+"|")();block.tables=merge({},block.gfm,{nptable:/^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*/,table:/^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*/});function Lexer(options){this.tokens=[];this.tokens.links={};this.options=options||marked.defaults;this.rules=block.normal;if(this.options.gfm){if(this.options.tables){this.rules=block.tables}else{this.rules=block.gfm}}}Lexer.rules=block;Lexer.lex=function(src,options){var lexer=new Lexer(options);return lexer.lex(src)};Lexer.prototype.lex=function(src){src=src.replace(/\r\n|\r/g,"\n").replace(/\t/g," ").replace(/\u00a0/g," ").replace(/\u2424/g,"\n");return this.token(src,true)};Lexer.prototype.token=function(src,top,bq){var src=src.replace(/^ +$/gm,""),next,loose,cap,bull,b,item,space,i,l;while(src){if(cap=this.rules.newline.exec(src)){src=src.substring(cap[0].length);if(cap[0].length>1){this.tokens.push({type:"space"})}}if(cap=this.rules.code.exec(src)){src=src.substring(cap[0].length);cap=cap[0].replace(/^ {4}/gm,"");this.tokens.push({type:"code",text:!this.options.pedantic?cap.replace(/\n+$/,""):cap});continue}if(cap=this.rules.fences.exec(src)){src=src.substring(cap[0].length);this.tokens.push({type:"code",lang:cap[2],text:cap[3]||""});continue}if(cap=this.rules.heading.exec(src)){src=src.substring(cap[0].length);this.tokens.push({type:"heading",depth:cap[1].length,text:cap[2]});continue}if(top&&(cap=this.rules.nptable.exec(src))){src=src.substring(cap[0].length);item={type:"table",header:cap[1].replace(/^ *| *\| *$/g,"").split(/ *\| */),align:cap[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:cap[3].replace(/\n$/,"").split("\n")};for(i=0;i ?/gm,"");this.token(cap,top,true);this.tokens.push({type:"blockquote_end"});continue}if(cap=this.rules.list.exec(src)){src=src.substring(cap[0].length);bull=cap[2];this.tokens.push({type:"list_start",ordered:bull.length>1});cap=cap[0].match(this.rules.item);next=false;l=cap.length;i=0;for(;i1&&b.length>1)){src=cap.slice(i+1).join("\n")+src;i=l-1}}loose=next||/\n\n(?!\s*$)/.test(item);if(i!==l-1){next=item.charAt(item.length-1)==="\n";if(!loose)loose=next}this.tokens.push({type:loose?"loose_item_start":"list_item_start"});this.token(item,false,bq);this.tokens.push({type:"list_item_end"})}this.tokens.push({type:"list_end"});continue}if(cap=this.rules.html.exec(src)){src=src.substring(cap[0].length);this.tokens.push({type:this.options.sanitize?"paragraph":"html",pre:!this.options.sanitizer&&(cap[1]==="pre"||cap[1]==="script"||cap[1]==="style"),text:cap[0]});continue}if(!bq&&top&&(cap=this.rules.def.exec(src))){src=src.substring(cap[0].length);this.tokens.links[cap[1].toLowerCase()]={href:cap[2],title:cap[3]};continue}if(top&&(cap=this.rules.table.exec(src))){src=src.substring(cap[0].length);item={type:"table",header:cap[1].replace(/^ *| *\| *$/g,"").split(/ *\| */),align:cap[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:cap[3].replace(/(?: *\| *)?\n$/,"").split("\n")};for(i=0;i])/,autolink:/^<([^ >]+(@|:\/)[^ >]+)>/,url:noop,tag:/^|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/,link:/^!?\[(inside)\]\(href\)/,reflink:/^!?\[(inside)\]\s*\[([^\]]*)\]/,nolink:/^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/,strong:/^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/,em:/^\b_((?:[^_]|__)+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/,code:/^(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/,br:/^ {2,}\n(?!\s*$)/,del:noop,text:/^[\s\S]+?(?=[\\?(?:\s+['"]([\s\S]*?)['"])?\s*/;inline.link=replace(inline.link)("inside",inline._inside)("href",inline._href)();inline.reflink=replace(inline.reflink)("inside",inline._inside)();inline.normal=merge({},inline);inline.pedantic=merge({},inline.normal,{strong:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,em:/^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/});inline.gfm=merge({},inline.normal,{escape:replace(inline.escape)("])","~|])")(),url:/^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/,del:/^~~(?=\S)([\s\S]*?\S)~~/,text:replace(inline.text)("]|","~]|")("|","|https?://|")()});inline.breaks=merge({},inline.gfm,{br:replace(inline.br)("{2,}","*")(),text:replace(inline.gfm.text)("{2,}","*")()});function InlineLexer(links,options){this.options=options||marked.defaults;this.links=links;this.rules=inline.normal;this.renderer=this.options.renderer||new Renderer;this.renderer.options=this.options;if(!this.links){throw new Error("Tokens array requires a `links` property.")}if(this.options.gfm){if(this.options.breaks){this.rules=inline.breaks}else{this.rules=inline.gfm}}else if(this.options.pedantic){this.rules=inline.pedantic}}InlineLexer.rules=inline;InlineLexer.output=function(src,links,options){var inline=new InlineLexer(links,options);return inline.output(src)};InlineLexer.prototype.output=function(src){var out="",link,text,href,cap;while(src){if(cap=this.rules.escape.exec(src)){src=src.substring(cap[0].length);out+=cap[1];continue}if(cap=this.rules.autolink.exec(src)){src=src.substring(cap[0].length);if(cap[2]==="@"){text=cap[1].charAt(6)===":"?this.mangle(cap[1].substring(7)):this.mangle(cap[1]);href=this.mangle("mailto:")+text}else{text=escape(cap[1]);href=text}out+=this.renderer.link(href,null,text);continue}if(!this.inLink&&(cap=this.rules.url.exec(src))){src=src.substring(cap[0].length);text=escape(cap[1]);href=text;out+=this.renderer.link(href,null,text);continue}if(cap=this.rules.tag.exec(src)){if(!this.inLink&&/^/i.test(cap[0])){this.inLink=false}src=src.substring(cap[0].length);out+=this.options.sanitize?this.options.sanitizer?this.options.sanitizer(cap[0]):escape(cap[0]):cap[0];continue}if(cap=this.rules.link.exec(src)){src=src.substring(cap[0].length);this.inLink=true;out+=this.outputLink(cap,{href:cap[2],title:cap[3]});this.inLink=false;continue}if((cap=this.rules.reflink.exec(src))||(cap=this.rules.nolink.exec(src))){src=src.substring(cap[0].length);link=(cap[2]||cap[1]).replace(/\s+/g," ");link=this.links[link.toLowerCase()];if(!link||!link.href){out+=cap[0].charAt(0);src=cap[0].substring(1)+src;continue}this.inLink=true;out+=this.outputLink(cap,link);this.inLink=false;continue}if(cap=this.rules.strong.exec(src)){src=src.substring(cap[0].length);out+=this.renderer.strong(this.output(cap[2]||cap[1]));continue}if(cap=this.rules.em.exec(src)){src=src.substring(cap[0].length);out+=this.renderer.em(this.output(cap[2]||cap[1]));continue}if(cap=this.rules.code.exec(src)){src=src.substring(cap[0].length);out+=this.renderer.codespan(escape(cap[2],true));continue}if(cap=this.rules.br.exec(src)){src=src.substring(cap[0].length);out+=this.renderer.br();continue}if(cap=this.rules.del.exec(src)){src=src.substring(cap[0].length);out+=this.renderer.del(this.output(cap[1]));continue}if(cap=this.rules.text.exec(src)){src=src.substring(cap[0].length);out+=this.renderer.text(escape(this.smartypants(cap[0])));continue}if(src){throw new Error("Infinite loop on byte: "+src.charCodeAt(0))}}return out};InlineLexer.prototype.outputLink=function(cap,link){var href=escape(link.href),title=link.title?escape(link.title):null;return cap[0].charAt(0)!=="!"?this.renderer.link(href,title,this.output(cap[1])):this.renderer.image(href,title,escape(cap[1]))};InlineLexer.prototype.smartypants=function(text){if(!this.options.smartypants)return text;return text.replace(/---/g,"—").replace(/--/g,"–").replace(/(^|[-\u2014/(\[{"\s])'/g,"$1‘").replace(/'/g,"’").replace(/(^|[-\u2014/(\[{\u2018\s])"/g,"$1“").replace(/"/g,"”").replace(/\.{3}/g,"…")};InlineLexer.prototype.mangle=function(text){if(!this.options.mangle)return text;var out="",l=text.length,i=0,ch;for(;i.5){ch="x"+ch.toString(16)}out+="&#"+ch+";"}return out};function Renderer(options){this.options=options||{}}Renderer.prototype.code=function(code,lang,escaped){if(this.options.highlight){var out=this.options.highlight(code,lang);if(out!=null&&out!==code){escaped=true;code=out}}if(!lang){return"
"+(escaped?code:escape(code,true))+"\n
"}return'
'+(escaped?code:escape(code,true))+"\n
\n"};Renderer.prototype.blockquote=function(quote){return"
\n"+quote+"
\n"};Renderer.prototype.html=function(html){return html};Renderer.prototype.heading=function(text,level,raw){return"'+text+"\n"};Renderer.prototype.hr=function(){return this.options.xhtml?"
\n":"
\n"};Renderer.prototype.list=function(body,ordered){var type=ordered?"ol":"ul";return"<"+type+">\n"+body+"\n"};Renderer.prototype.listitem=function(text){return"
  • "+text+"
  • \n"};Renderer.prototype.paragraph=function(text){return"

    "+text+"

    \n"};Renderer.prototype.table=function(header,body){return"\n"+"\n"+header+"\n"+"\n"+body+"\n"+"
    \n"};Renderer.prototype.tablerow=function(content){return"\n"+content+"\n"};Renderer.prototype.tablecell=function(content,flags){var type=flags.header?"th":"td";var tag=flags.align?"<"+type+' style="text-align:'+flags.align+'">':"<"+type+">";return tag+content+"\n"};Renderer.prototype.strong=function(text){return""+text+""};Renderer.prototype.em=function(text){return""+text+""};Renderer.prototype.codespan=function(text){return""+text+""};Renderer.prototype.br=function(){return this.options.xhtml?"
    ":"
    "};Renderer.prototype.del=function(text){return""+text+""};Renderer.prototype.link=function(href,title,text){if(this.options.sanitize){try{var prot=decodeURIComponent(unescape(href)).replace(/[^\w:]/g,"").toLowerCase()}catch(e){return""}if(prot.indexOf("javascript:")===0||prot.indexOf("vbscript:")===0){return""}}var out='
    ";return out};Renderer.prototype.image=function(href,title,text){var out=''+text+'":">";return out};Renderer.prototype.text=function(text){return text};function Parser(options){this.tokens=[];this.token=null;this.options=options||marked.defaults;this.options.renderer=this.options.renderer||new Renderer;this.renderer=this.options.renderer;this.renderer.options=this.options}Parser.parse=function(src,options,renderer){var parser=new Parser(options,renderer);return parser.parse(src)};Parser.prototype.parse=function(src){this.inline=new InlineLexer(src.links,this.options,this.renderer);this.tokens=src.reverse();var out="";while(this.next()){out+=this.tok()}return out};Parser.prototype.next=function(){return this.token=this.tokens.pop()};Parser.prototype.peek=function(){return this.tokens[this.tokens.length-1]||0};Parser.prototype.parseText=function(){var body=this.token.text;while(this.peek().type==="text"){body+="\n"+this.next().text}return this.inline.output(body)};Parser.prototype.tok=function(){switch(this.token.type){case"space":{return""}case"hr":{return this.renderer.hr()}case"heading":{return this.renderer.heading(this.inline.output(this.token.text),this.token.depth,this.token.text)}case"code":{return this.renderer.code(this.token.text,this.token.lang,this.token.escaped)}case"table":{var header="",body="",i,row,cell,flags,j;cell="";for(i=0;i/g,">").replace(/"/g,""").replace(/'/g,"'")}function unescape(html){return html.replace(/&([#\w]+);/g,function(_,n){n=n.toLowerCase();if(n==="colon")return":";if(n.charAt(0)==="#"){return n.charAt(1)==="x"?String.fromCharCode(parseInt(n.substring(2),16)):String.fromCharCode(+n.substring(1))}return""})}function replace(regex,opt){regex=regex.source;opt=opt||"";return function self(name,val){if(!name)return new RegExp(regex,opt);val=val.source||val;val=val.replace(/(^|[^\[])\^/g,"$1");regex=regex.replace(name,val);return self}}function noop(){}noop.exec=noop;function merge(obj){var i=1,target,key;for(;iAn error occured:

    "+escape(e.message+"",true)+"
    "}throw e}}marked.options=marked.setOptions=function(opt){merge(marked.defaults,opt);return marked};marked.defaults={gfm:true,tables:true,breaks:false,pedantic:false,sanitize:false,sanitizer:null,mangle:true,smartLists:false,silent:false,highlight:null,langPrefix:"lang-",smartypants:false,headerPrefix:"",renderer:new Renderer,xhtml:false};marked.Parser=Parser;marked.parser=Parser.parse;marked.Renderer=Renderer;marked.Lexer=Lexer;marked.lexer=Lexer.lex;marked.InlineLexer=InlineLexer;marked.inlineLexer=InlineLexer.output;marked.parse=marked;if(typeof module!=="undefined"&&typeof exports==="object"){module.exports=marked}else if(typeof define==="function"&&define.amd){define(function(){return marked})}else{this.marked=marked}}).call(function(){return this||(typeof window!=="undefined"?window:global)}()); diff --git a/atmo/static/lib/notebook.css b/atmo/static/lib/notebook.css new file mode 100644 index 00000000..02f6b460 --- /dev/null +++ b/atmo/static/lib/notebook.css @@ -0,0 +1,80 @@ +/*Original from https://github.com/jsvine/nbpreview/blob/9da3f2dad5fa3cc1d38702fdc62df80c75baf664/css/vendor/notebook.css +Copyright (c) 2015, Jeremy Singer-Vine*/ +.nb-notebook { + line-height: 1.5; +} + +.nb-stdout, .nb-stderr { + white-space: pre-wrap; + margin: 1em 0; + padding: 0.1em 0.5em; +} + +.nb-stderr { + background-color: #FAA; +} + +.nb-cell + .nb-cell { + margin-top: 0.5em; +} + +.nb-output table { + border: 1px solid #000; + border-collapse: collapse; +} + +.nb-output th { + font-weight: bold; +} + +.nb-output th, .nb-output td { + border: 1px solid #000; + padding: 0.25em; + text-align: left; + vertical-align: middle; + border-collapse: collapse; +} + +.nb-cell { + position: relative; +} + +.nb-raw-cell { + white-space: pre-wrap; + background-color: #f5f2f0; + font-family: Consolas, Monaco, 'Andale Mono', monospace; + padding: 1em; + margin: .5em 0; +} + +.nb-output { + min-height: 1em; + width: 100%; + overflow-x: scroll; + border-right: 1px dotted #CCC; +} + +.nb-output img { + max-width: 100%; +} + +.nb-output:before, .nb-input:before { + position: absolute; + font-family: monospace; + color: #999; + left: -7.5em; + width: 7em; + text-align: right; +} + +.nb-input:before { + content: "In [" attr(data-prompt-number) "]:"; +} +.nb-output:before { + content: "Out [" attr(data-prompt-number) "]:"; +} + +// Fix pandas dataframe formatting +div[style="max-height:1000px;max-width:1500px;overflow:auto;"] { + max-height: none !important; +} diff --git a/atmo/static/lib/notebook.min.js b/atmo/static/lib/notebook.min.js new file mode 100644 index 00000000..0f38f2f7 --- /dev/null +++ b/atmo/static/lib/notebook.min.js @@ -0,0 +1 @@ +(function(){var root=this;var VERSION="0.2.5";var doc=root.document||require("jsdom").jsdom();var ident=function(x){return x};var makeElement=function(tag,classNames){var el=doc.createElement(tag);el.className=(classNames||[]).map(function(cn){return nb.prefix+cn}).join(" ");return el};var escapeHTML=function(raw){var replaced=raw.replace(//g,">");return replaced};var joinText=function(text){if(text.join){return text.map(joinText).join("")}else{return text}};var condRequire=function(module_name){return typeof require==="function"&&require(module_name)};var getMarkdown=function(){return root.marked||condRequire("marked")};var getAnsi=function(){var req=condRequire("ansi_up");var lib=root.ansi_up||req;return lib&&lib.ansi_to_html};var nb={prefix:"nb-",markdown:getMarkdown()||ident,ansi:getAnsi()||ident,VERSION:VERSION};nb.Input=function(raw,cell){this.raw=raw;this.cell=cell};nb.Input.prototype.render=function(){if(!this.raw.length){return makeElement("div")}var holder=makeElement("div",["input"]);var cell=this.cell;if(typeof cell.number==="number"){holder.setAttribute("data-prompt-number",this.cell.number)}var pre_el=makeElement("pre");var code_el=makeElement("code");var notebook=cell.worksheet.notebook;var m=notebook.metadata;var lang=this.cell.raw.language||m.language||m.language_info.name;code_el.setAttribute("data-language",lang);code_el.className="lang-"+lang;code_el.innerHTML=escapeHTML(joinText(this.raw));pre_el.appendChild(code_el);holder.appendChild(pre_el);this.el=holder;return holder};var imageCreator=function(format){return function(data){var el=makeElement("img",["image-output"]);el.src="data:image/"+format+";base64,"+joinText(data).replace(/\n/g,"");return el}};nb.display={};nb.display.text=function(text){var el=makeElement("pre",["text-output"]);el.innerHTML=escapeHTML(joinText(text));return el};nb.display["text/plain"]=nb.display.text;nb.display.html=function(html){var el=makeElement("div",["html-output"]);el.innerHTML=joinText(html);return el};nb.display["text/html"]=nb.display.html;nb.display.marked=function(md){return nb.display.html(nb.markdown(joinText(md)))};nb.display["text/markdown"]=nb.display.marked;nb.display.svg=function(svg){var el=makeElement("div",["svg-output"]);el.innerHTML=joinText(svg);return el};nb.display["text/svg+xml"]=nb.display.svg;nb.display.latex=function(latex){var el=makeElement("div",["latex-output"]);el.innerHTML=join(latex);return el};nb.display["text/latex"]=nb.display.latex;nb.display.javascript=function(js){var el=makeElement("script");script.innerHTML=js;return el};nb.display["application/javascript"]=nb.display.javascript;nb.display.png=imageCreator("png");nb.display["image/png"]=nb.display.png;nb.display.jpeg=imageCreator("jpeg");nb.display["image/jpeg"]=nb.display.jpeg;nb.display_priority=["png","image/png","jpeg","image/jpeg","svg","text/svg+xml","html","text/html","text/markdown","latex","text/latex","javascript","application/javascript","text","text/plain"];var render_display_data=function(){var o=this;var formats=nb.display_priority.filter(function(d){return o.raw.data?o.raw.data[d]:o.raw[d]});var format=formats[0];if(format){if(nb.display[format]){return nb.display[format](o.raw[format]||o.raw.data[format])}}return makeElement("div",["empty-output"])};var render_error=function(){var el=makeElement("pre",["pyerr"]);var raw=this.raw.traceback.join("\n");el.innerHTML=nb.ansi(escapeHTML(raw));return el};nb.Output=function(raw,cell){this.raw=raw;this.cell=cell;this.type=raw.output_type};nb.Output.prototype.renderers={display_data:render_display_data,execute_result:render_display_data,pyout:render_display_data,pyerr:render_error,error:render_error,stream:function(){var el=makeElement("pre",[this.raw.stream||this.raw.name]);var raw=joinText(this.raw.text);el.innerHTML=nb.ansi(escapeHTML(raw));return el}};nb.Output.prototype.render=function(){var outer=makeElement("div",["output"]);if(typeof this.cell.number==="number"){outer.setAttribute("data-prompt-number",this.cell.number)}var inner=this.renderers[this.type].call(this);outer.appendChild(inner);this.el=outer;return outer};nb.coalesceStreams=function(outputs){if(!outputs.length){return outputs}var last=outputs[0];var new_outputs=[last];outputs.slice(1).forEach(function(o){if(o.raw.output_type==="stream"&&last.raw.output_type==="stream"&&o.raw.stream===last.raw.stream){last.raw.text=last.raw.text.concat(o.raw.text)}else{new_outputs.push(o);last=o}});return new_outputs};nb.Cell=function(raw,worksheet){var cell=this;cell.raw=raw;cell.worksheet=worksheet;cell.type=raw.cell_type;if(cell.type==="code"){cell.number=raw.prompt_number>-1?raw.prompt_number:raw.execution_count;var source=raw.input||[raw.source];cell.input=new nb.Input(source,cell);var raw_outputs=(cell.raw.outputs||[]).map(function(o){return new nb.Output(o,cell)});cell.outputs=nb.coalesceStreams(raw_outputs)}};nb.Cell.prototype.renderers={markdown:function(){var el=makeElement("div",["cell","markdown-cell"]);el.innerHTML=nb.markdown(joinText(this.raw.source));return el},heading:function(){var el=makeElement("h"+this.raw.level,["cell","heading-cell"]);el.innerHTML=joinText(this.raw.source);return el},raw:function(){var el=makeElement("div",["cell","raw-cell"]);el.innerHTML=joinText(this.raw.source);return el},code:function(){var cell_el=makeElement("div",["cell","code-cell"]);cell_el.appendChild(this.input.render());var output_els=this.outputs.forEach(function(o){cell_el.appendChild(o.render())});return cell_el}};nb.Cell.prototype.render=function(){var el=this.renderers[this.type].call(this);this.el=el;return el};nb.Worksheet=function(raw,notebook){var worksheet=this;this.raw=raw;this.notebook=notebook;this.cells=raw.cells.map(function(c){return new nb.Cell(c,worksheet)});this.render=function(){var worksheet_el=makeElement("div",["worksheet"]);worksheet.cells.forEach(function(c){worksheet_el.appendChild(c.render())});this.el=worksheet_el;return worksheet_el}};nb.Notebook=function(raw,config){var notebook=this;this.raw=raw;this.config=config;var meta=this.metadata=raw.metadata;this.title=meta.title||meta.name;var _worksheets=raw.worksheets||[{cells:raw.cells}];this.worksheets=_worksheets.map(function(ws){return new nb.Worksheet(ws,notebook)});this.sheet=this.worksheets[0]};nb.Notebook.prototype.render=function(){var notebook_el=makeElement("div",["notebook"]);this.worksheets.forEach(function(w){notebook_el.appendChild(w.render())});this.el=notebook_el;return notebook_el};nb.parse=function(nbjson,config){return new nb.Notebook(nbjson,config)};if(typeof define==="function"&&define.amd){define(function(){return nb})}if(typeof exports!=="undefined"){if(typeof module!=="undefined"&&module.exports){exports=module.exports=nb}exports.nb=nb}else{root.nb=nb}}).call(this); diff --git a/atmo/static/lib/prism.css b/atmo/static/lib/prism.css new file mode 100644 index 00000000..fa49e6df --- /dev/null +++ b/atmo/static/lib/prism.css @@ -0,0 +1,138 @@ +/* http://prismjs.com/download.html?themes=prism&languages=clike+javascript+ruby+go+java+python+scala */ +/** + * prism.js default theme for JavaScript, CSS and HTML + * Based on dabblet (http://dabblet.com) + * @author Lea Verou + */ + +code[class*="language-"], +pre[class*="language-"] { + color: black; + background: none; + text-shadow: 0 1px white; + font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + word-wrap: normal; + line-height: 1.5; + + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; + + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; +} + +pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection, +code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection { + text-shadow: none; + background: #b3d4fc; +} + +pre[class*="language-"]::selection, pre[class*="language-"] ::selection, +code[class*="language-"]::selection, code[class*="language-"] ::selection { + text-shadow: none; + background: #b3d4fc; +} + +@media print { + code[class*="language-"], + pre[class*="language-"] { + text-shadow: none; + } +} + +/* Code blocks */ +pre[class*="language-"] { + padding: 1em; + margin: .5em 0; + overflow: auto; +} + +:not(pre) > code[class*="language-"], +pre[class*="language-"] { + background: #f5f2f0; +} + +/* Inline code */ +:not(pre) > code[class*="language-"] { + padding: .1em; + border-radius: .3em; + white-space: normal; +} + +.token.comment, +.token.prolog, +.token.doctype, +.token.cdata { + color: slategray; +} + +.token.punctuation { + color: #999; +} + +.namespace { + opacity: .7; +} + +.token.property, +.token.tag, +.token.boolean, +.token.number, +.token.constant, +.token.symbol, +.token.deleted { + color: #905; +} + +.token.selector, +.token.attr-name, +.token.string, +.token.char, +.token.builtin, +.token.inserted { + color: #690; +} + +.token.operator, +.token.entity, +.token.url, +.language-css .token.string, +.style .token.string { + color: #a67f59; + background: hsla(0, 0%, 100%, .5); +} + +.token.atrule, +.token.attr-value, +.token.keyword { + color: #07a; +} + +.token.function { + color: #DD4A68; +} + +.token.regex, +.token.important, +.token.variable { + color: #e90; +} + +.token.important, +.token.bold { + font-weight: bold; +} +.token.italic { + font-style: italic; +} + +.token.entity { + cursor: help; +} diff --git a/atmo/static/lib/prism.min.js b/atmo/static/lib/prism.min.js new file mode 100644 index 00000000..f696b0a6 --- /dev/null +++ b/atmo/static/lib/prism.min.js @@ -0,0 +1,9 @@ +/* http://prismjs.com/download.html?themes=prism-okaidia&languages=clike+javascript+ruby+go+java+python+scala */ +var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(){var e=/\blang(?:uage)?-(\w+)\b/i,t=0,n=_self.Prism={util:{encode:function(e){return e instanceof a?new a(e.type,n.util.encode(e.content),e.alias):"Array"===n.util.type(e)?e.map(n.util.encode):e.replace(/&/g,"&").replace(/e.length)break e;if(!(v instanceof a)){u.lastIndex=0;var b=u.exec(v),k=1;if(!b&&h&&m!=r.length-1){if(u.lastIndex=y,b=u.exec(e),!b)break;for(var w=b.index+(g?b[1].length:0),_=b.index+b[0].length,A=m,S=y,P=r.length;P>A&&_>S;++A)S+=(r[A].matchedStr||r[A]).length,w>=S&&(++m,y=S);if(r[m]instanceof a||r[A-1].greedy)continue;k=A-m,v=e.slice(y,S),b.index-=y}if(b){g&&(f=b[1].length);var w=b.index+f,b=b[0].slice(f),_=w+b.length,x=v.slice(0,w),O=v.slice(_),j=[m,k];x&&j.push(x);var N=new a(l,c?n.tokenize(b,c):b,d,b,h);j.push(N),O&&j.push(O),Array.prototype.splice.apply(r,j)}}}}}return r},hooks:{all:{},add:function(e,t){var a=n.hooks.all;a[e]=a[e]||[],a[e].push(t)},run:function(e,t){var a=n.hooks.all[e];if(a&&a.length)for(var r,i=0;r=a[i++];)r(t)}}},a=n.Token=function(e,t,n,a,r){this.type=e,this.content=t,this.alias=n,this.matchedStr=a||null,this.greedy=!!r};if(a.stringify=function(e,t,r){if("string"==typeof e)return e;if("Array"===n.util.type(e))return e.map(function(n){return a.stringify(n,t,e)}).join("");var i={type:e.type,content:a.stringify(e.content,t,r),tag:"span",classes:["token",e.type],attributes:{},language:t,parent:r};if("comment"==i.type&&(i.attributes.spellcheck="true"),e.alias){var l="Array"===n.util.type(e.alias)?e.alias:[e.alias];Array.prototype.push.apply(i.classes,l)}n.hooks.run("wrap",i);var o="";for(var s in i.attributes)o+=(o?" ":"")+s+'="'+(i.attributes[s]||"")+'"';return"<"+i.tag+' class="'+i.classes.join(" ")+'"'+(o?" "+o:"")+">"+i.content+""},!_self.document)return _self.addEventListener?(_self.addEventListener("message",function(e){var t=JSON.parse(e.data),a=t.language,r=t.code,i=t.immediateClose;_self.postMessage(n.highlight(r,n.languages[a],a)),i&&_self.close()},!1),_self.Prism):_self.Prism;var r=document.currentScript||[].slice.call(document.getElementsByTagName("script")).pop();return r&&(n.filename=r.src,document.addEventListener&&!r.hasAttribute("data-manual")&&("loading"!==document.readyState?window.requestAnimationFrame?window.requestAnimationFrame(n.highlightAll):window.setTimeout(n.highlightAll,16):document.addEventListener("DOMContentLoaded",n.highlightAll))),_self.Prism}();"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism); +Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\w\W]*?\*\//,lookbehind:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0}],string:{pattern:/(["'])(\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/((?:\b(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[a-z0-9_\.\\]+/i,lookbehind:!0,inside:{punctuation:/(\.|\\)/}},keyword:/\b(if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,"boolean":/\b(true|false)\b/,"function":/[a-z0-9_]+(?=\()/i,number:/\b-?(?:0x[\da-f]+|\d*\.?\d+(?:e[+-]?\d+)?)\b/i,operator:/--?|\+\+?|!=?=?|<=?|>=?|==?=?|&&?|\|\|?|\?|\*|\/|~|\^|%/,punctuation:/[{}[\];(),.:]/}; +Prism.languages.javascript=Prism.languages.extend("clike",{keyword:/\b(as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|var|void|while|with|yield)\b/,number:/\b-?(0x[\dA-Fa-f]+|0b[01]+|0o[0-7]+|\d*\.?\d+([Ee][+-]?\d+)?|NaN|Infinity)\b/,"function":/[_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*(?=\()/i,operator:/--?|\+\+?|!=?=?|<=?|>=?|==?=?|&&?|\|\|?|\?|\*\*?|\/|~|\^|%|\.{3}/}),Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:/(^|[^\/])\/(?!\/)(\[.+?]|\\.|[^\/\\\r\n])+\/[gimyu]{0,5}(?=\s*($|[\r\n,.;})]))/,lookbehind:!0,greedy:!0}}),Prism.languages.insertBefore("javascript","string",{"template-string":{pattern:/`(?:\\\\|\\?[^\\])*?`/,greedy:!0,inside:{interpolation:{pattern:/\$\{[^}]+\}/,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:Prism.languages.javascript}},string:/[\s\S]+/}}}),Prism.languages.markup&&Prism.languages.insertBefore("markup","tag",{script:{pattern:/()[\w\W]*?(?=<\/script>)/i,lookbehind:!0,inside:Prism.languages.javascript,alias:"language-javascript"}}),Prism.languages.js=Prism.languages.javascript; +!function(e){e.languages.ruby=e.languages.extend("clike",{comment:/#(?!\{[^\r\n]*?\}).*/,keyword:/\b(alias|and|BEGIN|begin|break|case|class|def|define_method|defined|do|each|else|elsif|END|end|ensure|false|for|if|in|module|new|next|nil|not|or|raise|redo|require|rescue|retry|return|self|super|then|throw|true|undef|unless|until|when|while|yield)\b/});var n={pattern:/#\{[^}]+\}/,inside:{delimiter:{pattern:/^#\{|\}$/,alias:"tag"},rest:e.util.clone(e.languages.ruby)}};e.languages.insertBefore("ruby","keyword",{regex:[{pattern:/%r([^a-zA-Z0-9\s\{\(\[<])(?:[^\\]|\\[\s\S])*?\1[gim]{0,3}/,inside:{interpolation:n}},{pattern:/%r\((?:[^()\\]|\\[\s\S])*\)[gim]{0,3}/,inside:{interpolation:n}},{pattern:/%r\{(?:[^#{}\\]|#(?:\{[^}]+\})?|\\[\s\S])*\}[gim]{0,3}/,inside:{interpolation:n}},{pattern:/%r\[(?:[^\[\]\\]|\\[\s\S])*\][gim]{0,3}/,inside:{interpolation:n}},{pattern:/%r<(?:[^<>\\]|\\[\s\S])*>[gim]{0,3}/,inside:{interpolation:n}},{pattern:/(^|[^\/])\/(?!\/)(\[.+?]|\\.|[^\/\r\n])+\/[gim]{0,3}(?=\s*($|[\r\n,.;})]))/,lookbehind:!0}],variable:/[@$]+[a-zA-Z_][a-zA-Z_0-9]*(?:[?!]|\b)/,symbol:/:[a-zA-Z_][a-zA-Z_0-9]*(?:[?!]|\b)/}),e.languages.insertBefore("ruby","number",{builtin:/\b(Array|Bignum|Binding|Class|Continuation|Dir|Exception|FalseClass|File|Stat|File|Fixnum|Float|Hash|Integer|IO|MatchData|Method|Module|NilClass|Numeric|Object|Proc|Range|Regexp|String|Struct|TMS|Symbol|ThreadGroup|Thread|Time|TrueClass)\b/,constant:/\b[A-Z][a-zA-Z_0-9]*(?:[?!]|\b)/}),e.languages.ruby.string=[{pattern:/%[qQiIwWxs]?([^a-zA-Z0-9\s\{\(\[<])(?:[^\\]|\\[\s\S])*?\1/,inside:{interpolation:n}},{pattern:/%[qQiIwWxs]?\((?:[^()\\]|\\[\s\S])*\)/,inside:{interpolation:n}},{pattern:/%[qQiIwWxs]?\{(?:[^#{}\\]|#(?:\{[^}]+\})?|\\[\s\S])*\}/,inside:{interpolation:n}},{pattern:/%[qQiIwWxs]?\[(?:[^\[\]\\]|\\[\s\S])*\]/,inside:{interpolation:n}},{pattern:/%[qQiIwWxs]?<(?:[^<>\\]|\\[\s\S])*>/,inside:{interpolation:n}},{pattern:/("|')(#\{[^}]+\}|\\(?:\r?\n|\r)|\\?.)*?\1/,inside:{interpolation:n}}]}(Prism); +Prism.languages.go=Prism.languages.extend("clike",{keyword:/\b(break|case|chan|const|continue|default|defer|else|fallthrough|for|func|go(to)?|if|import|interface|map|package|range|return|select|struct|switch|type|var)\b/,builtin:/\b(bool|byte|complex(64|128)|error|float(32|64)|rune|string|u?int(8|16|32|64|)|uintptr|append|cap|close|complex|copy|delete|imag|len|make|new|panic|print(ln)?|real|recover)\b/,"boolean":/\b(_|iota|nil|true|false)\b/,operator:/[*\/%^!=]=?|\+[=+]?|-[=-]?|\|[=|]?|&(?:=|&|\^=?)?|>(?:>=?|=)?|<(?:<=?|=|-)?|:=|\.\.\./,number:/\b(-?(0x[a-f\d]+|(\d+\.?\d*|\.\d+)(e[-+]?\d+)?)i?)\b/i,string:/("|'|`)(\\?.|\r|\n)*?\1/}),delete Prism.languages.go["class-name"]; +Prism.languages.java=Prism.languages.extend("clike",{keyword:/\b(abstract|continue|for|new|switch|assert|default|goto|package|synchronized|boolean|do|if|private|this|break|double|implements|protected|throw|byte|else|import|public|throws|case|enum|instanceof|return|transient|catch|extends|int|short|try|char|final|interface|static|void|class|finally|long|strictfp|volatile|const|float|native|super|while)\b/,number:/\b0b[01]+\b|\b0x[\da-f]*\.?[\da-fp\-]+\b|\b\d*\.?\d+(?:e[+-]?\d+)?[df]?\b/i,operator:{pattern:/(^|[^.])(?:\+[+=]?|-[-=]?|!=?|<>?>?=?|==?|&[&=]?|\|[|=]?|\*=?|\/=?|%=?|\^=?|[?:~])/m,lookbehind:!0}}),Prism.languages.insertBefore("java","function",{annotation:{alias:"punctuation",pattern:/(^|[^.])@\w+/,lookbehind:!0}}); +Prism.languages.python={"triple-quoted-string":{pattern:/"""[\s\S]+?"""|'''[\s\S]+?'''/,alias:"string"},comment:{pattern:/(^|[^\\])#.*/,lookbehind:!0},string:{pattern:/("|')(?:\\\\|\\?[^\\\r\n])*?\1/,greedy:!0},"function":{pattern:/((?:^|\s)def[ \t]+)[a-zA-Z_][a-zA-Z0-9_]*(?=\()/g,lookbehind:!0},"class-name":{pattern:/(\bclass\s+)[a-z0-9_]+/i,lookbehind:!0},keyword:/\b(?:as|assert|async|await|break|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|pass|print|raise|return|try|while|with|yield)\b/,"boolean":/\b(?:True|False)\b/,number:/\b-?(?:0[bo])?(?:(?:\d|0x[\da-f])[\da-f]*\.?\d*|\.\d+)(?:e[+-]?\d+)?j?\b/i,operator:/[-+%=]=?|!=|\*\*?=?|\/\/?=?|<[<=>]?|>[=>]?|[&|^~]|\b(?:or|and|not)\b/,punctuation:/[{}[\];(),.:]/}; +Prism.languages.scala=Prism.languages.extend("java",{keyword:/<-|=>|\b(?:abstract|case|catch|class|def|do|else|extends|final|finally|for|forSome|if|implicit|import|lazy|match|new|null|object|override|package|private|protected|return|sealed|self|super|this|throw|trait|try|type|val|var|while|with|yield)\b/,string:[{pattern:/"""[\W\w]*?"""/,greedy:!0},{pattern:/("|')(?:\\\\|\\?[^\\\r\n])*?\1/,greedy:!0}],builtin:/\b(?:String|Int|Long|Short|Byte|Boolean|Double|Float|Char|Any|AnyRef|AnyVal|Unit|Nothing)\b/,number:/\b(?:0x[\da-f]*\.?[\da-f]+|\d*\.?\d+e?\d*[dfl]?)\b/i,symbol:/'[^\d\s\\]\w*/}),delete Prism.languages.scala["class-name"],delete Prism.languages.scala["function"]; diff --git a/atmo/templates/account/login.html b/atmo/templates/account/login.html index 7326aa56..913eab5f 100644 --- a/atmo/templates/account/login.html +++ b/atmo/templates/account/login.html @@ -9,7 +9,7 @@ {% block content %}
    -
    +

    Telemetry Analysis Service

    To get started, log in with your @mozilla.com Google account

    @@ -19,9 +19,8 @@

    Telemetry Analysis Service

    -
    -
    +

    What is this?

    @@ -36,7 +35,7 @@

    What is this?

    -
    +

    Links & resources

    diff --git a/atmo/templates/atmo/_form.html b/atmo/templates/atmo/_form.html new file mode 100644 index 00000000..75cf132b --- /dev/null +++ b/atmo/templates/atmo/_form.html @@ -0,0 +1,34 @@ +{% if form.non_field_errors %} +
    +

    + {% for error in form.non_field_errors %} + {{ error }}{% if not forloop.last %}, {% endif %} + {% endfor %} +

    +
    +{% endif %} +{% for field in form %} +
    + {% if not field.is_hidden %} + + {% endif %} + {% if 'datetimepicker' in field.field.widget.attrs.class %} +
    + {% endif %} + {{ field }} + {% if 'datetimepicker' in field.field.widget.attrs.class %} + +
    + {% endif %} + {% if field.errors %} +

    + {% for error in field.errors %} + {{ error }}{% if not forloop.last %}, {% endif %} + {% endfor %} +

    + {% endif %} + {% if field.help_text %} +

    {{ field.help_text|safe }}

    + {% endif %} +
    +{% endfor %} diff --git a/atmo/templates/atmo/base.html b/atmo/templates/atmo/base.html index 594a50c5..15dbe316 100644 --- a/atmo/templates/atmo/base.html +++ b/atmo/templates/atmo/base.html @@ -6,7 +6,6 @@ - @@ -14,31 +13,30 @@ - + + - Telemetry Self-Serve Data Analysis - {% block head_title %}Welcome{% endblock %} + Telemetry Analysis Service - {% block head_title %}Welcome{% endblock %} {% endblock %} {% block head_extra %} {% endblock %} -