├── contacts ├── __init__.py ├── core │ ├── __init__.py │ ├── tests │ │ ├── __init__.py │ │ └── test_contact_model.py │ ├── migrations │ │ ├── __init__.py │ │ ├── 0002_sort_contact_model.py │ │ └── 0001_create_contact_model.py │ ├── admin.py │ ├── apps.py │ ├── models.py │ ├── static │ │ ├── styles.css │ │ └── app.js │ ├── templates │ │ └── core │ │ │ └── base.html │ └── views.py ├── wsgi.py ├── urls.py └── settings.py ├── .gitignore ├── .env.sample ├── Pipfile ├── manage.py ├── README.md ├── LICENSE └── Pipfile.lock /contacts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /contacts/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /contacts/core/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /contacts/core/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .env 3 | __pycache__/ 4 | db.sqlite3 5 | staticfiles/ 6 | -------------------------------------------------------------------------------- /contacts/core/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /contacts/core/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CoreConfig(AppConfig): 5 | name = 'core' 6 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | SECRET_KEY=my-dear-secret 2 | DEBUG=True 3 | ALLOWED_HOSTS=localhost,127.0.0.1 4 | 5 | LANGUAGE_CODE=pt-br 6 | TIME_ZONE=America/Sao_Paulo 7 | 8 | DATABASE_URL=sqlite:///db.sqlite3 9 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | 3 | url = "https://pypi.python.org/simple" 4 | verify_ssl = true 5 | name = "pypi" 6 | 7 | 8 | [requires] 9 | 10 | python_version = "3.6" 11 | 12 | 13 | [packages] 14 | 15 | dj-database-url = "*" 16 | django = "*" 17 | python-decouple = "*" 18 | whitenoise = "*" 19 | "psycopg2" = "*" 20 | mixer = "*" 21 | 22 | 23 | [dev-packages] 24 | 25 | ipdb = "*" 26 | prospector = "*" 27 | -------------------------------------------------------------------------------- /contacts/core/migrations/0002_sort_contact_model.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.2 on 2018-03-10 20:59 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('core', '0001_create_contact_model'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='contact', 15 | options={'ordering': ['name']}, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /contacts/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for contacts project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "contacts.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "contacts.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError as exc: 10 | raise ImportError( 11 | "Couldn't import Django. Are you sure it's installed and " 12 | "available on your PYTHONPATH environment variable? Did you " 13 | "forget to activate a virtual environment?" 14 | ) from exc 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /contacts/core/models.py: -------------------------------------------------------------------------------- 1 | from hashlib import md5 2 | from urllib.parse import urlencode 3 | 4 | from django.db import models 5 | 6 | 7 | class Contact(models.Model): 8 | name = models.CharField('Nome', max_length=128) 9 | fone = models.CharField('Telefone', max_length=16, null=True) 10 | email = models.EmailField('Email') 11 | 12 | def avatar(self): 13 | email = self.email.lower() 14 | hash = md5(email.encode()).hexdigest() 15 | default = f'https://api.adorable.io/avatars/256/{email}.png' 16 | params = urlencode({'d': default, 's': 512}) 17 | return f'https://www.gravatar.com/avatar/{hash}?{params}' 18 | 19 | class Meta: 20 | ordering = ['name'] 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django-Ajax Contacts 2 | 3 | Django backend and vanilla JavaScript frontend for a contacts app. The purpose of this repo is to discuss responsabilities of backend (Python) and frontend (JavaScript) code in the architecture of a web app in a [live coding series](https://www.youtube.com/playlist?list=PLUj8WMX6gr49dYxG8jnPwDW-9quA9NEAX). 4 | 5 | ## Install 6 | 7 | Requires [Python](https://www.python.org/) 3.6+ with [Pipenv](https://docs.pipenv.org/) – just `pip install pipenv` and you're ready to go. 8 | 9 | 1. Copy `.env.sample` as `.env` and adjust your settings 10 | 1. Install de dependencies:
`$ pipenv install --dev` 11 | 1. Activate the environment:
`$ pipenv shell` 12 | 1. Run the project:
`$ python manage.py runserver` -------------------------------------------------------------------------------- /contacts/core/static/styles.css: -------------------------------------------------------------------------------- 1 | .container-fluid { 2 | margin: 3rem auto; 3 | max-width: 62%; 4 | } 5 | 6 | /* Header */ 7 | 8 | header { 9 | border-bottom: 1px solid gray; 10 | margin-bottom: 3rem; 11 | } 12 | 13 | header div { 14 | line-height: 3rem; 15 | vertical-align: middle; 16 | } 17 | 18 | /* Sidebar */ 19 | 20 | aside li { 21 | cursor: pointer; 22 | padding: 2rem 0; 23 | opacity: 0.75; 24 | font-size: 1.9rem; 25 | } 26 | 27 | aside li.active, 28 | aside li:hover { 29 | opacity: 1 30 | } 31 | 32 | aside img { 33 | width: 2.6rem; 34 | } 35 | 36 | /* Contact details */ 37 | 38 | article h2 , 39 | article dd { 40 | margin: 0 0 2rem 0; 41 | } 42 | 43 | article img { 44 | width: 7rem; 45 | } 46 | 47 | article a { 48 | color: inherit; 49 | } 50 | -------------------------------------------------------------------------------- /contacts/core/migrations/0001_create_contact_model.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.2 on 2018-02-27 17:21 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Contact', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('name', models.CharField(max_length=128, verbose_name='Nome')), 19 | ('fone', models.CharField(max_length=16, null=True, verbose_name='Telefone')), 20 | ('email', models.EmailField(max_length=254, verbose_name='Email')), 21 | ], 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /contacts/core/tests/test_contact_model.py: -------------------------------------------------------------------------------- 1 | from hashlib import md5 2 | from urllib.parse import urlencode 3 | 4 | from django.test import TestCase 5 | from mixer.backend.django import mixer 6 | 7 | from contacts.core.models import Contact 8 | 9 | 10 | class TestContactModel(TestCase): 11 | 12 | def setUp(self): 13 | self.contact = mixer.blend(Contact) 14 | 15 | def test_create(self): 16 | self.assertEqual(1, Contact.objects.count()) 17 | 18 | def test_phone_is_optional(self): 19 | Contact.objects.create(name='Fulano', email='fulano@server.org') 20 | self.assertEqual(2, Contact.objects.count()) 21 | 22 | def test_email_is_required(self): 23 | with self.assertRaises(TypeError): 24 | Contact.objects.create(name='Fulano', phone='011 1406') 25 | self.assertEqual(1, Contact.objects.count()) 26 | 27 | def test_avatar(self): 28 | email = self.contact.email.lower() 29 | hash = md5(email.encode()).hexdigest() 30 | params = urlencode({ 31 | 'd': f'https://api.adorable.io/avatars/256/{email}.png', 32 | 's': 512 33 | }) 34 | expected = f'https://www.gravatar.com/avatar/{hash}?{params}' 35 | self.assertEqual(expected, self.contact.avatar()) 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to -------------------------------------------------------------------------------- /contacts/urls.py: -------------------------------------------------------------------------------- 1 | """contacts URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.0/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.conf import settings 17 | from django.conf.urls.static import static 18 | from django.urls import path 19 | 20 | from contacts.core.views import contact_detail, contacts, contacts_new, home 21 | 22 | 23 | urlpatterns = [ 24 | path('', home, name='home'), 25 | path('contacts//', contact_detail, name='contact-details'), 26 | path('contacts/', contacts, name='contacts'), 27 | path('contacts/new/', contacts_new, name='contacts-new') 28 | ] 29 | 30 | if settings.DEBUG: 31 | urlpatterns += static( 32 | settings.STATIC_URL, 33 | document_root=settings.STATIC_ROOT 34 | ) 35 | -------------------------------------------------------------------------------- /contacts/core/templates/core/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Agenda de Contatos 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 |
23 |
24 |

Agenda de Contatos

25 |
26 |
27 |
28 | 31 | 34 |
35 |
36 |
37 | 38 |
39 | 40 | 43 | 44 |
45 |
46 | 47 |
48 |
49 | 50 | {% csrf_token %} 51 | 52 | -------------------------------------------------------------------------------- /contacts/core/views.py: -------------------------------------------------------------------------------- 1 | from django.http import JsonResponse, HttpResponseNotAllowed 2 | from django.shortcuts import get_object_or_404, render, resolve_url 3 | 4 | from contacts.core.models import Contact 5 | 6 | 7 | def home(request): 8 | return render(request, 'core/base.html') 9 | 10 | 11 | def contacts(request): 12 | contacts = [_contact_summary(contact) for contact in Contact.objects.all()] 13 | return JsonResponse(dict(contacts=contacts)) 14 | 15 | 16 | def contact_detail(request, pk): 17 | if request.method == 'POST': 18 | data = (request.POST.get(key) for key in ('name', 'fone', 'email')) 19 | contact = Contact.objects.get(pk=pk) 20 | contact.name, contact.fone, contact.email = data 21 | contact.save() 22 | else: 23 | contact = get_object_or_404(Contact, pk=pk) 24 | 25 | response = dict( 26 | name=contact.name, 27 | avatar=contact.avatar(), 28 | email=contact.email, 29 | phone=contact.fone, 30 | url=resolve_url('contact-details', pk=contact.pk) 31 | ) 32 | return JsonResponse(response) 33 | 34 | 35 | def contacts_new(request): 36 | if request.method != 'POST': 37 | return HttpResponseNotAllowed(('POST',)) 38 | 39 | data = { 40 | key: value for key, value in request.POST.items() 41 | if key in ('name', 'fone', 'email') 42 | } 43 | contact = Contact.objects.create(**data) 44 | response = dict( 45 | name=contact.name, 46 | avatar=contact.avatar(), 47 | email=contact.email, 48 | phone=contact.fone 49 | ) 50 | 51 | return JsonResponse(response, status=201) 52 | 53 | 54 | def _contact_summary(contact): 55 | return dict( 56 | name=contact.name, 57 | avatar=contact.avatar(), 58 | url=resolve_url('contact-details', pk=contact.pk) 59 | ) 60 | -------------------------------------------------------------------------------- /contacts/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for contacts project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.0.2. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | from decouple import Csv, config 16 | from dj_database_url import parse 17 | 18 | 19 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 20 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 21 | 22 | 23 | # Quick-start development settings - unsuitable for production 24 | # See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/ 25 | 26 | # SECURITY WARNING: keep the secret key used in production secret! 27 | SECRET_KEY = config('SECRET_KEY') 28 | 29 | # SECURITY WARNING: don't run with debug turned on in production! 30 | DEBUG = config('DEBUG', default=False, cast=bool) 31 | 32 | ALLOWED_HOSTS = config('ALLOWED_HOSTS', cast=Csv()) 33 | 34 | 35 | # Application definition 36 | 37 | INSTALLED_APPS = [ 38 | 'django.contrib.admin', 39 | 'django.contrib.auth', 40 | 'django.contrib.contenttypes', 41 | 'django.contrib.sessions', 42 | 'django.contrib.messages', 43 | 'django.contrib.staticfiles', 44 | 'contacts.core' 45 | ] 46 | 47 | MIDDLEWARE = [ 48 | 'django.middleware.security.SecurityMiddleware', 49 | 'whitenoise.middleware.WhiteNoiseMiddleware', 50 | 'django.contrib.sessions.middleware.SessionMiddleware', 51 | 'django.middleware.common.CommonMiddleware', 52 | 'django.middleware.csrf.CsrfViewMiddleware', 53 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 54 | 'django.contrib.messages.middleware.MessageMiddleware', 55 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 56 | ] 57 | 58 | ROOT_URLCONF = 'contacts.urls' 59 | 60 | TEMPLATES = [ 61 | { 62 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 63 | 'DIRS': [], 64 | 'APP_DIRS': True, 65 | 'OPTIONS': { 66 | 'context_processors': [ 67 | 'django.template.context_processors.debug', 68 | 'django.template.context_processors.request', 69 | 'django.contrib.auth.context_processors.auth', 70 | 'django.contrib.messages.context_processors.messages', 71 | ], 72 | }, 73 | }, 74 | ] 75 | 76 | WSGI_APPLICATION = 'contacts.wsgi.application' 77 | 78 | 79 | # Database 80 | # https://docs.djangoproject.com/en/2.0/ref/settings/#databases 81 | 82 | DATABASES = {} 83 | FALLBACK_DATABASE = 'sqlite:///db.sqlite3' 84 | DATABASES['default'] = parse(config('DATABASE_URL', default=FALLBACK_DATABASE)) 85 | 86 | 87 | # Password validation 88 | # https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators 89 | 90 | AUTH_PASSWORD_VALIDATORS = [ 91 | { 92 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 93 | }, 94 | { 95 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 96 | }, 97 | { 98 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 99 | }, 100 | { 101 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 102 | }, 103 | ] 104 | 105 | 106 | # Internationalization 107 | # https://docs.djangoproject.com/en/2.0/topics/i18n/ 108 | 109 | LANGUAGE_CODE = config('LANGUAGE_CODE', default='en-us') 110 | 111 | TIME_ZONE = config('TIME_ZONE', default='UTC') 112 | 113 | USE_I18N = True 114 | 115 | USE_L10N = True 116 | 117 | USE_TZ = True 118 | 119 | 120 | # Static files (CSS, JavaScript, Images) 121 | # https://docs.djangoproject.com/en/2.0/howto/static-files/ 122 | 123 | STATIC_URL = '/static/' 124 | STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') 125 | STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' 126 | -------------------------------------------------------------------------------- /contacts/core/static/app.js: -------------------------------------------------------------------------------- 1 | /****************************************************************************/ 2 | /* Boilerplate / 3 | /****************************************************************************/ 4 | 5 | var fail = function (request) {}; 6 | var error_ = function () {}; 7 | 8 | var ajax = function(url, method, data, success, fail, error_) { 9 | var request = new XMLHttpRequest(); 10 | request.open(method, url, true); 11 | request.onerror = error_; 12 | request.onload = function() { 13 | if (request.status >= 200 && request.status < 400) { 14 | success(request); 15 | } else { 16 | fail(request); 17 | } 18 | }; 19 | 20 | if (method == 'POST') { 21 | var csrftoken = document.querySelector('input[name=csrfmiddlewaretoken]').value; 22 | request.setRequestHeader("X-CSRFToken", csrftoken); 23 | 24 | request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8'); 25 | request.send(data); 26 | } else { 27 | request.send(); 28 | } 29 | }; 30 | 31 | 32 | /****************************************************************************/ 33 | /* Contact details / 34 | /****************************************************************************/ 35 | 36 | var enable_edit_button = function (contact) { 37 | var button = document.querySelector('button.edit'); 38 | var current_classes = button.className.split(' '); 39 | var new_classes = current_classes.filter( 40 | function (cls) { return cls != 'hide'; } 41 | ); 42 | button.className = new_classes.join(' '); 43 | button.removeAttribute('disabled'); 44 | button.onclick = function () { 45 | show_form(contact); 46 | }; 47 | }; 48 | 49 | var disable_edit_button = function () { 50 | var button = document.querySelector('button.edit'); 51 | if (button.className.indexOf('hide') == -1) { 52 | button.className += ' hide'; 53 | } 54 | button.setAttribute('disabled', true); 55 | }; 56 | 57 | var show_contact_details = function (contact) { 58 | var article = document.querySelector('article'); 59 | article.innerHTML = ''; 60 | 61 | var h2 = document.createElement('h2'); 62 | var img = document.createElement('img'); 63 | var dl = document.createElement('dl'); 64 | var a = document.createElement('a'); 65 | var dt_phone = document.createElement('dt'); 66 | var dd_phone = document.createElement('dd'); 67 | var dt_email = document.createElement('dt'); 68 | var dd_email = document.createElement('dd'); 69 | 70 | var name = document.createTextNode(contact.name); 71 | var phone = document.createTextNode(contact.phone); 72 | var phone_label = document.createTextNode('Telefone'); 73 | var email = document.createTextNode(contact.email); 74 | var email_label = document.createTextNode('E-mail'); 75 | 76 | img.setAttribute('src', contact.avatar); 77 | img.setAttribute('alt', contact.name); 78 | img.setAttribute('class', 'img-circle'); 79 | 80 | h2.appendChild(img); 81 | h2.appendChild(name); 82 | 83 | dt_phone.appendChild(phone_label); 84 | dd_phone.appendChild(phone); 85 | 86 | a.setAttribute('href', 'mailto:' + contact.email); 87 | a.appendChild(email); 88 | 89 | dt_email.appendChild(email_label); 90 | dd_email.appendChild(a); 91 | 92 | dl.appendChild(dt_phone); 93 | dl.appendChild(dd_phone); 94 | dl.appendChild(dt_email); 95 | dl.appendChild(dd_email); 96 | 97 | article.appendChild(h2); 98 | article.appendChild(dl); 99 | article.dataset.editContactUrl = contact.url; 100 | 101 | enable_edit_button(contact); 102 | 103 | }; 104 | 105 | var load_contact_details = function (e) { 106 | var url = e.target.dataset.url; 107 | 108 | var contacts = document.querySelectorAll('aside ol li'); 109 | for (var i = 0; i < contacts.length; i++) { 110 | contacts[i].setAttribute('class', ''); 111 | } 112 | e.target.setAttribute('class', 'active'); 113 | 114 | var success = function (request) { 115 | var contact = JSON.parse(request.responseText); 116 | show_contact_details(contact); 117 | }; 118 | 119 | ajax(url, 'GET', {}, success, fail, error_); 120 | }; 121 | 122 | 123 | /****************************************************************************/ 124 | /* Contact list / 125 | /****************************************************************************/ 126 | 127 | var add_to_contact_list = function (contact, contact_list) { 128 | var li = document.createElement('li'); 129 | var img = document.createElement('img'); 130 | var name = document.createTextNode(contact.name); 131 | 132 | img.setAttribute('src', contact.avatar); 133 | img.setAttribute('alt', contact.name); 134 | img.setAttribute('class', 'img-circle'); 135 | 136 | li.setAttribute('data-url', contact.url); 137 | li.appendChild(img); 138 | li.appendChild(name); 139 | li.onclick = load_contact_details; 140 | 141 | contact_list.appendChild(li); 142 | }; 143 | 144 | var load_contacts = function () { 145 | var success = function (request) { 146 | var resp = JSON.parse(request.responseText); 147 | var contacts = document.querySelector('aside ol'); 148 | contacts.innerHTML = ''; 149 | 150 | for (var i = 0; i < resp.contacts.length; i++) { 151 | var contact = resp.contacts[i]; 152 | add_to_contact_list(contact, contacts); 153 | } 154 | }; 155 | 156 | ajax('/contacts/', 'GET', {}, success, fail, error_); 157 | }; 158 | 159 | 160 | /****************************************************************************/ 161 | /* Forms / 162 | /****************************************************************************/ 163 | 164 | var form_group = function (field_name, field_label, field_type, value) { 165 | var div = document.createElement('div'); 166 | var label = document.createElement('label'); 167 | var input = document.createElement('input'); 168 | 169 | label.setAttribute('for', field_name); 170 | label.innerHTML = field_label; 171 | 172 | input.setAttribute('type', field_type); 173 | input.setAttribute('id', field_name); 174 | input.className = 'form-control input-sm'; 175 | if (Boolean(value)) { 176 | input.setAttribute('value', value); 177 | } 178 | 179 | div.className = 'form-group'; 180 | div.appendChild(label); 181 | div.appendChild(input); 182 | 183 | return div; 184 | }; 185 | 186 | var show_form = function (contact) { 187 | disable_edit_button(); 188 | 189 | var article = document.querySelector('article'); 190 | article.innerHTML = ''; 191 | 192 | var name_value = ''; 193 | var telephone_value = ''; 194 | var email_value = ''; 195 | if (contact.constructor.name !== 'MouseEvent') { 196 | name_value = contact.name; 197 | telephone_value = contact.phone; 198 | email_value = contact.email; 199 | } 200 | 201 | var h2 = document.createElement('h2'); 202 | var form = document.createElement('form'); 203 | var name = form_group('name', 'Nome', 'text', name_value); 204 | var telephone = form_group('telephone', 'Telefone', 'text', telephone_value); 205 | var email = form_group('email', 'E-mail', 'email', email_value); 206 | var p = document.createElement('p'); 207 | var button = document.createElement('button'); 208 | 209 | if (contact.constructor.name !== 'MouseEvent') { 210 | h2.innerHTML = 'Editar contato'; 211 | } else { 212 | h2.innerHTML = 'Novo contato'; 213 | } 214 | 215 | button.setAttribute('type', 'submit'); 216 | button.className = 'btn btn-default'; 217 | button.innerHTML = 'Salvar'; 218 | 219 | p.className = 'pull-right'; 220 | p.appendChild(button); 221 | 222 | form.setAttribute('role', 'form'); 223 | form.appendChild(name); 224 | form.appendChild(telephone); 225 | form.appendChild(email); 226 | form.appendChild(p); 227 | 228 | if (contact.constructor.name !== 'MouseEvent') { 229 | form.onsubmit = edit_contact; 230 | } else { 231 | form.onsubmit = new_contact; 232 | } 233 | 234 | article.appendChild(h2); 235 | article.appendChild(form); 236 | 237 | }; 238 | 239 | 240 | /****************************************************************************/ 241 | /* User actions / 242 | /****************************************************************************/ 243 | 244 | var get_form_data = function () { 245 | var name = document.forms[0].name.value; 246 | var telephone = document.forms[0].telephone.value; 247 | var email = document.forms[0].email.value; 248 | 249 | var article = document.querySelector('article'); 250 | article.innerHTML = ''; 251 | 252 | return 'name=' + encodeURI(name) + '&fone=' + encodeURI(telephone) + '&email=' + encodeURI(email); 253 | }; 254 | 255 | var success_insert_or_update = function (request) { 256 | var contact = JSON.parse(request.responseText); 257 | show_contact_details(contact); 258 | load_contacts(); 259 | }; 260 | 261 | var new_contact = function (e) { 262 | e.preventDefault(); 263 | var url = document.querySelector('article').dataset.newContactUrl; 264 | var data = get_form_data(); 265 | ajax(url, 'POST', data, success_insert_or_update, fail, error_); 266 | }; 267 | 268 | var edit_contact = function (e) { 269 | e.preventDefault(); 270 | var url = document.querySelector('article').dataset.editContactUrl; 271 | var data = get_form_data(); 272 | ajax(url, 'POST', data, success_insert_or_update, fail, error_); 273 | }; 274 | 275 | 276 | /****************************************************************************/ 277 | /* Init / 278 | /****************************************************************************/ 279 | 280 | load_contacts(); 281 | document.querySelector('button.new').onclick = show_form; 282 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "9b843199db1d7f8983bc57af2b3fc2f47d5e58fd26561d85fecec55f759eb16c" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.6" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.python.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "dj-database-url": { 20 | "hashes": [ 21 | "sha256:4aeaeb1f573c74835b0686a2b46b85990571159ffc21aa57ecd4d1e1cb334163", 22 | "sha256:851785365761ebe4994a921b433062309eb882fedd318e1b0fcecc607ed02da9" 23 | ], 24 | "version": "==0.5.0" 25 | }, 26 | "django": { 27 | "hashes": [ 28 | "sha256:7c8ff92285406fb349e765e9ade685eec7271d6f5c3f918e495a74768b765c99", 29 | "sha256:dc3b61d054f1bced64628c62025d480f655303aea9f408e5996c339a543b45f0" 30 | ], 31 | "version": "==2.0.2" 32 | }, 33 | "faker": { 34 | "hashes": [ 35 | "sha256:2f6ccc9da046d4cd20401734cf6a1ac73a4e4d8256e7b283496ee6827ad2eb60", 36 | "sha256:e928cf853ef69d7471421f2a3716a1239e43de0fa9855f4016ee0c9f1057328a" 37 | ], 38 | "version": "==0.8.8" 39 | }, 40 | "mixer": { 41 | "hashes": [ 42 | "sha256:159b6e3e1fa6aea1d5af101986f8bb6e283be56e0ed4fb9562f1b721de604938", 43 | "sha256:f7cc0fd3b7177e459178c7a7fc57d827a8e0470f3c60f34470bddcf5362aedf6" 44 | ], 45 | "version": "==6.0.1" 46 | }, 47 | "psycopg2": { 48 | "hashes": [ 49 | "sha256:027ae518d0e3b8fff41990e598bc7774c3d08a3a20e9ecc0b59fb2aaaf152f7f", 50 | "sha256:092a80da1b052a181b6e6c765849c9b32d46c5dac3b81bf8c9b83e697f3cdbe8", 51 | "sha256:0b9851e798bae024ed1a2a6377a8dab4b8a128a56ed406f572f9f06194e4b275", 52 | "sha256:179c52eb870110a8c1b460c86d4f696d58510ea025602cd3f81453746fccb94f", 53 | "sha256:19983b77ec1fc2a210092aa0333ee48811fd9fb5f194c6cd5b927ed409aea5f8", 54 | "sha256:1d90379d01d0dc50ae9b40c863933d87ff82d51dd7d52cea5d1cb7019afd72cd", 55 | "sha256:27467fd5af1dcc0a82d72927113b8f92da8f44b2efbdb8906bd76face95b596d", 56 | "sha256:32702e3bd8bfe12b36226ba9846ed9e22336fc4bd710039d594b36bd432ae255", 57 | "sha256:33f9e1032095e1436fa9ec424abcbd4c170da934fb70e391c5d78275d0307c75", 58 | "sha256:36030ca7f4b4519ee4f52a74edc4ec73c75abfb6ea1d80ac7480953d1c0aa3c3", 59 | "sha256:363fbbf4189722fc46779be1fad2597e2c40b3f577dc618f353a46391cf5d235", 60 | "sha256:6f302c486132f8dd11f143e919e236ea4467d53bf18c451cac577e6988ecbd05", 61 | "sha256:733166464598c239323142c071fa4c9b91c14359176e5ae7e202db6bcc1d2eb5", 62 | "sha256:7cbc3b21ce2f681ca9ad2d8c0901090b23a30c955e980ebf1006d41f37068a95", 63 | "sha256:888bba7841116e529f407f15c6d28fe3ef0760df8c45257442ec2f14f161c871", 64 | "sha256:8966829cb0d21a08a3c5ac971a2eb67c3927ae27c247300a8476554cc0ce2ae8", 65 | "sha256:8bf51191d60f6987482ef0cfe8511bbf4877a5aa7f313d7b488b53189cf26209", 66 | "sha256:8eb94c0625c529215b53c08fb4e461546e2f3fc96a49c13d5474b5ad7aeab6cf", 67 | "sha256:8ebba5314c609a05c6955e5773c7e0e57b8dd817e4f751f30de729be58fa5e78", 68 | "sha256:932a4c101af007cb3132b1f8a9ffef23386acc53dad46536dc5ba43a3235ae02", 69 | "sha256:ad75fe10bea19ad2188c5cb5fc4cdf53ee808d9b44578c94a3cd1e9fc2beb656", 70 | "sha256:aeaba399254ca79c299d9fe6aa811d3c3eac61458dee10270de7f4e71c624998", 71 | "sha256:b178e0923c93393e16646155794521e063ec17b7cc9f943f15b7d4b39776ea2c", 72 | "sha256:b68e89bb086a9476fa85298caab43f92d0a6af135a5f433d1f6b6d82cafa7b55", 73 | "sha256:d74cf9234ba76426add5e123449be08993a9b13ff434c6efa3a07caa305a619f", 74 | "sha256:f3d3a88128f0c219bdc5b2d9ccd496517199660cea021c560a3252116df91cbd", 75 | "sha256:fe6a7f87356116f5ea840c65b032af17deef0e1a5c34013a2962dd6f99b860dd" 76 | ], 77 | "version": "==2.7.4" 78 | }, 79 | "python-dateutil": { 80 | "hashes": [ 81 | "sha256:891c38b2a02f5bb1be3e4793866c8df49c7d19baabf9c1bad62547e0b4866aca", 82 | "sha256:95511bae634d69bc7329ba55e646499a842bc4ec342ad54a8cdb65645a0aad3c" 83 | ], 84 | "version": "==2.6.1" 85 | }, 86 | "python-decouple": { 87 | "hashes": [ 88 | "sha256:1317df14b43efee4337a4aa02914bf004f010cd56d6c4bd894e6474ec8c4fe2d" 89 | ], 90 | "version": "==3.1" 91 | }, 92 | "pytz": { 93 | "hashes": [ 94 | "sha256:07edfc3d4d2705a20a6e99d97f0c4b61c800b8232dc1c04d87e8554f130148dd", 95 | "sha256:3a47ff71597f821cd84a162e71593004286e5be07a340fd462f0d33a760782b5", 96 | "sha256:410bcd1d6409026fbaa65d9ed33bf6dd8b1e94a499e32168acfc7b332e4095c0", 97 | "sha256:5bd55c744e6feaa4d599a6cbd8228b4f8f9ba96de2c38d56f08e534b3c9edf0d", 98 | "sha256:61242a9abc626379574a166dc0e96a66cd7c3b27fc10868003fa210be4bff1c9", 99 | "sha256:887ab5e5b32e4d0c86efddd3d055c1f363cbaa583beb8da5e22d2fa2f64d51ef", 100 | "sha256:ba18e6a243b3625513d85239b3e49055a2f0318466e0b8a92b8fb8ca7ccdf55f", 101 | "sha256:ed6509d9af298b7995d69a440e2822288f2eca1681b8cce37673dbb10091e5fe", 102 | "sha256:f93ddcdd6342f94cea379c73cddb5724e0d6d0a1c91c9bdef364dc0368ba4fda" 103 | ], 104 | "version": "==2018.3" 105 | }, 106 | "six": { 107 | "hashes": [ 108 | "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", 109 | "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" 110 | ], 111 | "version": "==1.11.0" 112 | }, 113 | "text-unidecode": { 114 | "hashes": [ 115 | "sha256:5a1375bb2ba7968740508ae38d92e1f889a0832913cb1c447d5e2046061a396d", 116 | "sha256:801e38bd550b943563660a91de8d4b6fa5df60a542be9093f7abf819f86050cc" 117 | ], 118 | "version": "==1.2" 119 | }, 120 | "whitenoise": { 121 | "hashes": [ 122 | "sha256:15f43b2e701821b95c9016cf469d29e2a546cb1c7dead584ba82c36f843995cf", 123 | "sha256:9d81515f2b5b27051910996e1e860b1332e354d9e7bcf30c98f21dcb6713e0dd" 124 | ], 125 | "version": "==3.3.1" 126 | } 127 | }, 128 | "develop": { 129 | "appnope": { 130 | "hashes": [ 131 | "sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0", 132 | "sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71" 133 | ], 134 | "markers": "sys_platform == 'darwin'", 135 | "version": "==0.1.0" 136 | }, 137 | "astroid": { 138 | "hashes": [ 139 | "sha256:db5cfc9af6e0b60cd07c19478fb54021fc20d2d189882fbcbc94fc69a8aecc58", 140 | "sha256:f0a0e386dbca9f93ea9f3ea6f32b37a24720502b7baa9cb17c3976a680d43a06" 141 | ], 142 | "version": "==1.6.1" 143 | }, 144 | "backports.functools-lru-cache": { 145 | "hashes": [ 146 | "sha256:9d98697f088eb1b0fa451391f91afb5e3ebde16bbdb272819fd091151fda4f1a", 147 | "sha256:f0b0e4eba956de51238e17573b7087e852dfe9854afd2e9c873f73fc0ca0a6dd" 148 | ], 149 | "markers": "python_version < '3.4'", 150 | "version": "==1.5" 151 | }, 152 | "colorama": { 153 | "hashes": [ 154 | "sha256:463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda", 155 | "sha256:48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1" 156 | ], 157 | "markers": "sys_platform == 'win32'", 158 | "version": "==0.3.9" 159 | }, 160 | "configparser": { 161 | "hashes": [ 162 | "sha256:5308b47021bc2340965c371f0f058cc6971a04502638d4244225c49d80db273a" 163 | ], 164 | "markers": "python_version == '2.7'", 165 | "version": "==3.5.0" 166 | }, 167 | "decorator": { 168 | "hashes": [ 169 | "sha256:7d46dd9f3ea1cf5f06ee0e4e1277ae618cf48dfb10ada7c8427cd46c42702a0e", 170 | "sha256:94d1d8905f5010d74bbbd86c30471255661a14187c45f8d7f3e5aa8540fdb2e5" 171 | ], 172 | "version": "==4.2.1" 173 | }, 174 | "dodgy": { 175 | "hashes": [ 176 | "sha256:65e13cf878d7aff129f1461c13cb5fd1bb6dfe66bb5327e09379c3877763280c" 177 | ], 178 | "version": "==0.1.9" 179 | }, 180 | "enum34": { 181 | "hashes": [ 182 | "sha256:2d81cbbe0e73112bdfe6ef8576f2238f2ba27dd0d55752a776c41d38b7da2850", 183 | "sha256:644837f692e5f550741432dd3f223bbb9852018674981b1664e5dc339387588a", 184 | "sha256:6bd0f6ad48ec2aa117d3d141940d484deccda84d4fcd884f5c3d93c23ecd8c79", 185 | "sha256:8ad8c4783bf61ded74527bffb48ed9b54166685e4230386a9ed9b1279e2df5b1" 186 | ], 187 | "markers": "python_version < '3.4'", 188 | "version": "==1.1.6" 189 | }, 190 | "flake8": { 191 | "hashes": [ 192 | "sha256:7253265f7abd8b313e3892944044a365e3f4ac3fcdcfb4298f55ee9ddf188ba0", 193 | "sha256:c7841163e2b576d435799169b78703ad6ac1bbb0f199994fc05f700b2a90ea37" 194 | ], 195 | "version": "==3.5.0" 196 | }, 197 | "flake8-polyfill": { 198 | "hashes": [ 199 | "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9", 200 | "sha256:e44b087597f6da52ec6393a709e7108b2905317d0c0b744cdca6208e670d8eda" 201 | ], 202 | "version": "==1.0.2" 203 | }, 204 | "ipdb": { 205 | "hashes": [ 206 | "sha256:7081c65ed7bfe7737f83fa4213ca8afd9617b42ff6b3f1daf9a3419839a2a00a" 207 | ], 208 | "version": "==0.11" 209 | }, 210 | "ipython": { 211 | "hashes": [ 212 | "sha256:51c158a6c8b899898d1c91c6b51a34110196815cc905f9be0fa5878e19355608", 213 | "sha256:fcc6d46f08c3c4de7b15ae1c426e15be1b7932bcda9d83ce1a4304e8c1129df3" 214 | ], 215 | "version": "==6.2.1" 216 | }, 217 | "ipython-genutils": { 218 | "hashes": [ 219 | "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8", 220 | "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8" 221 | ], 222 | "version": "==0.2.0" 223 | }, 224 | "isort": { 225 | "hashes": [ 226 | "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", 227 | "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", 228 | "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" 229 | ], 230 | "version": "==4.3.4" 231 | }, 232 | "jedi": { 233 | "hashes": [ 234 | "sha256:d6e799d04d1ade9459ed0f20de47c32f2285438956a677d083d3c98def59fa97", 235 | "sha256:d795f2c2e659f5ea39a839e5230d70a0b045d0daee7ca2403568d8f348d0ad89" 236 | ], 237 | "version": "==0.11.1" 238 | }, 239 | "lazy-object-proxy": { 240 | "hashes": [ 241 | "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", 242 | "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", 243 | "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", 244 | "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", 245 | "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", 246 | "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", 247 | "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", 248 | "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", 249 | "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", 250 | "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", 251 | "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", 252 | "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", 253 | "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", 254 | "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", 255 | "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", 256 | "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", 257 | "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", 258 | "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", 259 | "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", 260 | "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", 261 | "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", 262 | "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", 263 | "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", 264 | "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", 265 | "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", 266 | "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", 267 | "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", 268 | "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", 269 | "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" 270 | ], 271 | "version": "==1.3.1" 272 | }, 273 | "mccabe": { 274 | "hashes": [ 275 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 276 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 277 | ], 278 | "version": "==0.6.1" 279 | }, 280 | "parso": { 281 | "hashes": [ 282 | "sha256:5815f3fe254e5665f3c5d6f54f086c2502035cb631a91341591b5a564203cffb", 283 | "sha256:a7bb86fe0844304869d1c08e8bd0e52be931228483025c422917411ab82d628a" 284 | ], 285 | "version": "==0.1.1" 286 | }, 287 | "pep8-naming": { 288 | "hashes": [ 289 | "sha256:0b82da414024f290e0e7aad80d9a7614d6a85bedd82304deeb2e4e0a74f8968b", 290 | "sha256:5a2a085340b1d530605a7f4a96816433be43504c58eace799dbacdcfd6febe9d" 291 | ], 292 | "version": "==0.5.0" 293 | }, 294 | "pexpect": { 295 | "hashes": [ 296 | "sha256:67b85a1565968e3d5b5e7c9283caddc90c3947a2625bed1905be27bd5a03e47d", 297 | "sha256:6ff881b07aff0cb8ec02055670443f784434395f90c3285d2ae470f921ade52a" 298 | ], 299 | "markers": "sys_platform != 'win32'", 300 | "version": "==4.4.0" 301 | }, 302 | "pickleshare": { 303 | "hashes": [ 304 | "sha256:84a9257227dfdd6fe1b4be1319096c20eb85ff1e82c7932f36efccfe1b09737b", 305 | "sha256:c9a2541f25aeabc070f12f452e1f2a8eae2abd51e1cd19e8430402bdf4c1d8b5" 306 | ], 307 | "version": "==0.7.4" 308 | }, 309 | "prompt-toolkit": { 310 | "hashes": [ 311 | "sha256:1df952620eccb399c53ebb359cc7d9a8d3a9538cb34c5a1344bdbeb29fbcc381", 312 | "sha256:3f473ae040ddaa52b52f97f6b4a493cfa9f5920c255a12dc56a7d34397a398a4", 313 | "sha256:858588f1983ca497f1cf4ffde01d978a3ea02b01c8a26a8bbc5cd2e66d816917" 314 | ], 315 | "version": "==1.0.15" 316 | }, 317 | "prospector": { 318 | "hashes": [ 319 | "sha256:79b658fd86150054cbbefed6533efbf4d94c08f6a91da051604176f6da828a14" 320 | ], 321 | "version": "==0.12.7" 322 | }, 323 | "ptyprocess": { 324 | "hashes": [ 325 | "sha256:e64193f0047ad603b71f202332ab5527c5e52aa7c8b609704fc28c0dc20c4365", 326 | "sha256:e8c43b5eee76b2083a9badde89fd1bbce6c8942d1045146e100b7b5e014f4f1a" 327 | ], 328 | "version": "==0.5.2" 329 | }, 330 | "pycodestyle": { 331 | "hashes": [ 332 | "sha256:2ce83f2046f5ab85c652ceceddfbde7a64a909900989b4b43e92b10b743d0ce5", 333 | "sha256:37f0420b14630b0eaaf452978f3a6ea4816d787c3e6dcbba6fb255030adae2e7" 334 | ], 335 | "version": "==2.0.0" 336 | }, 337 | "pydocstyle": { 338 | "hashes": [ 339 | "sha256:08a870edc94508264ed90510db466c6357c7192e0e866561d740624a8fc7d90c", 340 | "sha256:4d5bcde961107873bae621f3d580c3e35a426d3687ffc6f8fb356f6628da5a97", 341 | "sha256:af9fcccb303899b83bec82dc9a1d56c60fc369973223a5e80c3dfa9bdf984405" 342 | ], 343 | "version": "==2.1.1" 344 | }, 345 | "pyflakes": { 346 | "hashes": [ 347 | "sha256:08bd6a50edf8cffa9fa09a463063c425ecaaf10d1eb0335a7e8b1401aef89e6f", 348 | "sha256:8d616a382f243dbf19b54743f280b80198be0bca3a5396f1d2e1fca6223e8805" 349 | ], 350 | "version": "==1.6.0" 351 | }, 352 | "pygments": { 353 | "hashes": [ 354 | "sha256:78f3f434bcc5d6ee09020f92ba487f95ba50f1e3ef83ae96b9d5ffa1bab25c5d", 355 | "sha256:dbae1046def0efb574852fab9e90209b23f556367b5a320c0bcb871c77c3e8cc" 356 | ], 357 | "version": "==2.2.0" 358 | }, 359 | "pylint": { 360 | "hashes": [ 361 | "sha256:156839bedaa798febee72893beef00c650c2e7abafb5586fc7a6a56be7f80412", 362 | "sha256:4fe3b99da7e789545327b75548cee6b511e4faa98afe268130fea1af4b5ec022" 363 | ], 364 | "version": "==1.8.2" 365 | }, 366 | "pylint-celery": { 367 | "hashes": [ 368 | "sha256:41e32094e7408d15c044178ea828dd524beedbdbe6f83f712c5e35bde1de4beb" 369 | ], 370 | "version": "==0.3" 371 | }, 372 | "pylint-common": { 373 | "hashes": [ 374 | "sha256:3276b9e4db16f41cee656c78c74cfef3da383e8301e5b3b91146586ae5b53659" 375 | ], 376 | "version": "==0.2.5" 377 | }, 378 | "pylint-django": { 379 | "hashes": [ 380 | "sha256:80d42bf98002f2f452c644887375d9462317434ccf8d8bcaffad5ebd1cb5fab2", 381 | "sha256:caa87de582ac8d565ae020c780cb7cb8f117d903641edfde0c69faf994549b38" 382 | ], 383 | "version": "==0.9.1" 384 | }, 385 | "pylint-flask": { 386 | "hashes": [ 387 | "sha256:8fcdbb7cbf13d8c2ac1f2230b2aa1c1b83bb3ca2bd8b76f95561cb8757a305ec" 388 | ], 389 | "version": "==0.5" 390 | }, 391 | "pylint-plugin-utils": { 392 | "hashes": [ 393 | "sha256:053ade7c76f83242225b49d47624d9ecb803c60347e2c5127e97a19bf0c9f95e" 394 | ], 395 | "version": "==0.2.6" 396 | }, 397 | "pyyaml": { 398 | "hashes": [ 399 | "sha256:0c507b7f74b3d2dd4d1322ec8a94794927305ab4cebbe89cc47fe5e81541e6e8", 400 | "sha256:16b20e970597e051997d90dc2cddc713a2876c47e3d92d59ee198700c5427736", 401 | "sha256:3262c96a1ca437e7e4763e2843746588a965426550f3797a79fca9c6199c431f", 402 | "sha256:326420cbb492172dec84b0f65c80942de6cedb5233c413dd824483989c000608", 403 | "sha256:4474f8ea030b5127225b8894d626bb66c01cda098d47a2b0d3429b6700af9fd8", 404 | "sha256:592766c6303207a20efc445587778322d7f73b161bd994f227adaa341ba212ab", 405 | "sha256:5ac82e411044fb129bae5cfbeb3ba626acb2af31a8d17d175004b70862a741a7", 406 | "sha256:5f84523c076ad14ff5e6c037fe1c89a7f73a3e04cf0377cb4d017014976433f3", 407 | "sha256:827dc04b8fa7d07c44de11fabbc888e627fa8293b695e0f99cb544fdfa1bf0d1", 408 | "sha256:b4c423ab23291d3945ac61346feeb9a0dc4184999ede5e7c43e1ffb975130ae6", 409 | "sha256:bc6bced57f826ca7cb5125a10b23fd0f2fff3b7c4701d64c439a300ce665fff8", 410 | "sha256:c01b880ec30b5a6e6aa67b09a2fe3fb30473008c85cd6a67359a1b15ed6d83a4", 411 | "sha256:ca233c64c6e40eaa6c66ef97058cdc80e8d0157a443655baa1b2966e812807ca", 412 | "sha256:e863072cdf4c72eebf179342c94e6989c67185842d9997960b3e69290b2fa269" 413 | ], 414 | "version": "==3.12" 415 | }, 416 | "requirements-detector": { 417 | "hashes": [ 418 | "sha256:f790cc2d8a7084a5f111384a606363fb7b78d0e486d49b34c5093859eaa358f3" 419 | ], 420 | "version": "==0.5.2" 421 | }, 422 | "setoptconf": { 423 | "hashes": [ 424 | "sha256:5b0b5d8e0077713f5d5152d4f63be6f048d9a1bb66be15d089a11c898c3cf49c" 425 | ], 426 | "version": "==0.2.0" 427 | }, 428 | "simplegeneric": { 429 | "hashes": [ 430 | "sha256:dc972e06094b9af5b855b3df4a646395e43d1c9d0d39ed345b7393560d0b9173" 431 | ], 432 | "version": "==0.8.1" 433 | }, 434 | "singledispatch": { 435 | "hashes": [ 436 | "sha256:5b06af87df13818d14f08a028e42f566640aef80805c3b50c5056b086e3c2b9c", 437 | "sha256:833b46966687b3de7f438c761ac475213e53b306740f1abfaa86e1d1aae56aa8" 438 | ], 439 | "markers": "python_version < '3.4'", 440 | "version": "==3.4.0.3" 441 | }, 442 | "six": { 443 | "hashes": [ 444 | "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", 445 | "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" 446 | ], 447 | "version": "==1.11.0" 448 | }, 449 | "snowballstemmer": { 450 | "hashes": [ 451 | "sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128", 452 | "sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89" 453 | ], 454 | "version": "==1.2.1" 455 | }, 456 | "traitlets": { 457 | "hashes": [ 458 | "sha256:9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835", 459 | "sha256:c6cb5e6f57c5a9bdaa40fa71ce7b4af30298fbab9ece9815b5d995ab6217c7d9" 460 | ], 461 | "version": "==4.3.2" 462 | }, 463 | "wcwidth": { 464 | "hashes": [ 465 | "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", 466 | "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c" 467 | ], 468 | "version": "==0.1.7" 469 | }, 470 | "wrapt": { 471 | "hashes": [ 472 | "sha256:d4d560d479f2c21e1b5443bbd15fe7ec4b37fe7e53d335d3b9b0a7b1226fe3c6" 473 | ], 474 | "version": "==1.10.11" 475 | } 476 | } 477 | } 478 | --------------------------------------------------------------------------------