├── backend ├── api │ ├── __init__.py │ └── schema.py ├── assets │ └── .gitkeep ├── locale │ └── .gitkeep ├── pycon_ml │ ├── __init__.py │ ├── settings │ │ ├── __init__.py │ │ ├── prod.py │ │ ├── dev.py │ │ ├── test.py │ │ └── base.py │ ├── wsgi.py │ └── urls.py ├── tests │ ├── __init__.py │ ├── users │ │ ├── __init__.py │ │ ├── test_models.py │ │ └── test_managers.py │ ├── api │ │ ├── users │ │ │ ├── __init__.py │ │ │ ├── test_logout.py │ │ │ ├── test_me.py │ │ │ ├── test_login.py │ │ │ └── test_register.py │ │ ├── challenges │ │ │ ├── __init__.py │ │ │ ├── test_query_challenge_by_id.py │ │ │ ├── test_query_challenges.py │ │ │ ├── test_upload_submission.py │ │ │ ├── test_query_submissions.py │ │ │ └── test_query_leaderboard.py │ │ ├── fixtures.py │ │ ├── test_root.py │ │ └── utils.py │ └── conftest.py ├── users │ ├── __init__.py │ ├── api │ │ ├── __init__.py │ │ ├── schema.py │ │ ├── types.py │ │ └── mutations.py │ ├── migrations │ │ ├── __init__.py │ │ ├── 0002_user_is_staff.py │ │ ├── 0003_user_username.py │ │ ├── 0006_auto_20180410_1907.py │ │ ├── 0004_auto_20180408_2249.py │ │ ├── 0005_team.py │ │ └── 0001_initial.py │ ├── templates │ │ └── users │ │ │ ├── post-login.html │ │ │ └── test-login.html │ ├── forms.py │ ├── managers.py │ ├── models.py │ └── admin.py ├── challenges │ ├── __init__.py │ ├── api │ │ ├── __init__.py │ │ ├── types.py │ │ ├── mutations.py │ │ └── schema.py │ ├── migrations │ │ ├── __init__.py │ │ ├── 0004_auto_20180420_0626.py │ │ ├── 0005_auto_20180421_0728.py │ │ ├── 0001_initial.py │ │ ├── 0003_auto_20180420_0623.py │ │ └── 0002_submission.py │ ├── apps.py │ ├── admin.py │ ├── models.py │ └── forms.py ├── pytest.ini ├── .env.sample ├── manage.py ├── Pipfile ├── .editorconfig ├── .gitignore ├── Dockerfile └── Pipfile.lock ├── .dockerignore ├── frontend ├── .eslintrc ├── src │ ├── index.css │ ├── client │ │ └── index.js │ ├── index.js │ ├── authentication │ │ └── index.js │ ├── pages │ │ ├── login │ │ │ ├── index.js │ │ │ └── form.js │ │ ├── profile │ │ │ └── index.js │ │ ├── leaderboard │ │ │ └── index.js │ │ ├── submission │ │ │ ├── index.js │ │ │ └── form.js │ │ ├── register │ │ │ └── index.js │ │ └── home │ │ │ └── index.js │ ├── components │ │ ├── private-route │ │ │ └── index.js │ │ └── header │ │ │ └── index.js │ └── app.js ├── public │ ├── favicon.ico │ ├── data │ │ ├── variants.csv │ │ ├── variants_year.db │ │ └── description.txt │ ├── manifest.json │ └── index.html ├── Dockerfile ├── .gitignore └── package.json ├── nginx ├── Dockerfile └── nginx.conf ├── .gitignore ├── .env.sample ├── docker-compose.yml └── README.md /backend/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/locale/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /backend/pycon_ml/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/users/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/challenges/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/tests/users/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/users/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/challenges/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/pycon_ml/settings/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/tests/api/users/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/users/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/challenges/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/tests/api/challenges/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "react-app" 3 | } 4 | -------------------------------------------------------------------------------- /nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx 2 | 3 | COPY nginx.conf /etc/nginx/nginx.conf 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | .DS_Store 3 | score_calculator.py 4 | ground_truth.csv 5 | -------------------------------------------------------------------------------- /backend/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | DJANGO_SETTINGS_MODULE=pycon_ml.settings.test 3 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | DEBUG=False 2 | SECRET_KEY=change-this-in-prod 3 | ALLOWED_HOSTS=localhost,example -------------------------------------------------------------------------------- /backend/.env.sample: -------------------------------------------------------------------------------- 1 | SECRET_KEY=a!e(-@8mbndh=dontusethis$1#&!y9@*-+ett5#-p 2 | DEBUG=True 3 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /backend/pycon_ml/settings/prod.py: -------------------------------------------------------------------------------- 1 | from .base import * # noqa 2 | 3 | 4 | SECRET_KEY = env('SECRET_KEY') 5 | -------------------------------------------------------------------------------- /backend/pycon_ml/settings/dev.py: -------------------------------------------------------------------------------- 1 | from .base import * # noqa 2 | 3 | SECRET_KEY = 'do not use this in production' 4 | -------------------------------------------------------------------------------- /backend/pycon_ml/settings/test.py: -------------------------------------------------------------------------------- 1 | from .base import * # noqa 2 | 3 | SECRET_KEY = 'do not use this in production' 4 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/machine-learning-contest/master/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/src/client/index.js: -------------------------------------------------------------------------------- 1 | import ApolloClient from "apollo-boost"; 2 | 3 | export const client = new ApolloClient(); 4 | -------------------------------------------------------------------------------- /backend/challenges/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ChallengesConfig(AppConfig): 5 | name = 'challenges' 6 | -------------------------------------------------------------------------------- /frontend/public/data/variants.csv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/machine-learning-contest/master/frontend/public/data/variants.csv -------------------------------------------------------------------------------- /frontend/public/data/variants_year.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/machine-learning-contest/master/frontend/public/data/variants_year.db -------------------------------------------------------------------------------- /backend/challenges/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Challenge, Submission 4 | 5 | 6 | admin.site.register(Challenge) 7 | admin.site.register(Submission) 8 | -------------------------------------------------------------------------------- /backend/tests/api/fixtures.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture() 5 | def graphql_client(client): 6 | from .utils import GraphQLClient 7 | 8 | return GraphQLClient(client) 9 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './app'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); 7 | -------------------------------------------------------------------------------- /backend/tests/api/test_root.py: -------------------------------------------------------------------------------- 1 | def test_root(graphql_client): 2 | resp = graphql_client.query(''' 3 | { 4 | hello 5 | } 6 | ''') 7 | 8 | assert resp['data']['hello'] == "world" 9 | -------------------------------------------------------------------------------- /backend/pycon_ml/wsgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.core.wsgi import get_wsgi_application 4 | 5 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pycon_ml.settings.prod") 6 | 7 | application = get_wsgi_application() 8 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:9 2 | 3 | RUN curl -o- -L https://yarnpkg.com/install.sh | bash 4 | 5 | RUN mkdir /app 6 | 7 | COPY ./package.json ./yarn.* /tmp/ 8 | WORKDIR /tmp 9 | RUN yarn install 10 | WORKDIR /app 11 | RUN ln -s /tmp/node_modules . 12 | COPY . /app/ 13 | 14 | RUN yarn build 15 | -------------------------------------------------------------------------------- /backend/pycon_ml/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from django.contrib import admin 3 | from django.views.decorators.csrf import csrf_exempt 4 | 5 | from graphene_django.views import GraphQLView 6 | 7 | 8 | urlpatterns = [ 9 | path('admin/', admin.site.urls), 10 | path('graphql', csrf_exempt(GraphQLView.as_view(graphiql=True))), 11 | ] 12 | -------------------------------------------------------------------------------- /backend/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | import environ 6 | 7 | environ.Env.read_env() 8 | 9 | 10 | if __name__ == "__main__": 11 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pycon_ml.settings.prod") 12 | 13 | from django.core.management import execute_from_command_line 14 | 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /backend/users/templates/users/post-login.html: -------------------------------------------------------------------------------- 1 | {% comment %} TODO: design {% endcomment %} 2 |
3 | logging in... 4 |
5 | 6 | 16 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /backend/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | 3 | url = "https://pypi.python.org/simple" 4 | verify_ssl = true 5 | name = "pypi" 6 | 7 | 8 | [packages] 9 | 10 | django = "*" 11 | django-environ = "*" 12 | graphene = "*" 13 | graphene-django = "*" 14 | django-model-utils = "*" 15 | "psycopg2" = "*" 16 | uwsgi = "*" 17 | pandas = "*" 18 | sklearn = "*" 19 | scipy = "*" 20 | 21 | 22 | [dev-packages] 23 | 24 | pytest = "*" 25 | pytest-django = "*" 26 | "flake8" = "*" 27 | -------------------------------------------------------------------------------- /backend/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from users.models import User 4 | 5 | from .api.fixtures import * # noqa 6 | 7 | 8 | @pytest.fixture() 9 | def admin_user(db): 10 | user = User._default_manager.create_superuser( 11 | 'admin@example.com', 'password' 12 | ) 13 | 14 | return user 15 | 16 | 17 | @pytest.fixture() 18 | def user(db): 19 | user = User._default_manager.create_user('user@example.com', 'password') 20 | 21 | return user 22 | -------------------------------------------------------------------------------- /backend/challenges/migrations/0004_auto_20180420_0626.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.4 on 2018-04-20 06:26 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('challenges', '0003_auto_20180420_0623'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameField( 14 | model_name='submission', 15 | old_name='data', 16 | new_name='solution', 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | end_of_line = lf 9 | charset = utf-8 10 | 11 | [*.py] 12 | max_line_length = 119 13 | 14 | # Use 2 spaces for the HTML files 15 | [*.html] 16 | indent_size = 2 17 | 18 | # The JSON files contain newlines inconsistently 19 | [*.json] 20 | indent_size = 2 21 | 22 | # Makefiles always use tabs for indentation 23 | [Makefile] 24 | indent_style = tab 25 | -------------------------------------------------------------------------------- /backend/challenges/migrations/0005_auto_20180421_0728.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.4 on 2018-04-21 07:28 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('challenges', '0004_auto_20180420_0626'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='submission', 15 | name='score', 16 | field=models.CharField(editable=False, max_length=30), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | *.sw[po] 3 | .coverage 4 | .env 5 | *.orig 6 | .noseids 7 | *~ 8 | #* 9 | .#* 10 | *.egg-info 11 | *.sqlite3 12 | notes.org 13 | .DS_Store 14 | .sass-cache 15 | pip-log.txt 16 | __pycache__/ 17 | .cache/ 18 | .vagrant/ 19 | dist/ 20 | node_modules/ 21 | tmp/ 22 | tags 23 | secrets/ 24 | *.tfstate.backup 25 | media/*.jpg 26 | .elasticbeanstalk/ 27 | # Elastic Beanstalk Files 28 | .elasticbeanstalk/* 29 | !.elasticbeanstalk/*.cfg.yml 30 | !.elasticbeanstalk/*.global.yml 31 | !.elasticbeanstalk/config.yml 32 | .pytest_cache/ 33 | -------------------------------------------------------------------------------- /backend/users/api/schema.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | 3 | from .mutations import LoginMutation, LogoutMutation, RegisterMutation 4 | 5 | from .types import UserType 6 | 7 | 8 | class UserMutations(): 9 | login = LoginMutation.Field() 10 | logout = LogoutMutation.Field() 11 | register = RegisterMutation.Field() 12 | 13 | 14 | class UserQuery(): 15 | me = graphene.Field(UserType) 16 | 17 | def resolve_me(self, info): 18 | if not info.context.user.is_authenticated: 19 | raise ValueError('You are not logged in.') 20 | 21 | return info.context.user 22 | -------------------------------------------------------------------------------- /backend/users/migrations/0002_user_is_staff.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11 on 2017-07-15 14:22 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('users', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='user', 17 | name='is_staff', 18 | field=models.BooleanField(default=False, verbose_name='is staff'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /backend/api/schema.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | 3 | from users.api.schema import UserMutations, UserQuery 4 | from challenges.api.schema import ChallengeQuery, ChallengeMutations 5 | 6 | 7 | class Query( 8 | ChallengeQuery, 9 | UserQuery, 10 | graphene.ObjectType 11 | ): 12 | hello = graphene.String() 13 | 14 | def resolve_hello(self, info): 15 | return 'world' 16 | 17 | 18 | class Mutations( 19 | UserMutations, 20 | ChallengeMutations, 21 | graphene.ObjectType 22 | ): 23 | pass 24 | 25 | 26 | schema = graphene.Schema(query=Query, mutation=Mutations) 27 | -------------------------------------------------------------------------------- /backend/users/migrations/0003_user_username.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11 on 2017-07-22 16:11 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('users', '0002_user_is_staff'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='user', 17 | name='username', 18 | field=models.CharField(blank=True, max_length=100, null=True, verbose_name='username'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /backend/challenges/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.4 on 2018-04-10 19:07 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='Challenge', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('name', models.CharField(max_length=100)), 19 | ], 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /backend/users/api/types.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graphene_django.types import DjangoObjectType 3 | 4 | from ..models import User, Team 5 | 6 | 7 | class TeamType(DjangoObjectType): 8 | class Meta: 9 | model = Team 10 | only_fields = ('name', ) 11 | 12 | 13 | class UserType(DjangoObjectType): 14 | team = graphene.Field(TeamType) 15 | 16 | def resolve_team(self, context): 17 | try: 18 | return self.team 19 | except Team.DoesNotExist: 20 | return None 21 | 22 | class Meta: 23 | model = User 24 | only_fields = ('full_name', 'email') 25 | -------------------------------------------------------------------------------- /backend/users/migrations/0006_auto_20180410_1907.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.4 on 2018-04-10 19:07 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('users', '0005_team'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='team', 17 | name='owner', 18 | field=models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='team', to=settings.AUTH_USER_MODEL), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /backend/tests/api/users/test_logout.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.mark.django_db 5 | def test_correct_logout_removes_cookies_and_session( 6 | graphql_client, 7 | admin_user, 8 | ): 9 | graphql_client.client.force_login(admin_user) 10 | 11 | resp = graphql_client.query(''' 12 | mutation Logout { 13 | logout { 14 | ok 15 | } 16 | } 17 | ''') 18 | 19 | assert 'errors' not in resp 20 | 21 | assert resp['data']['logout']['ok'] 22 | 23 | cookies = graphql_client.client.cookies 24 | 25 | assert 'sessionid' in cookies 26 | assert cookies['sessionid'].value == '' 27 | -------------------------------------------------------------------------------- /backend/users/migrations/0004_auto_20180408_2249.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.4 on 2018-04-08 22:49 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('users', '0003_user_username'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='user', 15 | name='username', 16 | ), 17 | migrations.AddField( 18 | model_name='user', 19 | name='full_name', 20 | field=models.CharField(blank=True, max_length=200, null=True, verbose_name='full name'), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /backend/users/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from .models import User, Team 4 | 5 | 6 | class RegisterForm(forms.ModelForm): 7 | team_name = forms.CharField(max_length=200, required=False) 8 | 9 | class Meta: 10 | model = User 11 | fields = '__all__' 12 | 13 | def save(self, *args, **kwargs): 14 | obj = super().save(*args, **kwargs) 15 | 16 | obj.is_active = True 17 | obj.set_password(self.cleaned_data['password']) 18 | obj.save() 19 | 20 | if self.cleaned_data['team_name']: 21 | Team.objects.create(name=self.cleaned_data['team_name'], owner=obj) 22 | 23 | return obj 24 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | backend: 4 | build: backend 5 | depends_on: 6 | - postgres 7 | ports: 8 | - 8000 9 | environment: 10 | DATABASE_URL: postgres://pyconmlsite@postgres/pyconml 11 | SECRET_KEY: ${SECRET_KEY} 12 | ALLOWED_HOSTS: ${ALLOWED_HOSTS} 13 | DEBUG: ${DEBUG} 14 | 15 | volumes: 16 | - ./backend:/app 17 | 18 | postgres: 19 | image: postgres:9.6.2-alpine 20 | environment: 21 | POSTGRES_USER: pyconmlsite 22 | POSTGRES_DB: pyconml 23 | 24 | nginx: 25 | build: nginx 26 | volumes: 27 | - ./frontend/build:/app/build 28 | ports: 29 | - 80:80 30 | depends_on: 31 | - backend 32 | -------------------------------------------------------------------------------- /backend/challenges/migrations/0003_auto_20180420_0623.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.4 on 2018-04-20 06:23 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('challenges', '0002_submission'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='submission', 15 | name='code', 16 | field=models.TextField(default=''), 17 | preserve_default=False, 18 | ), 19 | migrations.AlterField( 20 | model_name='submission', 21 | name='score', 22 | field=models.CharField(editable=False, max_length=10), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /frontend/src/authentication/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import localforage from "localforage"; 3 | 4 | const KEY = "IS_AUTHENTICATED"; 5 | 6 | export const isAuthenticated = () => localforage.getItem(KEY); 7 | export const setIsAuthenticated = value => localforage.setItem(KEY, value); 8 | 9 | export class IsAuthenticated extends React.Component { 10 | state = { 11 | loading: true, 12 | isAuthenticated: null 13 | }; 14 | 15 | componentDidMount() { 16 | isAuthenticated().then(is => 17 | this.setState({ loading: false, isAuthenticated: is }) 18 | ); 19 | } 20 | 21 | render() { 22 | if (this.state.loading) { 23 | return null; 24 | } 25 | 26 | return this.props.children(this.state.isAuthenticated); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /backend/challenges/models.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.db import models 3 | 4 | from model_utils.models import TimeStampedModel 5 | 6 | 7 | class Challenge(models.Model): 8 | name = models.CharField(max_length=100) 9 | 10 | def __str__(self): 11 | return self.name 12 | 13 | 14 | class Submission(TimeStampedModel): 15 | solution = models.TextField() 16 | code = models.TextField() 17 | score = models.CharField(max_length=30, editable=False) 18 | submitted_by = models.ForeignKey( 19 | settings.AUTH_USER_MODEL, 20 | on_delete=models.PROTECT 21 | ) 22 | challenge = models.ForeignKey(Challenge, on_delete=models.PROTECT) 23 | 24 | def __str__(self): 25 | return f'Submission on {self.challenge} by {self.submitted_by}' 26 | -------------------------------------------------------------------------------- /backend/users/migrations/0005_team.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.4 on 2018-04-09 21:16 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('users', '0004_auto_20180408_2249'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Team', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('name', models.CharField(max_length=200)), 20 | ('owner', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), 21 | ], 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ml-submission-frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@material-ui/icons": "^1.0.0-beta.42", 7 | "apollo-boost": "^0.1.4", 8 | "date-fns": "^1.29.0", 9 | "graphql": "^0.13.2", 10 | "idx": "^2.2.0", 11 | "localforage": "^1.7.1", 12 | "material-ui": "^1.0.0-beta.40", 13 | "material-ui-icons": "^1.0.0-beta.36", 14 | "react": "^16.3.1", 15 | "react-apollo": "^2.1.3", 16 | "react-dom": "^16.3.1", 17 | "react-router": "^4.2.0", 18 | "react-router-dom": "^4.2.2", 19 | "react-scripts": "1.1.4" 20 | }, 21 | "scripts": { 22 | "start": "react-scripts start", 23 | "build": "react-scripts build", 24 | "test": "react-scripts test --env=jsdom", 25 | "eject": "react-scripts eject" 26 | }, 27 | "proxy": "http://localhost:8000" 28 | } 29 | -------------------------------------------------------------------------------- /backend/challenges/api/types.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graphene_django.types import DjangoObjectType 3 | 4 | from django.core.exceptions import ObjectDoesNotExist 5 | 6 | from ..models import Challenge, Submission 7 | 8 | 9 | class ChallengeType(DjangoObjectType): 10 | class Meta: 11 | model = Challenge 12 | only_fields = ('name', ) 13 | 14 | 15 | class LeaderboardSubmissionType(DjangoObjectType): 16 | name = graphene.String() 17 | 18 | class Meta: 19 | model = Submission 20 | only_fields = ('score', ) 21 | 22 | def resolve_name(root, info): 23 | user = root.submitted_by 24 | 25 | try: 26 | return user.team.name 27 | except ObjectDoesNotExist: 28 | pass 29 | 30 | return user.full_name 31 | 32 | 33 | class SubmissionType(DjangoObjectType): 34 | class Meta: 35 | model = Submission 36 | -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.6.4@sha256:bf0718e2882cabc144c9c07d758b09c65efc104a6ddc72a9a976f8b26f67c2ee 2 | 3 | RUN pip install pipenv 4 | 5 | # -- Install Application into container: 6 | RUN set -ex && mkdir /app 7 | 8 | WORKDIR /app 9 | 10 | # -- Adding Pipfiles 11 | COPY Pipfile Pipfile 12 | COPY Pipfile.lock Pipfile.lock 13 | 14 | RUN pipenv install --deploy --system 15 | 16 | # -- Install dependencies: 17 | COPY . /app/ 18 | 19 | # # uWSGI will listen on this port 20 | EXPOSE 8000 21 | 22 | ENV DJANGO_SETTINGS_MODULE=pycon_ml.settings.prod 23 | 24 | # # uWSGI configuration (customize as needed): 25 | ENV UWSGI_WSGI_FILE=pycon_ml/wsgi.py UWSGI_HTTP=:8000 UWSGI_MASTER=1 UWSGI_WORKERS=3 26 | ENV UWSGI_THREADS=10 UWSGI_UID=1000 UWSGI_GID=2000 UWSGI_LAZY_APPS=1 UWSGI_WSGI_ENV_BEHAVIOR=holy 27 | 28 | # # Start uWSGI 29 | CMD ["uwsgi", "--http-auto-chunked", "--http-keepalive", "--static-map", "/static=/app/static"] 30 | -------------------------------------------------------------------------------- /backend/tests/api/challenges/test_query_challenge_by_id.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from challenges.models import Challenge 4 | 5 | 6 | @pytest.mark.django_db 7 | def test_returns_none_when_no_challenge(graphql_client): 8 | resp = graphql_client.query(''' 9 | query { 10 | challenge(id: 999) { 11 | name 12 | } 13 | } 14 | ''') 15 | 16 | assert 'errors' not in resp 17 | 18 | assert resp['data']['challenge'] is None 19 | 20 | 21 | @pytest.mark.django_db 22 | def test_returns_challenge(graphql_client): 23 | challenge = Challenge.objects.create(name='demo') 24 | 25 | resp = graphql_client.query(f''' 26 | query {{ 27 | challenge(id: {challenge.id}) {{ 28 | name 29 | }} 30 | }} 31 | ''') 32 | 33 | assert 'errors' not in resp 34 | 35 | assert resp['data']['challenge']['name'] == 'demo' 36 | -------------------------------------------------------------------------------- /backend/tests/api/users/test_me.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.mark.django_db 5 | def test_returns_error_when_not_logged_in(graphql_client): 6 | resp = graphql_client.query(''' 7 | query { 8 | me { 9 | fullName 10 | } 11 | } 12 | ''') 13 | 14 | assert 'errors' in resp 15 | 16 | assert resp['errors'][0]['message'] == 'You are not logged in.' 17 | 18 | 19 | @pytest.mark.django_db 20 | def test_returns_user_when_logged_in(graphql_client, user): 21 | user.full_name = 'Polly' 22 | user.save() 23 | graphql_client.client.force_login(user) 24 | 25 | resp = graphql_client.query(''' 26 | query { 27 | me { 28 | fullName 29 | } 30 | } 31 | ''') 32 | 33 | assert 'errors' not in resp 34 | 35 | assert resp['data']['me'] 36 | assert resp['data']['me']['fullName'] == 'Polly' 37 | -------------------------------------------------------------------------------- /backend/tests/api/challenges/test_query_challenges.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from challenges.models import Challenge 4 | 5 | 6 | @pytest.mark.django_db 7 | def test_returns_empty_when_no_challenges(graphql_client): 8 | resp = graphql_client.query(''' 9 | query { 10 | challenges { 11 | name 12 | } 13 | } 14 | ''') 15 | 16 | assert 'errors' not in resp 17 | 18 | assert resp['data']['challenges'] == [] 19 | 20 | 21 | @pytest.mark.django_db 22 | def test_returns_challenges(graphql_client): 23 | Challenge.objects.create(name='demo') 24 | 25 | resp = graphql_client.query(''' 26 | query { 27 | challenges { 28 | name 29 | } 30 | } 31 | ''') 32 | 33 | assert 'errors' not in resp 34 | 35 | assert len(resp['data']['challenges']) == 1 36 | assert resp['data']['challenges'][0]['name'] == 'demo' 37 | -------------------------------------------------------------------------------- /frontend/src/pages/login/index.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react"; 2 | import { withStyles } from "material-ui/styles"; 3 | import Typography from "material-ui/Typography"; 4 | import LoginForm from "./form"; 5 | 6 | import { Link } from "react-router-dom"; 7 | 8 | const styles = theme => ({}); 9 | 10 | class Login extends PureComponent { 11 | state = {}; 12 | 13 | handleChange = name => event => { 14 | this.setState({ 15 | [name]: event.target.value 16 | }); 17 | }; 18 | 19 | render() { 20 | return ( 21 |
22 | 23 | Login 24 | 25 | 26 | 27 | Not registered? Register here 28 | 29 | 30 | 31 |
32 | ); 33 | } 34 | } 35 | 36 | export default withStyles(styles)(Login); 37 | -------------------------------------------------------------------------------- /backend/tests/users/test_models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import authenticate, get_user_model 2 | 3 | import pytest 4 | from users.models import User 5 | from users.managers import UserManager 6 | 7 | 8 | def test_that_get_user_model_returns_correct_model(): 9 | assert get_user_model() == User 10 | 11 | @pytest.mark.django_db 12 | def test_user_manager(): 13 | assert isinstance(User.objects, UserManager) 14 | 15 | 16 | @pytest.mark.django_db 17 | def test_create_user_and_authenticate(): 18 | user = User.objects.create_user('lennon@thebeatles.com', 'johnpassword') 19 | 20 | authenticated_user = authenticate( 21 | username='lennon@thebeatles.com', 22 | password='johnpassword' 23 | ) 24 | 25 | assert user == authenticated_user 26 | 27 | @pytest.mark.django_db 28 | def test_user_get_short_name(): 29 | user = User.objects.create_user('lennon@thebeatles.com', 'johnpassword') 30 | 31 | assert 'lennon@thebeatles.com' == user.get_short_name() 32 | -------------------------------------------------------------------------------- /frontend/src/components/private-route/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { Route, Redirect } from "react-router"; 4 | import { isAuthenticated } from "../../authentication"; 5 | 6 | export class PrivateRoute extends React.Component { 7 | state = { 8 | isAuthenticated: null 9 | }; 10 | 11 | componentDidMount() { 12 | isAuthenticated().then(authenticated => 13 | this.setState({ 14 | isAuthenticated: !!authenticated 15 | }) 16 | ); 17 | } 18 | 19 | render() { 20 | const { component: Component, ...rest } = this.props; 21 | 22 | if (this.state.isAuthenticated === null) { 23 | return null; 24 | } 25 | 26 | return ( 27 | 30 | this.state.isAuthenticated ? ( 31 | 32 | ) : ( 33 | 39 | ) 40 | } 41 | /> 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /backend/users/managers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.base_user import BaseUserManager 2 | 3 | 4 | class UserManager(BaseUserManager): 5 | use_in_migrations = True 6 | 7 | def _create_user(self, email, password, **extra_fields): 8 | if not email: 9 | raise ValueError('The given email must be set') 10 | 11 | email = self.normalize_email(email) 12 | user = self.model(email=email, **extra_fields) 13 | user.set_password(password) 14 | user.save(using=self._db) 15 | 16 | return user 17 | 18 | def create_user(self, email, password=None, **extra_fields): 19 | extra_fields.setdefault('is_superuser', False) 20 | return self._create_user(email, password, **extra_fields) 21 | 22 | def create_superuser(self, email, password, **extra_fields): 23 | extra_fields.setdefault('is_superuser', True) 24 | extra_fields.setdefault('is_staff', True) 25 | 26 | if extra_fields.get('is_superuser') is not True: 27 | raise ValueError('Superuser must have is_superuser=True.') 28 | 29 | return self._create_user(email, password, **extra_fields) 30 | -------------------------------------------------------------------------------- /backend/challenges/forms.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | 3 | from django import forms 4 | 5 | from .models import Submission 6 | 7 | # try: 8 | from .score_calculator import calculate_score 9 | # except ImportError: 10 | # def calculate_score(*args): 11 | # return 1 12 | 13 | 14 | def save_to_tmp(content): 15 | with tempfile.NamedTemporaryFile('w', suffix='.csv', delete=False) as tmp: 16 | tmp.write(content) 17 | 18 | return tmp.name 19 | 20 | 21 | class SubmissionForm(forms.ModelForm): 22 | class Meta: 23 | model = Submission 24 | fields = ['code', 'solution', 'challenge'] 25 | 26 | def clean_solution(self): 27 | path = save_to_tmp(self.cleaned_data['solution']) 28 | 29 | try: 30 | self.score = calculate_score(path) 31 | except Exception as error: 32 | raise forms.ValidationError(error) 33 | 34 | return self.cleaned_data['solution'] 35 | 36 | def save(self, user): 37 | submission = super().save(commit=False) 38 | submission.submitted_by = user 39 | submission.score = self.score 40 | 41 | submission.save() 42 | 43 | return submission 44 | -------------------------------------------------------------------------------- /backend/users/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from .managers import UserManager 4 | 5 | from django.utils.translation import ugettext_lazy as _ 6 | 7 | from django.contrib.auth.models import PermissionsMixin 8 | from django.contrib.auth.base_user import AbstractBaseUser 9 | 10 | 11 | class User(AbstractBaseUser, PermissionsMixin): 12 | full_name = models.CharField( 13 | _('full name'), 14 | max_length=200, 15 | null=True, 16 | blank=True, 17 | ) 18 | email = models.EmailField(_('email address'), unique=True) 19 | 20 | date_joined = models.DateTimeField(_('date joined'), auto_now_add=True) 21 | is_active = models.BooleanField(_('active'), default=True) 22 | is_staff = models.BooleanField(_('is staff'), default=False) 23 | 24 | objects = UserManager() 25 | 26 | USERNAME_FIELD = 'email' 27 | REQUIRED_FIELDS = [] 28 | 29 | objects = UserManager() 30 | 31 | def get_short_name(self): 32 | return self.email 33 | 34 | 35 | class Team(models.Model): 36 | name = models.CharField(max_length=200) 37 | owner = models.OneToOneField( 38 | User, 39 | on_delete=models.PROTECT, 40 | related_name='team' 41 | ) 42 | -------------------------------------------------------------------------------- /backend/tests/api/utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | class GraphQLClient(): 5 | def __init__(self, client): 6 | self.client = client 7 | 8 | def query(self, query, op_name=None, variables=None): 9 | """ 10 | Args: 11 | query (string) - GraphQL query to run 12 | op_name (string) - If the query is a mutation or named query, you 13 | must supply the op_name. For annon queries 14 | ("{ ... }"), should be None (default). 15 | variables (dict) - If provided, the variables in GraphQL will be 16 | set to this value 17 | Returns: 18 | dict, response from graphql endpoint. 19 | The response has the "data" key. 20 | It will have the "error" key if any error happened. 21 | """ 22 | body = {'query': query} 23 | if op_name: 24 | body['operation_name'] = op_name 25 | if variables: 26 | body['variables'] = variables 27 | 28 | resp = self.client.post( 29 | '/graphql', json.dumps(body), content_type='application/json' 30 | ) 31 | 32 | return json.loads(resp.content.decode()) 33 | -------------------------------------------------------------------------------- /frontend/src/pages/profile/index.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react"; 2 | import { withStyles } from "material-ui/styles"; 3 | import Typography from "material-ui/Typography"; 4 | 5 | import gql from "graphql-tag"; 6 | import { Query } from "react-apollo"; 7 | 8 | const QUERY = gql` 9 | query { 10 | me { 11 | fullName 12 | } 13 | } 14 | `; 15 | 16 | const styles = theme => ({}); 17 | 18 | class Login extends PureComponent { 19 | state = {}; 20 | 21 | handleChange = name => event => { 22 | this.setState({ 23 | [name]: event.target.value 24 | }); 25 | }; 26 | 27 | render() { 28 | return ( 29 |
30 | 31 | Profile 32 | 33 | 34 | 35 | {({ data, loading, error }) => { 36 | if (loading) { 37 | return

Loading

; 38 | } 39 | 40 | return ( 41 | 42 |
Hello {data.me.fullName || "you"}!
43 |
44 | ); 45 | }} 46 |
47 |
48 | ); 49 | } 50 | } 51 | 52 | export default withStyles(styles)(Login); 53 | -------------------------------------------------------------------------------- /backend/users/templates/users/test-login.html: -------------------------------------------------------------------------------- 1 |
2 | post login page 3 | login via twitter 4 |
5 | 6 | 38 | -------------------------------------------------------------------------------- /nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | events { 2 | worker_connections 1024; 3 | } 4 | 5 | http { 6 | include /etc/nginx/mime.types; 7 | default_type application/octet-stream; 8 | charset utf-8; 9 | keepalive_timeout 65; 10 | server_tokens off; 11 | tcp_nopush on; 12 | tcp_nodelay off; 13 | 14 | server { 15 | root /app/build; 16 | index index.html index.htm; 17 | client_max_body_size 128m; 18 | 19 | location ~* \.(?:manifest|appcache|html?|xml|json)$ { 20 | expires -1; 21 | } 22 | 23 | 24 | location ~ ^/(graphql|admin|static/admin) { 25 | proxy_set_header Host $host; 26 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 27 | 28 | proxy_redirect off; 29 | proxy_pass http://backend:8000; 30 | } 31 | 32 | location ~* \.(?:css|js)$ { 33 | try_files $uri =404; 34 | expires 1y; 35 | access_log off; 36 | add_header Cache-Control "public"; 37 | } 38 | 39 | # Any route containing a file extension (e.g. /devicesfile.js) 40 | location ~ ^.+\..+$ { 41 | try_files $uri =404; 42 | } 43 | 44 | # Any route that doesn't have a file extension (e.g. /devices) 45 | location / { 46 | try_files $uri $uri/ /index.html; 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /backend/challenges/migrations/0002_submission.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.4 on 2018-04-11 12:06 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | import django.utils.timezone 7 | import model_utils.fields 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ('challenges', '0001_initial'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='Submission', 20 | fields=[ 21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), 23 | ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), 24 | ('data', models.TextField()), 25 | ('score', models.CharField(max_length=10)), 26 | ('challenge', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='challenges.Challenge')), 27 | ('submitted_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), 28 | ], 29 | options={ 30 | 'abstract': False, 31 | }, 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /backend/challenges/api/mutations.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | 3 | from ..forms import SubmissionForm 4 | 5 | 6 | class UploadSubmissionInput(graphene.InputObjectType): 7 | challenge = graphene.Int(required=True) 8 | code = graphene.String(required=True) 9 | solution = graphene.String(required=True) 10 | 11 | 12 | class UploadSubmissionErrors(graphene.ObjectType): 13 | challenge = graphene.List(graphene.NonNull(graphene.String)) 14 | code = graphene.List(graphene.NonNull(graphene.String)) 15 | solution = graphene.List(graphene.NonNull(graphene.String)) 16 | 17 | 18 | class UploadSubmissionMutation(graphene.Mutation): 19 | ok = graphene.Boolean() 20 | result = graphene.String() 21 | errors = graphene.Field(UploadSubmissionErrors, required=True) 22 | 23 | class Arguments: 24 | input = UploadSubmissionInput(required=True) 25 | 26 | @classmethod 27 | def mutate(cls, root, info, input): 28 | if not info.context.user.is_authenticated: 29 | raise ValueError('You are not logged in.') 30 | 31 | form = SubmissionForm(data=input) 32 | 33 | if form.is_valid(): 34 | submission = form.save(info.context.user) 35 | 36 | return cls( 37 | ok=True, 38 | result=submission.score, 39 | errors=UploadSubmissionErrors() 40 | ) 41 | 42 | return cls( 43 | ok=False, 44 | result=None, 45 | errors=UploadSubmissionErrors(**form.errors) 46 | ) 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyCon Nove ML Contest 2 | 3 | This is the source code of the app used for the Machine Learning contest 4 | that will be launched during the PyCon Nove Conference. 5 | 6 | ## Backend 7 | 8 | The backend is a Python application using Django and Graphene. 9 | 10 | ## Frontend 11 | 12 | The frontend is a single page application built with React and Material UI. 13 | 14 | ## How it works - Registration and Teams 15 | 16 | Users can login and create their team. A team can be formed by one or more 17 | people. You will always be in the team you have created. One user can only be 18 | in one single team. 19 | 20 | ### User Model 21 | 22 | - email 23 | - password 24 | 25 | ### Team Model 26 | 27 | - name 28 | - owner (One to One to user) 29 | - members (list of emails, excluding the current user) 30 | 31 | Instead of asking all the user to register we ask the team owner to provider 32 | their emails. Again, one user can only be in one team and no superuser can 33 | be in a team, since they have access to the admin. 34 | 35 | ## How it works - Challenges 36 | 37 | Each team can partecipate in one or more challenges. The challenges might be 38 | unlocked by a code (TBD). 39 | 40 | ### Challenge Model 41 | 42 | - name 43 | - description 44 | - data (url to dataset) 45 | - unlock_code (?) 46 | - solution (needed to validate a submission) 47 | 48 | A user can submit a solution more than once and they will get instant 49 | feedback on their result. 50 | 51 | ### Solution Model 52 | 53 | - team 54 | - created 55 | - score 56 | - submitted_data -------------------------------------------------------------------------------- /frontend/src/pages/leaderboard/index.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react"; 2 | import { withStyles } from "material-ui/styles"; 3 | import Typography from "material-ui/Typography"; 4 | import { ListItem, ListItemIcon, ListItemText } from "material-ui/List"; 5 | import GradeIcon from "material-ui-icons/Grade"; 6 | 7 | import { gql } from "apollo-boost"; 8 | import { Query } from "react-apollo"; 9 | 10 | const styles = theme => ({}); 11 | 12 | const LEADERBOARD = gql` 13 | query { 14 | leaderboard { 15 | name 16 | score 17 | } 18 | } 19 | `; 20 | 21 | class Leaderboard extends PureComponent { 22 | render() { 23 | return ( 24 |
25 | 26 | Leaderboard 27 | 28 | 29 | 30 | {({ loading, error, data }) => { 31 | if (loading) return
Loading...
; 32 | if (error) return
Error :(
; 33 | if (data.leaderboard.length === 0) 34 | return
No submission yet
; 35 | 36 | return data.leaderboard.map(x => ( 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | )); 45 | }} 46 |
47 |
48 | ); 49 | } 50 | } 51 | 52 | export default withStyles(styles)(Leaderboard); 53 | -------------------------------------------------------------------------------- /backend/challenges/api/schema.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | 3 | from ..models import Challenge, Submission 4 | 5 | from .types import ChallengeType, LeaderboardSubmissionType, SubmissionType 6 | from .mutations import UploadSubmissionMutation 7 | 8 | 9 | class ChallengeQuery(): 10 | challenge = graphene.Field(ChallengeType, id=graphene.ID()) 11 | challenges = graphene.List(graphene.NonNull(ChallengeType)) 12 | leaderboard = graphene.List(graphene.NonNull(LeaderboardSubmissionType)) 13 | my_submissions = graphene.List(graphene.NonNull(SubmissionType)) 14 | 15 | def resolve_challenges(self, info): 16 | return Challenge.objects.all() 17 | 18 | def resolve_challenge(self, info, id): 19 | try: 20 | return Challenge.objects.get(pk=id) 21 | except Challenge.DoesNotExist: 22 | return None 23 | 24 | def resolve_leaderboard(self, info): 25 | all_submissions = Submission.objects.order_by('-score') 26 | leaderboard = [] 27 | 28 | seen_user = set() 29 | 30 | for submission in all_submissions: 31 | user = submission.submitted_by 32 | 33 | if user.pk not in seen_user: 34 | leaderboard.append(submission) 35 | 36 | seen_user.add(user.pk) 37 | 38 | return leaderboard 39 | 40 | def resolve_my_submissions(self, info): 41 | if not info.context.user.is_authenticated: 42 | return Submission.objects.none() 43 | 44 | return Submission.objects.order_by('-created').filter( 45 | submitted_by=info.context.user 46 | ) 47 | 48 | 49 | class ChallengeMutations(): 50 | upload_submission = UploadSubmissionMutation.Field() 51 | -------------------------------------------------------------------------------- /backend/tests/users/test_managers.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from users.models import User 4 | from users.managers import UserManager 5 | 6 | 7 | @pytest.mark.django_db 8 | def test_create_user_empty_email(): 9 | with pytest.raises(ValueError) as error: 10 | UserManager().create_user('', 'johnpassword') 11 | 12 | assert 'The given email must be set' in str(error.value) 13 | 14 | 15 | @pytest.mark.django_db 16 | def test_create_user_with_email_and_password(): 17 | user = User.objects.create_user('lennon@thebeatles.com', 'johnpassword') 18 | 19 | assert user.email == 'lennon@thebeatles.com' 20 | assert not user.is_superuser 21 | assert not user.is_staff 22 | 23 | 24 | @pytest.mark.django_db 25 | def test_create_user_superuser_with_email_and_password(): 26 | user = User.objects.create_user( 27 | 'lennon@thebeatles.com', 28 | 'johnpassword', 29 | is_superuser=True 30 | ) 31 | 32 | assert user.email == 'lennon@thebeatles.com' 33 | assert user.is_superuser 34 | assert not user.is_staff 35 | 36 | 37 | @pytest.mark.django_db 38 | def test_cannot_create_superuser_not_superuser_flag(): 39 | with pytest.raises(ValueError) as error: 40 | User.objects.create_superuser( 41 | 'lennon@thebeatles.com', 42 | 'johnpassword', 43 | is_superuser=False 44 | ) 45 | 46 | assert 'Superuser must have is_superuser=True.' in str(error.value) 47 | 48 | 49 | @pytest.mark.django_db 50 | def test_create_superuser_with_email_and_password(): 51 | user = User.objects.create_superuser( 52 | 'lennon@thebeatles.com', 53 | 'johnpassword' 54 | ) 55 | 56 | assert user.email == 'lennon@thebeatles.com' 57 | assert user.is_staff 58 | assert user.is_superuser 59 | -------------------------------------------------------------------------------- /backend/tests/api/challenges/test_upload_submission.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from challenges.models import Challenge, Submission 4 | 5 | 6 | @pytest.mark.django_db 7 | def test_does_not_work_when_not_logged_in(graphql_client): 8 | challenge = Challenge.objects.create(name='demo') 9 | 10 | resp = graphql_client.query(''' 11 | mutation UploadSubmission($input: UploadSubmissionInput!) { 12 | uploadSubmission(input: $input) { 13 | ok 14 | result 15 | } 16 | } 17 | ''', variables={ 18 | 'input': { 19 | 'challenge': challenge.pk, 20 | 'code': 'example code', 21 | 'solution': 'example data', 22 | } 23 | }) 24 | 25 | assert 'errors' in resp 26 | 27 | assert resp['errors'][0]['message'] == 'You are not logged in.' 28 | 29 | 30 | # TODO: "404s" 31 | 32 | @pytest.mark.django_db 33 | def test_works_when_logged_in(graphql_client, user): 34 | challenge = Challenge.objects.create(name='demo') 35 | graphql_client.client.force_login(user) 36 | 37 | resp = graphql_client.query(''' 38 | mutation UploadSubmission($input: UploadSubmissionInput!) { 39 | uploadSubmission(input: $input) { 40 | ok 41 | result 42 | } 43 | } 44 | ''', variables={ 45 | 'input': { 46 | 'challenge': challenge.pk, 47 | 'code': 'example code', 48 | 'solution': 'example data', 49 | } 50 | }) 51 | 52 | assert 'errors' not in resp 53 | 54 | assert Submission.objects.count() == 1 55 | 56 | # TODO: test score 57 | submission = Submission.objects.first() 58 | 59 | assert submission.submitted_by == user 60 | assert submission.challenge == challenge 61 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 14 | 15 | 24 | PyData Challenge 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /backend/tests/api/users/test_login.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from django.contrib.sessions.models import Session 4 | 5 | 6 | @pytest.mark.django_db 7 | def test_correct_login_adds_cookie(graphql_client, admin_user): 8 | resp = graphql_client.query(''' 9 | mutation Login($username: String!, $password: String!) { 10 | login(username: $username, password: $password) { 11 | ok 12 | errors { 13 | nonFieldErrors 14 | } 15 | } 16 | } 17 | ''', variables={ 18 | 'username': admin_user.email, 19 | 'password': 'password', 20 | }) 21 | 22 | assert 'errors' not in resp 23 | assert not resp['data']['login']['errors']['nonFieldErrors'] 24 | 25 | assert resp['data']['login']['ok'] 26 | 27 | cookies = graphql_client.client.cookies 28 | 29 | assert 'sessionid' in cookies 30 | 31 | session = Session.objects.get(session_key=cookies['sessionid'].value) 32 | uid = session.get_decoded().get('_auth_user_id') 33 | 34 | assert uid == str(admin_user.id) 35 | 36 | 37 | @pytest.mark.django_db 38 | def test_wrong_email_returns_error(graphql_client, admin_user): 39 | resp = graphql_client.query(''' 40 | mutation Login($username: String!, $password: String!) { 41 | login(username: $username, password: $password) { 42 | ok 43 | errors { 44 | nonFieldErrors 45 | } 46 | } 47 | } 48 | ''', variables={ 49 | 'username': 'wrong-email', 50 | 'password': 'password', 51 | }) 52 | 53 | assert not resp['data']['login']['ok'] 54 | assert resp['data']['login']['errors']['nonFieldErrors'] 55 | 56 | errors = resp['data']['login']['errors'] 57 | 58 | assert ( 59 | errors['nonFieldErrors'] == [ 60 | 'Unable to log in with provided credentials' 61 | ] 62 | ) 63 | 64 | cookies = graphql_client.client.cookies 65 | 66 | assert 'sessionid' not in cookies 67 | -------------------------------------------------------------------------------- /frontend/src/app.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { MuiThemeProvider, createMuiTheme } from "material-ui/styles"; 3 | import Grid from "material-ui/Grid"; 4 | 5 | import { ApolloProvider } from "react-apollo"; 6 | 7 | import { Switch, Route } from "react-router"; 8 | 9 | import { BrowserRouter } from "react-router-dom"; 10 | 11 | import Header from "./components/header"; 12 | 13 | import HomePage from "./pages/home"; 14 | import RegisterPage from "./pages/register"; 15 | import SubmissionPage from "./pages/submission"; 16 | import LeaderboardPage from "./pages/leaderboard"; 17 | import LoginPage from "./pages/login"; 18 | import ProfilePage from "./pages/profile"; 19 | 20 | import { client } from "./client"; 21 | import { PrivateRoute } from "./components/private-route"; 22 | 23 | const theme = createMuiTheme({ 24 | typography: { 25 | fontSize: 15 26 | } 27 | }); 28 | 29 | const App = () => ( 30 | 31 | 32 | 33 |
34 | 39 | 40 | 41 | 42 | 43 | 44 | 49 | 50 | 51 |

Not Found

} /> 52 |
53 |
54 |
55 | 56 | 57 | 58 | ); 59 | 60 | export default App; 61 | -------------------------------------------------------------------------------- /backend/users/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11 on 2017-07-15 14:09 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import users.managers 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ('auth', '0008_alter_user_username_max_length'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='User', 20 | fields=[ 21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('password', models.CharField(max_length=128, verbose_name='password')), 23 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 24 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 25 | ('email', models.EmailField(max_length=254, unique=True, verbose_name='email address')), 26 | ('date_joined', models.DateTimeField(auto_now_add=True, verbose_name='date joined')), 27 | ('is_active', models.BooleanField(default=True, verbose_name='active')), 28 | ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), 29 | ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), 30 | ], 31 | options={ 32 | 'abstract': False, 33 | }, 34 | managers=[ 35 | ('objects', users.managers.UserManager()), 36 | ], 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /backend/users/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth.admin import UserAdmin 3 | from django.contrib.auth.forms import UserChangeForm, UserCreationForm 4 | from django.utils.translation import ugettext_lazy as _ 5 | 6 | from .models import User 7 | 8 | 9 | class PyConUserCreationForm(UserCreationForm): 10 | """ 11 | A form that creates a user, with no privileges, from the given email and 12 | password. 13 | """ 14 | 15 | class Meta: 16 | model = User 17 | fields = ('email', ) 18 | 19 | 20 | class PyConUserChangeForm(UserChangeForm): 21 | """A form for updating users. Includes all the fields on 22 | the user, but replaces the password field with admin's 23 | password hash display field. 24 | """ 25 | 26 | class Meta: 27 | model = User 28 | fields = '__all__' 29 | 30 | 31 | class PyConUserAdmin(UserAdmin): 32 | # The forms to add and change user instances 33 | 34 | # The fields to be used in displaying the User model. 35 | # These override the definitions on the base UserAdmin 36 | # that reference the removed 'username' field 37 | fieldsets = ( 38 | (None, { 39 | 'fields': ('email', 'password') 40 | }), 41 | (_('Permissions'), { 42 | 'fields': ( 43 | 'is_active', 44 | 'is_staff', 45 | 'is_superuser', 46 | 'groups', 47 | 'user_permissions', 48 | ) 49 | }), 50 | (_('Important dates'), { 51 | 'fields': ('last_login', 'date_joined') 52 | }), 53 | ) 54 | 55 | add_fieldsets = ((None, { 56 | 'classes': ('wide', ), 57 | 'fields': ('email', 'password1', 'password2') 58 | }), ) 59 | 60 | form = PyConUserChangeForm 61 | add_form = PyConUserCreationForm 62 | readonly_fields = ('date_joined', ) 63 | list_display = ('email', 'is_staff', 'is_superuser') 64 | search_fields = ('email', ) 65 | ordering = ('email', ) 66 | 67 | 68 | admin.site.register(User, PyConUserAdmin) 69 | -------------------------------------------------------------------------------- /backend/tests/api/challenges/test_query_submissions.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from challenges.models import Submission, Challenge 4 | from users.models import User 5 | 6 | 7 | @pytest.mark.django_db 8 | def test_returns_empty_when_no_submissions(graphql_client): 9 | resp = graphql_client.query(''' 10 | query { 11 | mySubmissions { 12 | score 13 | } 14 | } 15 | ''') 16 | 17 | assert 'errors' not in resp 18 | 19 | assert resp['data']['mySubmissions'] == [] 20 | 21 | 22 | @pytest.mark.django_db 23 | def test_returns_submissions(graphql_client, user): 24 | challenge = Challenge.objects.create(name='demo') 25 | 26 | Submission.objects.create( 27 | solution='abc', 28 | code='abc', 29 | score=123, 30 | submitted_by=user, 31 | challenge=challenge, 32 | ) 33 | 34 | graphql_client.client.force_login(user) 35 | 36 | resp = graphql_client.query(''' 37 | query { 38 | mySubmissions { 39 | score 40 | } 41 | } 42 | ''') 43 | 44 | assert 'errors' not in resp 45 | 46 | assert len(resp['data']['mySubmissions']) == 1 47 | assert resp['data']['mySubmissions'][0]['score'] == '123' 48 | 49 | 50 | @pytest.mark.django_db 51 | def test_returns_user_submissions(graphql_client, user): 52 | challenge = Challenge.objects.create(name='demo') 53 | 54 | b = User.objects.create(full_name='example') 55 | 56 | Submission.objects.create( 57 | solution='abc', 58 | code='abc', 59 | score=123, 60 | submitted_by=user, 61 | challenge=challenge, 62 | ) 63 | 64 | Submission.objects.create( 65 | solution='abc', 66 | code='abc', 67 | score=777, 68 | submitted_by=b, 69 | challenge=challenge, 70 | ) 71 | 72 | graphql_client.client.force_login(user) 73 | 74 | resp = graphql_client.query(''' 75 | query { 76 | mySubmissions { 77 | score 78 | } 79 | } 80 | ''') 81 | 82 | assert 'errors' not in resp 83 | 84 | assert len(resp['data']['mySubmissions']) == 1 85 | assert resp['data']['mySubmissions'][0]['score'] == '123' 86 | -------------------------------------------------------------------------------- /backend/tests/api/challenges/test_query_leaderboard.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from users.models import Team 4 | from challenges.models import Submission, Challenge 5 | 6 | 7 | @pytest.mark.django_db 8 | def test_returns_empty_when_no_submissions(graphql_client): 9 | resp = graphql_client.query(''' 10 | query { 11 | leaderboard { 12 | name 13 | score 14 | } 15 | } 16 | ''') 17 | 18 | assert 'errors' not in resp 19 | 20 | assert resp['data']['leaderboard'] == [] 21 | 22 | 23 | @pytest.mark.django_db 24 | def test_returns_submissions(graphql_client, user): 25 | challenge = Challenge.objects.create(name='demo') 26 | user.full_name = 'Patrick' 27 | user.save() 28 | 29 | Submission.objects.create( 30 | solution='abc', 31 | code='abc', 32 | score=123, 33 | submitted_by=user, 34 | challenge=challenge, 35 | ) 36 | 37 | resp = graphql_client.query(''' 38 | query { 39 | leaderboard { 40 | name 41 | score 42 | } 43 | } 44 | ''') 45 | 46 | assert 'errors' not in resp 47 | 48 | assert len(resp['data']['leaderboard']) == 1 49 | assert resp['data']['leaderboard'][0]['name'] == user.full_name 50 | 51 | 52 | @pytest.mark.django_db 53 | def test_returns_team_name_submissions(graphql_client, user): 54 | challenge = Challenge.objects.create(name='demo') 55 | user.full_name = 'Patrick' 56 | user.save() 57 | team = Team.objects.create(name='broccoletti', owner=user) 58 | 59 | Submission.objects.create( 60 | solution='abc', 61 | code='abc', 62 | score=123, 63 | submitted_by=user, 64 | challenge=challenge, 65 | ) 66 | 67 | resp = graphql_client.query(''' 68 | query { 69 | leaderboard { 70 | name 71 | score 72 | } 73 | } 74 | ''') 75 | 76 | assert 'errors' not in resp 77 | 78 | assert len(resp['data']['leaderboard']) == 1 79 | assert resp['data']['leaderboard'][0]['name'] == team.name 80 | 81 | 82 | @pytest.mark.django_db 83 | def test_returns_single_per_user_submissions(graphql_client, user): 84 | challenge = Challenge.objects.create(name='demo') 85 | user.full_name = 'Patrick' 86 | user.save() 87 | 88 | Submission.objects.create( 89 | solution='abc', 90 | code='abc', 91 | score=123, 92 | submitted_by=user, 93 | challenge=challenge, 94 | ) 95 | 96 | Submission.objects.create( 97 | solution='abc', 98 | code='abc', 99 | score=256, 100 | submitted_by=user, 101 | challenge=challenge, 102 | ) 103 | 104 | resp = graphql_client.query(''' 105 | query { 106 | leaderboard { 107 | name 108 | score 109 | } 110 | } 111 | ''') 112 | 113 | assert 'errors' not in resp 114 | 115 | assert len(resp['data']['leaderboard']) == 1 116 | assert resp['data']['leaderboard'][0]['score'] == '256' 117 | -------------------------------------------------------------------------------- /backend/users/api/mutations.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | 3 | from django.contrib.auth import login, logout, authenticate 4 | 5 | from ..forms import RegisterForm 6 | from .types import UserType 7 | 8 | 9 | class LoginErrors(graphene.ObjectType): 10 | non_field_errors = graphene.List(graphene.NonNull(graphene.String)) 11 | 12 | 13 | class LoginMutation(graphene.Mutation): 14 | ok = graphene.Boolean() 15 | errors = graphene.Field(LoginErrors, required=True) 16 | 17 | class Arguments: 18 | username = graphene.String() 19 | password = graphene.String() 20 | 21 | @classmethod 22 | def mutate(cls, root, info, username, password): 23 | user = authenticate(username=username, password=password) 24 | 25 | if user is None: 26 | error = [ 27 | 'Unable to log in with provided credentials' 28 | ] 29 | 30 | return cls( 31 | errors=LoginErrors(non_field_errors=error), 32 | ok=False, 33 | ) 34 | 35 | if not user.is_active: 36 | error = [ 37 | 'It seems your account has been disabled' 38 | ] 39 | 40 | return cls( 41 | errors=LoginErrors(non_field_errors=error), 42 | ok=False, 43 | ) 44 | 45 | login(info.context, user) 46 | 47 | return cls(ok=True, errors=LoginErrors()) 48 | 49 | 50 | class LogoutMutation(graphene.Mutation): 51 | ok = graphene.Boolean() 52 | 53 | @classmethod 54 | def mutate(cls, root, info): 55 | logout(info.context) 56 | 57 | return cls(ok=True) 58 | 59 | 60 | class RegistrationInput(graphene.InputObjectType): 61 | full_name = graphene.String(required=True) 62 | team_name = graphene.String() 63 | email = graphene.String(required=True) 64 | password = graphene.String(required=True) 65 | 66 | 67 | class RegistrationErrors(graphene.ObjectType): 68 | full_name = graphene.List(graphene.NonNull(graphene.String)) 69 | team_name = graphene.List(graphene.NonNull(graphene.String)) 70 | email = graphene.List(graphene.NonNull(graphene.String)) 71 | password = graphene.List(graphene.NonNull(graphene.String)) 72 | non_field_errors = graphene.List(graphene.NonNull(graphene.String)) 73 | 74 | 75 | class RegisterMutation(graphene.Mutation): 76 | ok = graphene.Boolean() 77 | user = graphene.Field(UserType) 78 | errors = graphene.Field(RegistrationErrors, required=True) 79 | 80 | class Arguments: 81 | input = RegistrationInput(required=True) 82 | 83 | @classmethod 84 | def mutate(cls, root, info, input): 85 | form = RegisterForm(data=input) 86 | 87 | if form.is_valid(): 88 | user = form.save() 89 | 90 | login(info.context, user) 91 | 92 | return cls( 93 | ok=True, 94 | user=user, 95 | errors=RegistrationErrors() 96 | ) 97 | 98 | return cls( 99 | ok=False, 100 | user=None, 101 | errors=RegistrationErrors(**form.errors) 102 | ) 103 | -------------------------------------------------------------------------------- /frontend/src/pages/login/form.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react"; 2 | import { withStyles } from "material-ui/styles"; 3 | import Button from "material-ui/Button"; 4 | import { LinearProgress } from "material-ui/Progress"; 5 | import TextField from "material-ui/TextField"; 6 | import idx from "idx"; 7 | 8 | import gql from "graphql-tag"; 9 | import { Mutation } from "react-apollo"; 10 | import { FormHelperText } from "material-ui"; 11 | 12 | import { setIsAuthenticated } from "../../authentication"; 13 | 14 | const LOGIN = gql` 15 | mutation LoginMutation($email: String!, $password: String!) { 16 | login(username: $email, password: $password) { 17 | ok 18 | errors { 19 | nonFieldErrors 20 | } 21 | } 22 | } 23 | `; 24 | 25 | const styles = theme => ({ 26 | container: { 27 | display: "flex", 28 | flexWrap: "wrap", 29 | flexDirection: "column" 30 | } 31 | }); 32 | 33 | class LoginForm extends PureComponent { 34 | state = {}; 35 | 36 | handleChange = name => event => { 37 | this.setState({ 38 | [name]: event.target.value 39 | }); 40 | }; 41 | 42 | render() { 43 | const { classes } = this.props; 44 | 45 | return ( 46 | { 49 | if (login.ok) { 50 | setIsAuthenticated(true).catch(e => console.warn(e)); 51 | } 52 | }} 53 | > 54 | {(login, { data, loading, called }) => { 55 | const errors = idx(data, _ => _.login.errors.nonFieldErrors); 56 | 57 | if (called && data && data.login.ok) { 58 | window.location = "/profile"; 59 | } 60 | 61 | return ( 62 |
{ 67 | e.preventDefault(); 68 | 69 | login({ 70 | variables: { 71 | email: this.state.email, 72 | password: this.state.password 73 | } 74 | }); 75 | }} 76 | > 77 | {errors && ( 78 | {errors.join(" ")} 79 | )} 80 | 81 | {loading && } 82 | 83 | 90 | 91 | 99 | 100 | 103 | 104 | ); 105 | }} 106 |
107 | ); 108 | } 109 | } 110 | 111 | export default withStyles(styles)(LoginForm); 112 | -------------------------------------------------------------------------------- /backend/pycon_ml/settings/base.py: -------------------------------------------------------------------------------- 1 | import environ 2 | 3 | root = environ.Path(__file__) - 3 4 | 5 | env = environ.Env( 6 | DEBUG=(bool, False), 7 | ALLOWED_HOSTS=(list, []), 8 | ) 9 | 10 | environ.Env.read_env(root('.env')) 11 | 12 | DEBUG = env('DEBUG') 13 | 14 | ALLOWED_HOSTS = env('ALLOWED_HOSTS') 15 | 16 | # Application definition 17 | 18 | INSTALLED_APPS = [ 19 | 'django.contrib.admin', 20 | 'django.contrib.auth', 21 | 'django.contrib.contenttypes', 22 | 'django.contrib.sessions', 23 | 'django.contrib.messages', 24 | 'django.contrib.staticfiles', 25 | 26 | 'graphene_django', 27 | 28 | 'users', 29 | 'challenges', 30 | ] 31 | 32 | MIDDLEWARE = [ 33 | 'django.middleware.security.SecurityMiddleware', 34 | 'django.contrib.sessions.middleware.SessionMiddleware', 35 | 'django.middleware.common.CommonMiddleware', 36 | 'django.middleware.csrf.CsrfViewMiddleware', 37 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 38 | 'django.contrib.messages.middleware.MessageMiddleware', 39 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 40 | ] 41 | 42 | ROOT_URLCONF = 'pycon_ml.urls' 43 | 44 | TEMPLATES = [ 45 | { 46 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 47 | 'DIRS': [ 48 | root('frontend'), 49 | ], 50 | 'APP_DIRS': True, 51 | 'OPTIONS': { 52 | 'context_processors': [ 53 | 'django.template.context_processors.debug', 54 | 'django.template.context_processors.request', 55 | 'django.contrib.auth.context_processors.auth', 56 | 'django.contrib.messages.context_processors.messages', 57 | ], 58 | }, 59 | }, 60 | ] 61 | 62 | WSGI_APPLICATION = 'pycon_ml.wsgi.application' 63 | 64 | 65 | # Database 66 | # https://docs.djangoproject.com/en/1.10/ref/settings/#databases 67 | 68 | DATABASES = { 69 | 'default': env.db(default='sqlite:///{}'.format(root('db.sqlite3'))), 70 | } 71 | 72 | # Password validation 73 | # https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators 74 | 75 | AUTH_PASSWORD_VALIDATORS = [{ 76 | 'NAME': 77 | 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 78 | }, { 79 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 80 | }, { 81 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 82 | }, { 83 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 84 | }] 85 | 86 | 87 | # Internationalization 88 | # https://docs.djangoproject.com/en/1.10/topics/i18n/ 89 | 90 | LANGUAGE_CODE = 'en-us' 91 | 92 | LOCALE_PATHS = [ 93 | root('locale'), 94 | ] 95 | 96 | TIME_ZONE = 'UTC' 97 | 98 | USE_I18N = True 99 | 100 | USE_L10N = True 101 | 102 | USE_TZ = True 103 | 104 | 105 | # Static files (CSS, JavaScript, Images) 106 | # https://docs.djangoproject.com/en/1.9/howto/static-files/ 107 | 108 | STATICFILES_DIRS = [ 109 | root('assets'), 110 | ] 111 | 112 | STATIC_URL = '/static/' 113 | STATIC_ROOT = root('static') 114 | 115 | MEDIA_URL = '/media/' 116 | MEDIA_ROOT = root('media') 117 | 118 | AUTH_USER_MODEL = 'users.User' 119 | 120 | GRAPHENE = { 121 | 'SCHEMA': 'api.schema.schema' 122 | } 123 | -------------------------------------------------------------------------------- /frontend/src/pages/submission/index.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react"; 2 | import { withStyles } from "material-ui/styles"; 3 | import Typography from "material-ui/Typography"; 4 | import { ListItem, ListItemIcon, ListItemText } from "material-ui/List"; 5 | import GradeIcon from "material-ui-icons/Grade"; 6 | import { format } from "date-fns"; 7 | 8 | import { gql } from "apollo-boost"; 9 | import { Query } from "react-apollo"; 10 | 11 | import Form from "./form"; 12 | 13 | const printDate = s => format(Date.parse(s), "D MMM [at] HH[:]mm"); 14 | 15 | const GET_CHALLENGE = gql` 16 | query GetChallenge($id: ID!) { 17 | challenge(id: $id) { 18 | name 19 | } 20 | } 21 | `; 22 | 23 | const GET_SUBMISSIONS = gql` 24 | query { 25 | mySubmissions { 26 | created 27 | score 28 | } 29 | } 30 | `; 31 | 32 | const styles = theme => ({ 33 | container: { 34 | display: "flex", 35 | flexWrap: "wrap", 36 | flexDirection: "column" 37 | }, 38 | label: { 39 | width: 100, 40 | margin: 10, 41 | display: "inline-block" 42 | }, 43 | button: { 44 | margin: theme.spacing.unit 45 | } 46 | }); 47 | 48 | class Submission extends PureComponent { 49 | state = {}; 50 | 51 | handleChange = name => event => { 52 | this.setState({ 53 | [name]: event.target.value 54 | }); 55 | }; 56 | 57 | render() { 58 | const { classes } = this.props; 59 | 60 | return ( 61 | 62 | 63 | {({ loading, error, data }) => { 64 | if (loading) return
Loading...
; 65 | if (error || data.challenge === null) return
Error :(
; 66 | 67 | const { challenge } = data; 68 | 69 | return ( 70 |
71 | 72 | Submit for {challenge.name} 73 | 74 | 75 |
76 |
77 | ); 78 | }} 79 |
80 | 81 | 87 | Previous submissions 88 | 89 | 90 | 91 | {({ loading, error, data }) => { 92 | if (loading) return
Loading previous submissions...
; 93 | if (error) return
Error while loading your submissions.
; 94 | 95 | const { mySubmissions } = data; 96 | 97 | if (mySubmissions.length === 0) 98 | return
No previous submission.
; 99 | 100 | return mySubmissions.map(x => ( 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | )); 109 | }} 110 |
111 |
112 | ); 113 | } 114 | } 115 | 116 | export default withStyles(styles)(Submission); 117 | -------------------------------------------------------------------------------- /frontend/src/pages/submission/form.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react"; 2 | import Button from "material-ui/Button"; 3 | import { LinearProgress } from "material-ui/Progress"; 4 | import Chip from "material-ui/Chip"; 5 | import idx from "idx"; 6 | 7 | import gql from "graphql-tag"; 8 | import { Mutation } from "react-apollo"; 9 | import { FormHelperText } from "material-ui"; 10 | 11 | const UPLOAD_SUBMISSION = gql` 12 | mutation UploadSubmission($input: UploadSubmissionInput!) { 13 | uploadSubmission(input: $input) { 14 | ok 15 | result 16 | errors { 17 | solution 18 | } 19 | } 20 | } 21 | `; 22 | 23 | class SubmissionForm extends PureComponent { 24 | state = { 25 | solution: null, 26 | code: null 27 | }; 28 | 29 | handleFileChange = event => { 30 | const { name } = event.target; 31 | 32 | const file = event.target.files[0]; 33 | const reader = new FileReader(); 34 | 35 | reader.onload = reader => { 36 | this.setState({ [name]: reader.target.result }); 37 | }; 38 | 39 | reader.readAsText(file); 40 | }; 41 | 42 | render() { 43 | const { classes } = this.props; 44 | 45 | const hasData = this.state.code && this.state.solution; 46 | 47 | return ( 48 | 49 | {(upload, { data, errors, loading, called }) => { 50 | const solutionErrors = idx( 51 | data, 52 | _ => _.uploadSubmission.errors.solution 53 | ); 54 | const ok = idx(data, _ => _.uploadSubmission.ok); 55 | 56 | return ( 57 | { 62 | e.preventDefault(); 63 | 64 | upload({ 65 | variables: { 66 | input: { 67 | code: this.state.code, 68 | solution: this.state.solution, 69 | challenge: +this.props.id 70 | } 71 | } 72 | }); 73 | }} 74 | > 75 | {errors && {errors}} 76 | {solutionErrors && ( 77 | {solutionErrors.join(" ")} 78 | )} 79 | 80 | {loading && } 81 | 82 | {!loading && 83 | called && ( 84 |
85 | {ok ? ( 86 |

Upload successful

87 | ) : ( 88 |

Upload unsuccessful

89 | )} 90 | 91 | {data && 92 | data.uploadSubmission.result && ( 93 |

Your result is {data.uploadSubmission.result}

94 | )} 95 |
96 | )} 97 | 98 | 109 | 110 | 121 | 122 | 131 | 132 | ); 133 | }} 134 |
135 | ); 136 | } 137 | } 138 | 139 | export default SubmissionForm; 140 | -------------------------------------------------------------------------------- /frontend/src/components/header/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { withStyles } from "material-ui/styles"; 4 | import AppBar from "material-ui/AppBar"; 5 | import Toolbar from "material-ui/Toolbar"; 6 | import Typography from "material-ui/Typography"; 7 | import Button from "material-ui/Button"; 8 | import Drawer from "material-ui/Drawer"; 9 | import IconButton from "material-ui/IconButton"; 10 | import MenuIcon from "material-ui-icons/Menu"; 11 | import List from "material-ui/List"; 12 | import Divider from "material-ui/Divider"; 13 | import { ListItem, ListItemIcon, ListItemText } from "material-ui/List"; 14 | import HomeIcon from "material-ui-icons/Home"; 15 | import StarIcon from "material-ui-icons/Star"; 16 | import AccountCircleIcon from "material-ui-icons/AccountCircle"; 17 | import ExitIcon from "material-ui-icons/ExitToApp"; 18 | 19 | import { Link } from "react-router-dom"; 20 | import { IsAuthenticated } from "../../authentication"; 21 | 22 | const styles = { 23 | root: { 24 | width: "100%" 25 | }, 26 | flex: { 27 | flex: 1 28 | }, 29 | menuButton: { 30 | marginLeft: -12, 31 | marginRight: 20 32 | }, 33 | list: { 34 | width: 250 35 | }, 36 | link: { 37 | textDecoration: "none", 38 | color: "inherit" 39 | } 40 | }; 41 | 42 | class Header extends React.Component { 43 | state = { 44 | drawerOpen: false 45 | }; 46 | 47 | toggleDrawer = open => () => { 48 | this.setState({ 49 | drawerOpen: open 50 | }); 51 | }; 52 | 53 | render() { 54 | const { classes } = this.props; 55 | return ( 56 |
57 | 58 | 59 | 65 | 66 | 67 | 72 | 73 | PyData Challenge 74 | 75 | 76 | 77 | 78 | {authenticated => 79 | !authenticated && ( 80 | 83 | ) 84 | } 85 | 86 | 87 | 88 | 89 |
95 |
96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | {authenticated => 112 | !authenticated && ( 113 | 114 | 115 | 116 | 117 | 118 | 119 | ) 120 | } 121 | 122 | 123 | 124 | 125 | ( 128 | {children} 129 | )} 130 | target="_blank" 131 | href="https://www.pycon.it" 132 | > 133 | 134 | 135 | 136 | 137 | 138 | 139 |
140 |
141 |
142 |
143 | ); 144 | } 145 | } 146 | 147 | Header.propTypes = { 148 | classes: PropTypes.object.isRequired 149 | }; 150 | 151 | export default withStyles(styles)(Header); 152 | -------------------------------------------------------------------------------- /frontend/public/data/description.txt: -------------------------------------------------------------------------------- 1 | The “variants” fields file informations. 2 | 3 | chrom 4 | The chromosome on which the variant resides. 5 | 6 | start 7 | Physical position on the chromosome of the variant 8 | 9 | ref 10 | Reference allele. 11 | 12 | alt 13 | Alternate allele for the variant 14 | 15 | gene 16 | Corresponding gene name of the highly affected transcript. 17 | 18 | Aminoacid 19 | The changing of aminoacid. 20 | 21 | Condition(s) 22 | The phenotype caused from the aminoacid changing. 23 | 24 | Clinical significance 25 | The global impact of the variant. 26 | 27 | 28 | 29 | The “variants_anno.db” fields file informations. 30 | 31 | chrom 32 | The chromosome on which the variant resides. 33 | 34 | start 35 | Physical position on the chromosome of the variant 36 | 37 | end 38 | The 1-based end position of the variant. 39 | 40 | ref 41 | Reference allele. 42 | 43 | alt 44 | Alternate allele for the variant 45 | 46 | qual 47 | Quality score for the assertion made in ALT 48 | 49 | filter 50 | A string of filters passed/failed in variant calling 51 | 52 | type 53 | The type of variant [snp, indel]. 54 | 55 | sub_type 56 | The variant sub-type. If type is snp: [ts, (transition), tv (transversion)]. If type is indel: [ins, (insertion), del (deletion)]. 57 | 58 | aaf 59 | The observed allele frequency for the alternate allele 60 | 61 | hwe 62 | The Chi-square probability of deviation from HWE (assumes random mating). 63 | 64 | inbreeding_coeff 65 | The inbreeding co-efficient that expresses the likelihood of effects due to inbreeding. 66 | 67 | pi 68 | The computed nucleotide diversity (pi) for the site. 69 | 70 | is_exonic 71 | Does the variant affect an exon for >= 1 transcript? 72 | 73 | is_coding 74 | Does the variant fall in a coding region (excl. 3’ & 5’ UTRs) for >= 1 transcript? 75 | 76 | is_lof 77 | Based on the value of the impact col, is the variant LOF for >= transcript? 78 | 79 | is_splicing 80 | Does the variant affect a canonical or possible splice site? That is, set to TRUE if the SO term is any of splice_acceptor_variant, splice_donor_variant, or splice_region_variant. 81 | 82 | codon_change 83 | What is the codon change? 84 | 85 | impact 86 | The consequence of the most severely affected transcript 87 | 88 | impact_so 89 | The Sequence ontology term for the most severe consequence 90 | 91 | impact_severity 92 | Severity of the highest order observed for the variant 93 | 94 | anc_allele 95 | Ancestral allele (based on 1000 genomes reference data) 96 | 97 | rms_bq 98 | The RMS base quality at this position. 99 | 100 | rms_map_qual 101 | RMS mapping quality, a measure of variance of quality scores 102 | 103 | in_dbsnp 104 | Absence (0) or presence (1) of the variant in dbsnp 105 | 106 | rs_ids 107 | A comma-separated list of rs ids for variants present in dbSNP 108 | 109 | in_hm2 110 | Whether the variant was part of HapMap2. 111 | 112 | in_hm3 113 | Whether the variant was part of HapMap3. 114 | 115 | in_esp 116 | Presence/absence of the variant in the ESP project data. 117 | 118 | n_1kg 119 | Presence/absence of the variant in the 1000 genome project data (phase 3). 120 | 121 | max_aaf_all 122 | The maximum of aaf_gnomad{afr,amr,eas,nfe,sas},aaf_esp_ea, aaf_esp_aa, aaf_1kg_amr, aaf_1kg_eas,aaf_1kg_sas,aaf_1kg_afr,aaf_1kg_eur,aaf_adj_exac_afr,aaf_adj_exac_amr,aaf_adj_exac_eas,aaf_adj_exac_nfe,aaf_adj_exac_sas. and -1 if none of those databases/populations contain the variant. 123 | 124 | in_cpg_island 125 | Does the variant overlap a CpG island?. Based on UCSC: Regulation > CpG Islands > cpgIslandExt 126 | 127 | in_segdup 128 | Does the variant overlap a segmental duplication?. Based on UCSC: Variation&Repeats > Segmental Dups > genomicSuperDups track 129 | 130 | is_conserved 131 | Does the variant overlap a conserved region? Based on the 29-way mammalian conservation study. 132 | 133 | gerp_bp_score 134 | GERP conservation score. Higher scores reflect greater conservation. At base-pair resolution. 135 | 136 | gerp_element_pval 137 | GERP elements P-val. Lower P-values scores reflect greater conservation. Not at base-pair resolution. 138 | 139 | recomb_rate 140 | Returns the mean recombination rate at the variant site. Based on HapMapII_GRCh37 genetic map. 141 | 142 | cadd_raw 143 | Raw CADD scores for scoring deleteriousness of SNV’s in the human genome 144 | 145 | cadd_scaled 146 | Scaled CADD scores (Phred like) for scoring deleteriousness of SNV’s 147 | 148 | fitcons 149 | fitCons scores estimating the probability that a point mutation at each position in a genome will influence fitness. Higher scores have more potential for interesting genomic function. Common ranges: 0.05-0.35 for non-coding and 0.4-0.8 for coding. 150 | 151 | encode_consensus_gm12878 152 | ENCODE consensus segmentation prediction for GM12878. CTCF: CTCF-enriched element; E: Predicted enhancer; PF: Predicted promoter flanking region; R: Predicted repressed or low-activity region; TSS: Predicted promoter region including TSS; T: Predicted transcribed region; WE: Predicted weak enhancer or open chromatin cis-regulatory element | unknown: This region of the genome had no functional prediction. 153 | 154 | encode_consensus_h1hesc 155 | ENCODE consensus segmentation prediction for h1HESC. 156 | 157 | encode_consensus_helas3 158 | ENCODE consensus segmentation prediction for Helas3. 159 | 160 | encode_consensus_hepg2 161 | ENCODE consensus segmentation prediction for HEPG2. 162 | 163 | encode_consensus_huvec 164 | ENCODE consensus segmentation prediction for HuVEC. 165 | 166 | encode_consensus_k562 167 | ENCODE consensus segmentation prediction for k562. 168 | 169 | vista_enhancers 170 | Experimentally validated human enhancers from VISTA. 171 | 172 | vep_hgvsc 173 | Human Genome Variation Sequence c-syntax nomenclature. 174 | vep_hgvsp 175 | Human Genome Variation Sequence p-syntax nomenclature. -------------------------------------------------------------------------------- /frontend/src/pages/register/index.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react"; 2 | import { withStyles } from "material-ui/styles"; 3 | import Typography from "material-ui/Typography"; 4 | import Button from "material-ui/Button"; 5 | import TextField from "material-ui/TextField"; 6 | import { gql } from "apollo-boost"; 7 | import { Mutation } from "react-apollo"; 8 | import { FormControl, FormHelperText } from "material-ui/Form"; 9 | import { LinearProgress } from "material-ui/Progress"; 10 | import { withRouter } from "react-router-dom"; 11 | 12 | import idx from "idx"; 13 | 14 | const getErrors = (data, key) => idx(data, _ => _.register.errors[key]) || []; 15 | const hasErrors = (data, key) => getErrors(data, key).length > 0; 16 | 17 | const styles = theme => ({ 18 | container: { 19 | display: "flex", 20 | flexWrap: "wrap", 21 | flexDirection: "column" 22 | } 23 | }); 24 | 25 | const REGISTER = gql` 26 | mutation Register($input: RegistrationInput!) { 27 | register(input: $input) { 28 | ok 29 | user { 30 | fullName 31 | team { 32 | name 33 | } 34 | } 35 | errors { 36 | fullName 37 | teamName 38 | email 39 | password 40 | nonFieldErrors 41 | } 42 | } 43 | } 44 | `; 45 | 46 | class Registration extends PureComponent { 47 | state = { 48 | fullName: "", 49 | email: "", 50 | teamName: "", 51 | password: "" 52 | }; 53 | 54 | handleChange = name => event => { 55 | this.setState({ 56 | [name]: event.target.value 57 | }); 58 | }; 59 | 60 | render() { 61 | const { classes } = this.props; 62 | 63 | return ( 64 |
65 | 66 | Register 67 | 68 | 69 | { 72 | const ok = idx(data, _ => _.register.ok); 73 | 74 | if (ok) { 75 | window.location = "/profile"; 76 | } 77 | }} 78 | variables={{ input: this.state }} 79 | > 80 | {(register, { loading, error, data }) => ( 81 | 82 | {loading && } 83 | 84 | {error && ( 85 | 86 | {error.message}, make sure you have compiled all the field 87 | correctly 88 | 89 | )} 90 | 91 |
{ 93 | e.preventDefault(); 94 | register(); 95 | }} 96 | className={classes.container} 97 | noValidate 98 | autoComplete="off" 99 | > 100 | 101 | 109 | 110 | {getErrors(data, "fullName").join(" ")} 111 | 112 | 113 | 114 | 115 | 123 | 124 | {getErrors(data, "teamName").join(" ")} 125 | 126 | 127 | 128 | 129 | 138 | 139 | {getErrors(data, "email").join(" ")} 140 | 141 | 142 | 143 | 144 | 154 | 155 | {getErrors(data, "password").join(" ")} 156 | 157 | 158 | 159 | 170 |
171 |
172 | )} 173 |
174 |
175 | ); 176 | } 177 | } 178 | 179 | export default withRouter(withStyles(styles)(Registration)); 180 | -------------------------------------------------------------------------------- /backend/tests/api/users/test_register.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from users.models import User, Team 4 | 5 | from django.contrib.sessions.models import Session 6 | 7 | 8 | @pytest.mark.django_db 9 | def test_registration_works(graphql_client): 10 | resp = graphql_client.query(''' 11 | mutation Register($input: RegistrationInput!) { 12 | register(input: $input) { 13 | ok 14 | user { 15 | fullName 16 | } 17 | errors { 18 | fullName 19 | teamName 20 | email 21 | password 22 | nonFieldErrors 23 | } 24 | } 25 | } 26 | ''', variables={ 27 | 'input': { 28 | 'fullName': 'Patrick Arminio', 29 | 'teamName': 'ORO 🏆', 30 | 'email': 'patrick.arminio@gmail.com', 31 | 'password': 'do you even 🏋🏼‍', 32 | } 33 | }) 34 | 35 | assert 'errors' not in resp 36 | 37 | data = resp['data']['register'] 38 | 39 | assert all([value is None for key, value in data['errors'].items()]) 40 | assert data['ok'] 41 | assert data['user']['fullName'] == 'Patrick Arminio' 42 | 43 | assert User.objects.count() == 1 44 | 45 | user = User.objects.first() 46 | 47 | assert user.full_name == 'Patrick Arminio' 48 | 49 | cookies = graphql_client.client.cookies 50 | 51 | assert 'sessionid' in cookies 52 | 53 | session = Session.objects.get(session_key=cookies['sessionid'].value) 54 | uid = session.get_decoded().get('_auth_user_id') 55 | 56 | assert uid == str(user.id) 57 | 58 | 59 | @pytest.mark.django_db 60 | def test_registration_with_errors(graphql_client): 61 | resp = graphql_client.query(''' 62 | mutation Register($input: RegistrationInput!) { 63 | register(input: $input) { 64 | ok 65 | user { 66 | fullName 67 | } 68 | errors { 69 | fullName 70 | teamName 71 | email 72 | password 73 | nonFieldErrors 74 | } 75 | } 76 | } 77 | ''', variables={ 78 | 'input': { 79 | 'fullName': 'Patrick Arminio', 80 | 'teamName': 'ORO 🏆', 81 | 'email': '', 82 | 'password': 'do you even 🏋🏼‍', 83 | } 84 | }) 85 | 86 | assert 'errors' not in resp 87 | 88 | data = resp['data']['register'] 89 | 90 | assert data['errors']['email'] == ['This field is required.'] 91 | 92 | 93 | @pytest.mark.django_db 94 | def test_registration_bad_email(graphql_client): 95 | resp = graphql_client.query(''' 96 | mutation Register($input: RegistrationInput!) { 97 | register(input: $input) { 98 | ok 99 | user { 100 | fullName 101 | } 102 | errors { 103 | fullName 104 | teamName 105 | email 106 | password 107 | nonFieldErrors 108 | } 109 | } 110 | } 111 | ''', variables={ 112 | 'input': { 113 | 'fullName': 'Patrick Arminio', 114 | 'teamName': 'ORO 🏆', 115 | 'email': 'aaaaa', 116 | 'password': 'do you even 🏋🏼‍', 117 | } 118 | }) 119 | 120 | assert 'errors' not in resp 121 | 122 | data = resp['data']['register'] 123 | 124 | assert data['errors']['email'] == ['Enter a valid email address.'] 125 | 126 | 127 | @pytest.mark.django_db 128 | def test_registration_unique_email(graphql_client): 129 | pat = User.objects.create(email='patrick.arminio@gmail.com') 130 | 131 | resp = graphql_client.query(''' 132 | mutation Register($input: RegistrationInput!) { 133 | register(input: $input) { 134 | ok 135 | user { 136 | fullName 137 | } 138 | errors { 139 | fullName 140 | teamName 141 | email 142 | password 143 | nonFieldErrors 144 | } 145 | } 146 | } 147 | ''', variables={ 148 | 'input': { 149 | 'fullName': 'Patrick Arminio', 150 | 'teamName': 'ORO 🏆', 151 | 'email': pat.email, 152 | 'password': 'do you even 🏋🏼‍', 153 | } 154 | }) 155 | 156 | assert 'errors' not in resp 157 | 158 | data = resp['data']['register'] 159 | 160 | assert data['errors']['email'] == [ 161 | 'User with this Email address already exists.' 162 | ] 163 | 164 | 165 | @pytest.mark.django_db 166 | def test_registration_creates_team(graphql_client): 167 | resp = graphql_client.query(''' 168 | mutation Register($input: RegistrationInput!) { 169 | register(input: $input) { 170 | ok 171 | user { 172 | fullName 173 | team { 174 | name 175 | } 176 | } 177 | errors { 178 | fullName 179 | teamName 180 | email 181 | password 182 | nonFieldErrors 183 | } 184 | } 185 | } 186 | ''', variables={ 187 | 'input': { 188 | 'fullName': 'Patrick Arminio', 189 | 'teamName': 'ORO 🏆', 190 | 'email': 'patrick.arminio@gmail.com', 191 | 'password': 'do you even 🏋🏼‍', 192 | } 193 | }) 194 | 195 | assert 'errors' not in resp 196 | 197 | data = resp['data']['register'] 198 | 199 | assert all([value is None for key, value in data['errors'].items()]) 200 | assert data['ok'] 201 | assert data['user']['team']['name'] == 'ORO 🏆' 202 | 203 | assert Team.objects.count() == 1 204 | 205 | team = Team.objects.first() 206 | 207 | assert team.name == 'ORO 🏆' 208 | 209 | 210 | @pytest.mark.django_db 211 | def test_registration_without_team(graphql_client): 212 | resp = graphql_client.query(''' 213 | mutation Register($input: RegistrationInput!) { 214 | register(input: $input) { 215 | ok 216 | user { 217 | fullName 218 | team { 219 | name 220 | } 221 | } 222 | errors { 223 | fullName 224 | teamName 225 | email 226 | password 227 | nonFieldErrors 228 | } 229 | } 230 | } 231 | ''', variables={ 232 | 'input': { 233 | 'fullName': 'Patrick Arminio', 234 | 'teamName': '', 235 | 'email': 'patrick.arminio@gmail.com', 236 | 'password': 'do you even 🏋🏼‍', 237 | } 238 | }) 239 | 240 | assert 'errors' not in resp 241 | 242 | data = resp['data']['register'] 243 | 244 | assert all([value is None for key, value in data['errors'].items()]) 245 | assert data['ok'] 246 | assert data['user']['team'] is None 247 | 248 | assert Team.objects.count() == 0 249 | -------------------------------------------------------------------------------- /frontend/src/pages/home/index.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react"; 2 | import { withStyles } from "material-ui/styles"; 3 | import Typography from "material-ui/Typography"; 4 | import List, { ListItem, ListItemText } from "material-ui/List"; 5 | import FileDownloadIcon from "@material-ui/icons/FileDownload"; 6 | import Avatar from "material-ui/Avatar"; 7 | import Button from "material-ui/Button"; 8 | import Stepper, { Step, StepLabel } from "material-ui/Stepper"; 9 | import { Link } from "react-router-dom"; 10 | 11 | const styles = theme => ({}); 12 | 13 | class Home extends PureComponent { 14 | render() { 15 | return ( 16 |
17 | 18 | Welcome to the PyCon Italia Machine Learning Challenge 19 | 20 | 21 | 22 | We have a prepared a little contest for those of you who love 23 | challenges and machine learning. 24 | 25 | 26 | 27 | The challenge "AI for Precision Medicine" has been created by{" "} 28 | 29 | GenomeUp 30 | . 31 | 32 | 33 | 34 | You will have the following files: 35 | 36 | 40 | 41 | 42 | 43 | 44 | 48 | 49 | 50 | 54 | 55 | 56 | 57 | 58 | 62 | 63 | 64 | 68 | 69 | 70 | 71 | 72 | 76 | 77 | 78 | 79 | 80 | 81 | Objective 82 | 83 | 84 | 85 | The objective of the challenge is to complete the data for the 86 | "Clinical significance" for the entries missing from the file{" "} 87 | variants.csv. 88 |
89 | The “Clinical significance” field conveys the degree of harmfulness of 90 | the corresponding mutation. 91 |
92 | Each row is uniquely identified by one of the following four field 93 | information: "CHROM", "START", "REF", "ALT". 94 |
95 | The values of the Clinical significance can be one of the following: 96 |
    97 |
  1. Benign
  2. 98 |
  3. Likely-Benign
  4. 99 |
  5. Benign / Likely-Benign
  6. 100 |
  7. Likely-Pathogenic
  8. 101 |
  9. Likely-Pathogenic / Pathogenic
  10. 102 |
  11. Pathogenic
  12. 103 |
  13. Other
  14. 104 |
105 | The only classes of interest for the challenge are 106 | the following:{" "} 107 | Benign, Likely-Benign, Likely-Pathogenic, Pathogenic. 108 |
109 | 110 | 111 | Steps 112 | 113 | 114 | 115 | Phase one 116 | 117 | 118 | 119 | 120 | Read each row from the variants.csv file 121 | 122 | 123 | 124 | Get additional data for each row from the variants_year.db 125 | 126 | 127 | 128 | 129 | A multicore solution will be considered a plus 130 | 131 | 132 | 133 | 134 | 135 | Phase two 136 | 137 | 138 | 139 | 140 | 141 | Create a machine learning algorithm that will be able to find the 142 | degree of harmfulness of the genetic mutations considering the 143 | data matched in phase one 144 | 145 | 146 | 147 | 148 | 149 | Output format 150 | 151 | 152 | 153 | The solution is expected to be a textual file formatted as follows: 154 |
CHROM;START;REF;ALT;{"{PREDICTED CLINICAL SIGNIFICANCE}"}
155 | where {"{PREDICTED CLINICAL SIGNIFICANCE}"} is one of 0,1,3,5 from the 156 | list above 157 |
158 |             {`chr22;49910903;G;A;5
159 | chr9;37781507;T;C;5
160 | chr7;21590585;C;G;0
161 | chr21;46134692;G;A;1`}
162 |           
163 |
164 | 165 | 166 | Solution 167 | 168 | 169 | 170 | Once you have completed the previous phases you can upload the CSV 171 | file with the results and the code here: 172 | 173 | 174 | 186 | 187 | NOTE: solutions without code won't qualify for the 188 | prizes. 189 | 190 | 191 | Scoring 192 | 193 | 194 | 195 | The winner will be judged based on the accuracy of the machine 196 | learning model 197 | 198 |
199 | ); 200 | } 201 | } 202 | 203 | export default withStyles(styles)(Home); 204 | -------------------------------------------------------------------------------- /backend/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "fb3ffe80d82f1881771e51ddec79a7b56fe358a2718a0d41a639c633d607f51a" 5 | }, 6 | "host-environment-markers": { 7 | "implementation_name": "cpython", 8 | "implementation_version": "3.6.1", 9 | "os_name": "posix", 10 | "platform_machine": "x86_64", 11 | "platform_python_implementation": "CPython", 12 | "platform_release": "17.5.0", 13 | "platform_system": "Darwin", 14 | "platform_version": "Darwin Kernel Version 17.5.0: Mon Mar 5 22:24:32 PST 2018; root:xnu-4570.51.1~1/RELEASE_X86_64", 15 | "python_full_version": "3.6.1", 16 | "python_version": "3.6", 17 | "sys_platform": "darwin" 18 | }, 19 | "pipfile-spec": 6, 20 | "requires": {}, 21 | "sources": [ 22 | { 23 | "name": "pypi", 24 | "url": "https://pypi.python.org/simple", 25 | "verify_ssl": true 26 | } 27 | ] 28 | }, 29 | "default": { 30 | "aniso8601": { 31 | "hashes": [ 32 | "sha256:f7052eb342bf2000c6264a253acedb362513bf9270800be2bc8e3e229fe08b5a", 33 | "sha256:7cf068e7aec00edeb21879c2bbda048656c34d281e133a77425be03b352122d8" 34 | ], 35 | "version": "==3.0.0" 36 | }, 37 | "django": { 38 | "hashes": [ 39 | "sha256:2d8b9eed8815f172a8e898678ae4289a5e9176bc08295676eff4228dd638ea61", 40 | "sha256:d81a1652963c81488e709729a80b510394050e312f386037f26b54912a3a10d0" 41 | ], 42 | "version": "==2.0.4" 43 | }, 44 | "django-environ": { 45 | "hashes": [ 46 | "sha256:e9c171b9d5f540e6f3bc42866941d9cd0bd77fb110a7c13a7c4857a2c08cfa40", 47 | "sha256:ee2f8405d83137e3328b26b3de01bd715b5395fca22feb919dcc905fb6099cfa" 48 | ], 49 | "version": "==0.4.4" 50 | }, 51 | "django-model-utils": { 52 | "hashes": [ 53 | "sha256:4356fad5f6fc9910da865fbf9371f9c2e028606dff2bbd8e32e67569260e530d", 54 | "sha256:a9baa7de943b4e8afa61728ce8c42ce99e88cc87d40e74df2b060a78d22c0f5c" 55 | ], 56 | "version": "==3.1.1" 57 | }, 58 | "graphene": { 59 | "hashes": [ 60 | "sha256:dc95daa3f808fa1c1b26b780d103216a3d562eb95f72744a054b5bf6609d5c5c", 61 | "sha256:cf8c7fd4dc74c959a44cd817d9e1b07abad185bbf586aac1f75650c1abdd5d31" 62 | ], 63 | "version": "==2.1" 64 | }, 65 | "graphene-django": { 66 | "hashes": [ 67 | "sha256:5cabf46b59f242a533fe1df1912c87d5ad6606246937609be2e6c9086cfdf7a4", 68 | "sha256:70d9358bc48c806b6a9458e8344f0a1211cd1f1ef923a092aa55e6bcacc2c3fc" 69 | ], 70 | "version": "==2.0.0" 71 | }, 72 | "graphql-core": { 73 | "hashes": [ 74 | "sha256:539355351343dede3ecb771e0d273a1b72405cb6d64f45bb8f92ecc4d7109af0", 75 | "sha256:4830699be53f9154273fa15726fc8b0c90bc22bbb8fc7c932586503b3cb9330e" 76 | ], 77 | "version": "==2.0" 78 | }, 79 | "graphql-relay": { 80 | "hashes": [ 81 | "sha256:2716b7245d97091af21abf096fabafac576905096d21ba7118fba722596f65db" 82 | ], 83 | "version": "==0.4.5" 84 | }, 85 | "iso8601": { 86 | "hashes": [ 87 | "sha256:210e0134677cc0d02f6028087fee1df1e1d76d372ee1db0bf30bf66c5c1c89a3", 88 | "sha256:bbbae5fb4a7abfe71d4688fd64bff70b91bbd74ef6a99d964bab18f7fdf286dd", 89 | "sha256:49c4b20e1f38aa5cf109ddcd39647ac419f928512c869dc01d5c7098eddede82" 90 | ], 91 | "version": "==0.1.12" 92 | }, 93 | "numpy": { 94 | "hashes": [ 95 | "sha256:719d914f564f35cce4dc103808f8297c807c9f0297ac183ed81ae8b5650e698e", 96 | "sha256:0f6a5ed0cd7ab1da11f5c07a8ecada73fc55a70ef7bb6311a4109891341d7277", 97 | "sha256:d0928076d9bd8a98de44e79b1abe50c1456e7abbb40af7ef58092086f1a6c729", 98 | "sha256:d858423f5ed444d494b15c4cc90a206e1b8c31354c781ac7584da0d21c09c1c3", 99 | "sha256:20cac3123d791e4bf8482a580d98d6b5969ba348b9d5364df791ba3a666b660d", 100 | "sha256:528ce59ded2008f9e8543e0146acb3a98a9890da00adf8904b1e18c82099418b", 101 | "sha256:56e392b7c738bd70e6f46cf48c8194d3d1dd4c5a59fae4b30c58bb6ef86e5233", 102 | "sha256:99051e03b445117b26028623f1a487112ddf61a09a27e2d25e6bc07d37d94f25", 103 | "sha256:768e777cc1ffdbf97c507f65975c8686ebafe0f3dc8925d02ac117acc4669ce9", 104 | "sha256:675e0f23967ce71067d12b6944add505d5f0a251f819cfb44bdf8ee7072c090d", 105 | "sha256:a958bf9d4834c72dee4f91a0476e7837b8a2966dc6fcfc42c421405f98d0da51", 106 | "sha256:bb370120de6d26004358611441e07acda26840e41dfedc259d7f8cc613f96495", 107 | "sha256:f2b1378b63bdb581d5d7af2ec0373c8d40d651941d283a2afd7fc71184b3f570", 108 | "sha256:a1413d06abfa942ca0553bf3bccaff5fdb36d55b84f2248e36228db871147dab", 109 | "sha256:7f76d406c6b998d6410198dcb82688dcdaec7d846aa87e263ccf52efdcfeba30", 110 | "sha256:a7157c9ac6bddd2908c35ef099e4b643bc0e0ebb4d653deb54891d29258dd329", 111 | "sha256:0fd65cbbfdbf76bbf80c445d923b3accefea0fe2c2082049e0ce947c81fe1d3f", 112 | "sha256:8c18ee4dddd5c6a811930c0a7c7947bf16387da3b394725f6063f1366311187d", 113 | "sha256:0739146eaf4985962f07c62f7133aca89f3a600faac891ce6c7f3a1e2afe5272", 114 | "sha256:07e21f14490324cc1160db101e9b6c1233c33985af4cb1d301dd02650fea1d7f", 115 | "sha256:e6120d63b50e2248219f53302af7ec6fa2a42ed1f37e9cda2c76dbaca65036a7", 116 | "sha256:6be6b0ca705321c178c9858e5ad5611af664bbdfae1df1541f938a840a103888", 117 | "sha256:facc6f925c3099ac01a1f03758100772560a0b020fb9d70f210404be08006bcb" 118 | ], 119 | "version": "==1.14.2" 120 | }, 121 | "pandas": { 122 | "hashes": [ 123 | "sha256:68ac484e857dcbbd07ea7c6f516cc67f7f143f5313d9bc661470e7f473528882", 124 | "sha256:12f2a19d0b0adf31170d98d0e8bcbc59add0965a9b0c65d39e0665400491c0c5", 125 | "sha256:68b121d13177f5128a4c118bb4f73ba40df28292c038389961aa55ea5a996427", 126 | "sha256:06efae5c00b9f4c6e6d3fe1eb52e590ff0ea8e5cb58032c724e04d31c540de53", 127 | "sha256:02541a4fdd31315f213a5c8e18708abad719ee03eda05f603c4fe973e9b9d770", 128 | "sha256:2907f3fe91ca2119ac3c38de6891bbbc83333bfe0d98309768fee28de563ee7a", 129 | "sha256:052a66f58783a59ea38fdfee25de083b107baa81fdbe38fabd169d0f9efce2bf", 130 | "sha256:244ae0b9e998cfa88452a49b20e29bf582cc7c0e69093876d505aec4f8e1c7fe", 131 | "sha256:66403162c8b45325a995493bdd78ad4d8be085e527d721dbfa773d56fbba9c88", 132 | "sha256:af0dbac881f6f87acd325415adea0ce8cccf28f5d4ad7a54b6a1e176e2f7bf70", 133 | "sha256:c2cd884794924687edbaad40d18ac984054d247bb877890932c4d41e3c3aba31", 134 | "sha256:c372db80a5bcb143c9cb254d50f902772c3b093a4f965275197ec2d2184b1e61", 135 | "sha256:97c8223d42d43d86ca359a57b4702ca0529c6553e83d736e93a5699951f0f8db", 136 | "sha256:587a9816cc663c958fcff7907c553b73fe196604f990bc98e1b71ebf07e45b44", 137 | "sha256:44a94091dd71f05922eec661638ec1a35f26d573c119aa2fad964f10a2880e6c" 138 | ], 139 | "version": "==0.22.0" 140 | }, 141 | "promise": { 142 | "hashes": [ 143 | "sha256:0bca4ed933e3d50e3d18fb54fc1432fa84b0564838cd093e824abcd718ab9304", 144 | "sha256:95506bac89df7a495e0b8c813fd782dd1ae590decb52f95248e316c6659ca49b" 145 | ], 146 | "version": "==2.1" 147 | }, 148 | "psycopg2": { 149 | "hashes": [ 150 | "sha256:aeaba399254ca79c299d9fe6aa811d3c3eac61458dee10270de7f4e71c624998", 151 | "sha256:1d90379d01d0dc50ae9b40c863933d87ff82d51dd7d52cea5d1cb7019afd72cd", 152 | "sha256:36030ca7f4b4519ee4f52a74edc4ec73c75abfb6ea1d80ac7480953d1c0aa3c3", 153 | "sha256:7cbc3b21ce2f681ca9ad2d8c0901090b23a30c955e980ebf1006d41f37068a95", 154 | "sha256:b178e0923c93393e16646155794521e063ec17b7cc9f943f15b7d4b39776ea2c", 155 | "sha256:fe6a7f87356116f5ea840c65b032af17deef0e1a5c34013a2962dd6f99b860dd", 156 | "sha256:6f302c486132f8dd11f143e919e236ea4467d53bf18c451cac577e6988ecbd05", 157 | "sha256:888bba7841116e529f407f15c6d28fe3ef0760df8c45257442ec2f14f161c871", 158 | "sha256:932a4c101af007cb3132b1f8a9ffef23386acc53dad46536dc5ba43a3235ae02", 159 | "sha256:179c52eb870110a8c1b460c86d4f696d58510ea025602cd3f81453746fccb94f", 160 | "sha256:33f9e1032095e1436fa9ec424abcbd4c170da934fb70e391c5d78275d0307c75", 161 | "sha256:092a80da1b052a181b6e6c765849c9b32d46c5dac3b81bf8c9b83e697f3cdbe8", 162 | "sha256:f3d3a88128f0c219bdc5b2d9ccd496517199660cea021c560a3252116df91cbd", 163 | "sha256:19983b77ec1fc2a210092aa0333ee48811fd9fb5f194c6cd5b927ed409aea5f8", 164 | "sha256:027ae518d0e3b8fff41990e598bc7774c3d08a3a20e9ecc0b59fb2aaaf152f7f", 165 | "sha256:363fbbf4189722fc46779be1fad2597e2c40b3f577dc618f353a46391cf5d235", 166 | "sha256:d74cf9234ba76426add5e123449be08993a9b13ff434c6efa3a07caa305a619f", 167 | "sha256:32702e3bd8bfe12b36226ba9846ed9e22336fc4bd710039d594b36bd432ae255", 168 | "sha256:8eb94c0625c529215b53c08fb4e461546e2f3fc96a49c13d5474b5ad7aeab6cf", 169 | "sha256:8ebba5314c609a05c6955e5773c7e0e57b8dd817e4f751f30de729be58fa5e78", 170 | "sha256:27467fd5af1dcc0a82d72927113b8f92da8f44b2efbdb8906bd76face95b596d", 171 | "sha256:b68e89bb086a9476fa85298caab43f92d0a6af135a5f433d1f6b6d82cafa7b55", 172 | "sha256:0b9851e798bae024ed1a2a6377a8dab4b8a128a56ed406f572f9f06194e4b275", 173 | "sha256:733166464598c239323142c071fa4c9b91c14359176e5ae7e202db6bcc1d2eb5", 174 | "sha256:ad75fe10bea19ad2188c5cb5fc4cdf53ee808d9b44578c94a3cd1e9fc2beb656", 175 | "sha256:8966829cb0d21a08a3c5ac971a2eb67c3927ae27c247300a8476554cc0ce2ae8", 176 | "sha256:8bf51191d60f6987482ef0cfe8511bbf4877a5aa7f313d7b488b53189cf26209" 177 | ], 178 | "version": "==2.7.4" 179 | }, 180 | "python-dateutil": { 181 | "hashes": [ 182 | "sha256:3220490fb9741e2342e1cf29a503394fdac874bc39568288717ee67047ff29df", 183 | "sha256:9d8074be4c993fbe4947878ce593052f71dac82932a677d49194d8ce9778002e" 184 | ], 185 | "version": "==2.7.2" 186 | }, 187 | "pytz": { 188 | "hashes": [ 189 | "sha256:65ae0c8101309c45772196b21b74c46b2e5d11b6275c45d251b150d5da334555", 190 | "sha256:c06425302f2cf668f1bba7a0a03f3c1d34d4ebeef2c72003da308b3947c7f749" 191 | ], 192 | "version": "==2018.4" 193 | }, 194 | "rx": { 195 | "hashes": [ 196 | "sha256:7357592bc7e881a95e0c2013b73326f704953301ab551fbc8133a6fadab84105", 197 | "sha256:13a1d8d9e252625c173dc795471e614eadfe1cf40ffc684e08b8fff0d9748c23" 198 | ], 199 | "version": "==1.6.1" 200 | }, 201 | "scikit-learn": { 202 | "hashes": [ 203 | "sha256:3775cca4ce3f94508bb7c8a6b113044b78c16b0a30a5c169ddeb6b9fe57a8a72", 204 | "sha256:873245b03361710f47c5410a050dc56ee8ae97b9f8dcc6e3a81521ca2b64ad10", 205 | "sha256:370919e3148253fd6552496c33a1e3d78290a336fc8d1b9349d9e9770fae6ec0", 206 | "sha256:ce78bf4d10bd7e28807c36c6d2ab25a9934aaf80906ad987622a5e45627d91a2", 207 | "sha256:ba3fd442ae1a46830789b3578867daaf2c8409dcca6bf192e30e85beeabbfc2f", 208 | "sha256:a21cf8217e31a9e8e32c559246e05e6909981816152406945ae2e3e244dfcc1f", 209 | "sha256:e54a3dd1fe1f8124de90b93c48d120e6da2ea8df29b6895325df01ddc1bd8e26", 210 | "sha256:f9abae483f4d52acd6f660addb1b67e35dc5748655250af479de2ea6aefc6df0", 211 | "sha256:5c9ff456d67ef9094e5ea272fff2be05d399a47fc30c6c8ed653b94bdf787bd1", 212 | "sha256:871669cdb5b3481650fe3adff46eb97c455e30ecdc307eaf382ef90d4e2570ab", 213 | "sha256:d4da369614e55540c7e830ccdd17ab4fe5412ff8e803a4906d3ece393e2e3a63", 214 | "sha256:42f3c5bd893ed73bf47ccccf04dfb98fae743f397d688bb58c2238c0e6ec15d2", 215 | "sha256:95b155ef6bf829ddfba6026f100ba8e4218b7171ecab97b2163bc9e8d206848f", 216 | "sha256:72c194c5092e921d6107a8de8a5adae58c35bbc54e030ba624b6f02fd823bb21", 217 | "sha256:f528c4b2bba652cf116f5cccf36f4db95a7f9cbfcd1ee549c4e8d0f8628783b5", 218 | "sha256:d384e6f9a055b7a43492f9d27779adb717eb5dcf78b0603b01d0f070a608d241", 219 | "sha256:ee8c3b1898c728b6e5b5659c233f547700a1fea13ce876b6fe7d3434c70cc0e0", 220 | "sha256:56cfa19c31edf62e6414da0a337efee37a4af488b135640e67238786b9be6ab3", 221 | "sha256:5db9e68a384ce80a17fc449d4d5d9b45025fe17cf468429599bf404eccb51049", 222 | "sha256:8b17fc29554c5c98d88142f895516a5bec2b6b61daa815e1193a64c868ad53d2", 223 | "sha256:13136c6e4f6b808569f7f59299d439b2cd718f85d72ea14b5b6077d44ebc7d17", 224 | "sha256:ddc1eb10138ae93c136cc4b5945d3977f302b5d693592a4731b2805a7d7f2a74", 225 | "sha256:5ca0ad32ee04abe0d4ba02c8d89d501b4e5e0304bdf4d45c2e9875a735b323a0", 226 | "sha256:6e0899953611d0c47c0d49c5950082ab016b38811fced91cd2dcc889dd94f50a", 227 | "sha256:b2a10e2f9b73de10d8486f7a23549093436062b69139158802910a0f154aa53b", 228 | "sha256:a58746d4f389ea7df1d908dba8b52f709835f91c342f459a3ade5424330c69d1", 229 | "sha256:fdc39e89bd3466befb76dfc0c258d4ccad159df974954a87de3be5759172a067" 230 | ], 231 | "version": "==0.19.1" 232 | }, 233 | "scipy": { 234 | "hashes": [ 235 | "sha256:3fab61f6ffbc1da9e1f8e813ba235929b5f361c1fdd728d120ead8f78560427c", 236 | "sha256:4a9f5a3adbfd08dd047de48a6b436bd4dae364913aa5b6349a41e9eaeb774050", 237 | "sha256:cc85b9d10a5da07a6b27449d17584b1a3d953f7286189f170a6045c6c297b0bc", 238 | "sha256:76f32095863f7eccecaf5224624d4c7f0f3b922b2cd0d0b0d6f037e4d9c54db6", 239 | "sha256:232a848a9935c37ffe2cade857296a13680724aef7d4623160dd08f99dbb9d02", 240 | "sha256:3eb28bdc6f40a964761c1cb339e45fef076ea2ca729886e1bce956932ef7e487", 241 | "sha256:103fd8253fd3f0df3f50a554abc01b13bee1d70483f23bb03636a74b0a0cbc71", 242 | "sha256:f26e54dcdeec3daff6ec71854a7b3bba719b78cf07e2d23645ee09d67a5e93df", 243 | "sha256:a3287b438b3c13970c4851a34bbc82db11e4680d1d6cdedd066479ca83028042", 244 | "sha256:200ca3dfebbd0bbacbc0d7389e6eda77428b49793cc1e0e3247f835687d1a6be", 245 | "sha256:f7c0624de958e2e2e6353813c78aa4f5f3f1ed0a3567fb496b6dad99f55e695e", 246 | "sha256:544e6f7796b9c131f8f965967e806da187553abf5d7766278b96a2a76abd19a7", 247 | "sha256:4a2837b07e77f8e2fc5303f38e224d7fd9f79f8cbf9c60ac72cf98594e1db6b5", 248 | "sha256:11cddcfc348ef593feb4ffe4b69c18a064eca199602f751d34083838bdc2f05a", 249 | "sha256:164f774de04e546fd10e1894d423b54e36255bb42887e005f0fbfb8eef6736f1", 250 | "sha256:800abaa55cfad422f00f5b3802403dd634ab9888f560731c09977a3c35e0acae", 251 | "sha256:be6f0af3f591c100923158f3e3f0f12fa16a0b231616eda407c528e1f9e10014", 252 | "sha256:1766acf2d8ff79ed59cae841d4272587af94772f513619dd217226027bd2ab76", 253 | "sha256:f2a51f70ef868c03430ed40d8983daa2f38d2e2160a0de4b57b7d9d070912c76", 254 | "sha256:89c1d2a80760a373e7f12f490446c60a7221b641435a789439e8ddb01f5ab7d4", 255 | "sha256:823a4b1a2eabd09f9e72003c14ceaac63f54d1f99a49f8c1b534f6a73135e995", 256 | "sha256:cc04cf28b9f77255eeb612af41a6f207142f92a082555871f782c773c83b7789", 257 | "sha256:8739c67842ed9a1c34c62d6cca6301d0ade40d50ef14ba292bd331f0d6c940ba" 258 | ], 259 | "version": "==1.0.1" 260 | }, 261 | "singledispatch": { 262 | "hashes": [ 263 | "sha256:833b46966687b3de7f438c761ac475213e53b306740f1abfaa86e1d1aae56aa8", 264 | "sha256:5b06af87df13818d14f08a028e42f566640aef80805c3b50c5056b086e3c2b9c" 265 | ], 266 | "version": "==3.4.0.3" 267 | }, 268 | "six": { 269 | "hashes": [ 270 | "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb", 271 | "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9" 272 | ], 273 | "version": "==1.11.0" 274 | }, 275 | "sklearn": { 276 | "hashes": [ 277 | "sha256:e23001573aa194b834122d2b9562459bf5ae494a2d59ca6b8aa22c85a44c0e31" 278 | ], 279 | "version": "==0.0" 280 | }, 281 | "typing": { 282 | "hashes": [ 283 | "sha256:b2c689d54e1144bbcfd191b0832980a21c2dbcf7b5ff7a66248a60c90e951eb8", 284 | "sha256:3a887b021a77b292e151afb75323dea88a7bc1b3dfa92176cff8e44c8b68bddf", 285 | "sha256:d400a9344254803a2368533e4533a4200d21eb7b6b729c173bc38201a74db3f2" 286 | ], 287 | "version": "==3.6.4" 288 | }, 289 | "uwsgi": { 290 | "hashes": [ 291 | "sha256:3dc2e9b48db92b67bfec1badec0d3fdcc0771316486c5efa3217569da3528bf2" 292 | ], 293 | "version": "==2.0.17" 294 | } 295 | }, 296 | "develop": { 297 | "attrs": { 298 | "hashes": [ 299 | "sha256:a17a9573a6f475c99b551c0e0a812707ddda1ec9653bed04c13841404ed6f450", 300 | "sha256:1c7960ccfd6a005cd9f7ba884e6316b5e430a3f1a6c37c5f87d8b43f83b54ec9" 301 | ], 302 | "version": "==17.4.0" 303 | }, 304 | "flake8": { 305 | "hashes": [ 306 | "sha256:c7841163e2b576d435799169b78703ad6ac1bbb0f199994fc05f700b2a90ea37", 307 | "sha256:7253265f7abd8b313e3892944044a365e3f4ac3fcdcfb4298f55ee9ddf188ba0" 308 | ], 309 | "version": "==3.5.0" 310 | }, 311 | "mccabe": { 312 | "hashes": [ 313 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 314 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 315 | ], 316 | "version": "==0.6.1" 317 | }, 318 | "more-itertools": { 319 | "hashes": [ 320 | "sha256:11a625025954c20145b37ff6309cd54e39ca94f72f6bb9576d1195db6fa2442e", 321 | "sha256:0dd8f72eeab0d2c3bd489025bb2f6a1b8342f9b198f6fc37b52d15cfa4531fea", 322 | "sha256:c9ce7eccdcb901a2c75d326ea134e0886abfbea5f93e91cc95de9507c0816c44" 323 | ], 324 | "version": "==4.1.0" 325 | }, 326 | "pluggy": { 327 | "hashes": [ 328 | "sha256:d345c8fe681115900d6da8d048ba67c25df42973bda370783cd58826442dcd7c", 329 | "sha256:e160a7fcf25762bb60efc7e171d4497ff1d8d2d75a3d0df7a21b76821ecbf5c5", 330 | "sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff" 331 | ], 332 | "version": "==0.6.0" 333 | }, 334 | "py": { 335 | "hashes": [ 336 | "sha256:983f77f3331356039fdd792e9220b7b8ee1aa6bd2b25f567a963ff1de5a64f6a", 337 | "sha256:29c9fab495d7528e80ba1e343b958684f4ace687327e6f789a94bf3d1915f881" 338 | ], 339 | "version": "==1.5.3" 340 | }, 341 | "pycodestyle": { 342 | "hashes": [ 343 | "sha256:5ff2fbcbab997895ba9ead77e1b38b3ebc2e5c3b8a6194ef918666e4c790a00e", 344 | "sha256:6c4245ade1edfad79c3446fadfc96b0de2759662dc29d07d80a6f27ad1ca6ba9", 345 | "sha256:1ec08a51c901dfe44921576ed6e4c1f5b7ecbad403f871397feedb5eb8e4fa14", 346 | "sha256:682256a5b318149ca0d2a9185d365d8864a768a28db66a84a2ea946bcc426766" 347 | ], 348 | "version": "==2.3.1" 349 | }, 350 | "pyflakes": { 351 | "hashes": [ 352 | "sha256:08bd6a50edf8cffa9fa09a463063c425ecaaf10d1eb0335a7e8b1401aef89e6f", 353 | "sha256:8d616a382f243dbf19b54743f280b80198be0bca3a5396f1d2e1fca6223e8805" 354 | ], 355 | "version": "==1.6.0" 356 | }, 357 | "pytest": { 358 | "hashes": [ 359 | "sha256:6266f87ab64692112e5477eba395cfedda53b1933ccd29478e671e73b420c19c", 360 | "sha256:fae491d1874f199537fd5872b5e1f0e74a009b979df9d53d1553fd03da1703e1" 361 | ], 362 | "version": "==3.5.0" 363 | }, 364 | "pytest-django": { 365 | "hashes": [ 366 | "sha256:534505e0261cc566279032d9d887f844235342806fd63a6925689670fa1b29d7", 367 | "sha256:7501942093db2250a32a4e36826edfc542347bb9b26c78ed0649cdcfd49e5789" 368 | ], 369 | "version": "==3.2.1" 370 | }, 371 | "six": { 372 | "hashes": [ 373 | "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb", 374 | "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9" 375 | ], 376 | "version": "==1.11.0" 377 | } 378 | } 379 | } 380 | --------------------------------------------------------------------------------