├── runtime.txt ├── biproductive ├── account │ ├── __init__.py │ ├── migrations │ │ └── __init__.py │ ├── admin.py │ ├── models.py │ ├── apps.py │ ├── urls.py │ ├── forms.py │ ├── views.py │ ├── tests.py │ └── templates │ │ ├── login.html │ │ └── signup.html ├── habits │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ ├── 0003_habitusage_status.py │ │ ├── 0002_auto_20211004_1206.py │ │ └── 0001_initial.py │ ├── admin.py │ ├── apps.py │ ├── urls.py │ ├── models.py │ ├── forms.py │ ├── scripts.py │ ├── tests.py │ ├── views.py │ └── templates │ │ ├── add_habit.html │ │ └── track_habits.html ├── home │ ├── __init__.py │ ├── migrations │ │ └── __init__.py │ ├── models.py │ ├── tests.py │ ├── admin.py │ ├── data │ │ ├── habits.csv │ │ ├── correlation.csv │ │ └── productivity.csv │ ├── urls.py │ ├── apps.py │ ├── views.py │ ├── scripts.py │ └── templates │ │ └── home.html ├── biproductive │ ├── __init__.py │ ├── asgi.py │ ├── wsgi.py │ ├── urls.py │ └── settings.py ├── productivity │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ ├── 0002_auto_20210930_1943.py │ │ └── 0001_initial.py │ ├── admin.py │ ├── urls.py │ ├── apps.py │ ├── models.py │ ├── scripts.py │ ├── views.py │ ├── tests.py │ └── templates │ │ └── game.html ├── report_generator │ ├── __init__.py │ ├── migrations │ │ └── __init__.py │ ├── admin.py │ ├── models.py │ ├── apps.py │ ├── tests.py │ └── views.py ├── templates │ ├── static │ │ ├── assets │ │ │ ├── github_logo.png │ │ │ └── email_logo.svg │ │ ├── scripts │ │ │ ├── datatables.js │ │ │ ├── brain-activity-chart.js │ │ │ └── game.js │ │ └── styles │ │ │ ├── base.css │ │ │ └── game.css │ ├── index.html │ └── base_generic.html ├── utils.py └── manage.py ├── docs ├── demo.gif ├── design_sketch.pdf ├── home_page_figma.png ├── login_page_figma.png ├── use-case-diagram.jpg ├── dynamic-view-updated.png ├── low_fidelity_prototype.png ├── use-case-diagram-updated.png ├── AUTHORS.MD ├── CONTRIBUTING.MD ├── HEROKU.MD ├── PATTERNS.MD ├── DESIGN_DEVELOPMENT.MD └── COVERAGE.md ├── env.example ├── .flake8 ├── Procfile ├── requirements.txt ├── .pre-commit-config.yaml ├── Dockerfile ├── docker-compose.yml ├── LICENSE ├── .github └── workflows │ └── codeql-analysis.yml ├── .gitignore └── README.md /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.9.7 -------------------------------------------------------------------------------- /biproductive/account/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /biproductive/habits/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /biproductive/home/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /biproductive/biproductive/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /biproductive/productivity/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /biproductive/account/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /biproductive/habits/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /biproductive/home/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /biproductive/report_generator/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /biproductive/productivity/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /biproductive/report_generator/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /biproductive/home/models.py: -------------------------------------------------------------------------------- 1 | # Create your models here. 2 | -------------------------------------------------------------------------------- /biproductive/home/tests.py: -------------------------------------------------------------------------------- 1 | # Create your tests here. 2 | -------------------------------------------------------------------------------- /biproductive/account/admin.py: -------------------------------------------------------------------------------- 1 | # Register your models here. 2 | -------------------------------------------------------------------------------- /biproductive/account/models.py: -------------------------------------------------------------------------------- 1 | # Create your models here. 2 | -------------------------------------------------------------------------------- /biproductive/home/admin.py: -------------------------------------------------------------------------------- 1 | # Register your models here. 2 | -------------------------------------------------------------------------------- /biproductive/report_generator/admin.py: -------------------------------------------------------------------------------- 1 | # Register your models here. 2 | -------------------------------------------------------------------------------- /biproductive/report_generator/models.py: -------------------------------------------------------------------------------- 1 | # Create your models here. 2 | -------------------------------------------------------------------------------- /docs/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rizvansky/biproductive/HEAD/docs/demo.gif -------------------------------------------------------------------------------- /docs/design_sketch.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rizvansky/biproductive/HEAD/docs/design_sketch.pdf -------------------------------------------------------------------------------- /docs/home_page_figma.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rizvansky/biproductive/HEAD/docs/home_page_figma.png -------------------------------------------------------------------------------- /docs/login_page_figma.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rizvansky/biproductive/HEAD/docs/login_page_figma.png -------------------------------------------------------------------------------- /docs/use-case-diagram.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rizvansky/biproductive/HEAD/docs/use-case-diagram.jpg -------------------------------------------------------------------------------- /docs/dynamic-view-updated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rizvansky/biproductive/HEAD/docs/dynamic-view-updated.png -------------------------------------------------------------------------------- /docs/low_fidelity_prototype.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rizvansky/biproductive/HEAD/docs/low_fidelity_prototype.png -------------------------------------------------------------------------------- /docs/use-case-diagram-updated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rizvansky/biproductive/HEAD/docs/use-case-diagram-updated.png -------------------------------------------------------------------------------- /env.example: -------------------------------------------------------------------------------- 1 | SECRET_KEY= 2 | DB_USER=postgres 3 | DB_PASSWORD=postgres 4 | DB_HOST=db 5 | DB_PORT=5432 6 | DB_NAME=postgres 7 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = D203 3 | exclude = biproductive/biproductive/settings.py, 4 | README.md 5 | max-line-length = 100 -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: sh -c 'cd biproductive && python manage.py makemigrations && python manage.py migrate && gunicorn biproductive.wsgi --log-file -' -------------------------------------------------------------------------------- /biproductive/home/data/habits.csv: -------------------------------------------------------------------------------- 1 | date,1_habit,habit_2 2 | no,no,no 3 | no,no,no 4 | no,no,no 5 | no,no,no 6 | no,no,no 7 | no,no,no 8 | no,no,no 9 | -------------------------------------------------------------------------------- /biproductive/home/data/correlation.csv: -------------------------------------------------------------------------------- 1 | Habit,Correlation 2 | 1_habit,Lacks data 3 | brain-activity,Lacks data 4 | date,Lacks data 5 | habit_2,Lacks data 6 | -------------------------------------------------------------------------------- /biproductive/home/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import home_view 4 | 5 | urlpatterns = [path("", home_view, name="home")] 6 | -------------------------------------------------------------------------------- /biproductive/report_generator/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ReportGeneratorConfig(AppConfig): 5 | name = 'report_generator' 6 | -------------------------------------------------------------------------------- /biproductive/templates/static/assets/github_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rizvansky/biproductive/HEAD/biproductive/templates/static/assets/github_logo.png -------------------------------------------------------------------------------- /biproductive/productivity/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import ProductivityCheck 4 | 5 | admin.site.register(ProductivityCheck) 6 | -------------------------------------------------------------------------------- /biproductive/productivity/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | urlpatterns = [ 6 | path("", views.productivity, name="game"), 7 | ] 8 | -------------------------------------------------------------------------------- /biproductive/home/data/productivity.csv: -------------------------------------------------------------------------------- 1 | date,brain-activity 2 | 2021-10-01,0 3 | 2021-10-02,0 4 | 2021-10-03,0 5 | 2021-10-04,0 6 | 2021-10-05,0 7 | 2021-10-06,0 8 | 2021-10-07,0 9 | -------------------------------------------------------------------------------- /biproductive/habits/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Habit, HabitUsage 4 | 5 | admin.site.register(Habit) 6 | admin.site.register(HabitUsage) 7 | -------------------------------------------------------------------------------- /biproductive/home/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class HomeConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "home" 7 | -------------------------------------------------------------------------------- /biproductive/habits/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class HabitsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'habits' 7 | -------------------------------------------------------------------------------- /biproductive/account/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AccountsConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "account" 7 | -------------------------------------------------------------------------------- /biproductive/productivity/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ProductivityConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'productivity' 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django==3.1.2 2 | django-environ==0.4.5 3 | psycopg2-binary==2.9.1 4 | whitenoise==5.3.0 5 | gunicorn==20.1.0 6 | pandas==1.3.3 7 | temppathlib==1.1.0 8 | seaborn==0.11.2 9 | tabulate==0.8.9 10 | md2pdf==0.5 11 | matplotlib==3.4.3 12 | -------------------------------------------------------------------------------- /biproductive/habits/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | app_name = "habits" 6 | urlpatterns = [ 7 | path("add_habit/", views.add_habit, name="add_habit"), 8 | path("track_habits/", views.track_habits, name='track_habits') 9 | ] 10 | -------------------------------------------------------------------------------- /biproductive/account/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import login_view, logout_view, signup 4 | 5 | urlpatterns = [ 6 | path("signup/", signup, name="signup"), 7 | path("logout/", logout_view, name="logout"), 8 | path("login", login_view, name="login"), 9 | ] 10 | -------------------------------------------------------------------------------- /biproductive/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base_generic.html" %} 2 | 3 | {% block title %} Index page {% endblock %} 4 | 5 | {% block content %} 6 | 7 | {% if message %} 8 |
9 |

{{ message }}

10 |
11 | 12 | {% endif %} 13 | 14 | {% endblock %} -------------------------------------------------------------------------------- /biproductive/utils.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | 4 | def last_n_days(n: int): 5 | today = datetime.datetime.today() 6 | dates = [] 7 | for i in range(n): 8 | delta = datetime.timedelta(days=n - 1 - i) 9 | day = (today - delta).date() 10 | dates.append(day) 11 | return dates 12 | -------------------------------------------------------------------------------- /docs/AUTHORS.MD: -------------------------------------------------------------------------------- 1 | # The core BiProductive team 2 | * [Rizvan Iskaliev](https://github.com/rizvansky) - DevOps Engineer, Backend Developer 3 | * [Maxim Faleev](https://github.com/implausibleDenyability) - Backend Developer, Data Scientist 4 | * [Shamil Arslanov](https://github.com/homomorfism) - Product Owner, Frontend Developer 5 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/ambv/black 3 | rev: 20.8b1 4 | hooks: 5 | - id: black 6 | - repo: https://gitlab.com/pycqa/flake8 7 | rev: 3.8.4 8 | hooks: 9 | - id: flake8 10 | - repo: https://github.com/timothycrosley/isort 11 | rev: 5.7.0 12 | hooks: 13 | - id: isort -------------------------------------------------------------------------------- /biproductive/templates/static/assets/email_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /biproductive/templates/static/scripts/datatables.js: -------------------------------------------------------------------------------- 1 | let table_data = JSON.parse(JSON.parse(document.getElementById("habit_table_data").textContent)); 2 | 3 | let columns = []; 4 | for (let column in table_data[0]) { 5 | columns.push({data: column}); 6 | } 7 | // columns.reverse() 8 | // console.log("now reversed") 9 | $("#habit_table").DataTable({ 10 | data: table_data, 11 | columns: columns, 12 | responsive: true 13 | 14 | }); -------------------------------------------------------------------------------- /biproductive/productivity/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.db import models 3 | 4 | 5 | class ProductivityCheck(models.Model): 6 | date = models.DateField() 7 | productivity_value = models.IntegerField() 8 | 9 | user = models.ForeignKey(User, on_delete=models.CASCADE) 10 | 11 | def __str__(self): 12 | return f"Productivity check: User:{self.user}, " \ 13 | f"Date:{self.date}, Value:{self.productivity_value}" 14 | -------------------------------------------------------------------------------- /biproductive/biproductive/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for biproductive project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "biproductive.settings") 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /biproductive/biproductive/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for biproductive project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "biproductive.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /biproductive/productivity/migrations/0002_auto_20210930_1943.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.2 on 2021-09-30 19:43 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('productivity', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='productivitycheck', 15 | name='date', 16 | field=models.DateField(), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /biproductive/habits/migrations/0003_habitusage_status.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.2 on 2021-10-09 18:30 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('habits', '0002_auto_20211004_1206'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='habitusage', 15 | name='status', 16 | field=models.BooleanField(default=False), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /biproductive/account/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib.auth.forms import UserCreationForm 3 | from django.contrib.auth.models import User 4 | 5 | 6 | class SignUpForm(UserCreationForm): 7 | email = forms.EmailField( 8 | max_length=254, help_text="Required. Inform a valid email address." 9 | ) 10 | 11 | class Meta: 12 | model = User 13 | fields = ( 14 | "username", 15 | "email", 16 | "password1", 17 | "password2", 18 | ) 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9-slim 2 | ENV PYTHONUNBUFFERED=1 3 | WORKDIR /biproductive 4 | 5 | # Install weasyprint dependencies 6 | RUN apt-get update && \ 7 | apt-get install -y --no-install-recommends \ 8 | python3-dev \ 9 | python3-pip \ 10 | python3-cffi \ 11 | libcairo2 \ 12 | libpango1.0-0 \ 13 | libgdk-pixbuf2.0-0 \ 14 | libffi-dev \ 15 | shared-mime-info && \ 16 | apt-get -y clean && \ 17 | rm -rf /var/lib/apt/lists/* 18 | COPY requirements.txt /biproductive/ 19 | RUN pip install -r requirements.txt 20 | COPY . /biproductive/ 21 | -------------------------------------------------------------------------------- /biproductive/habits/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.db import models 3 | 4 | 5 | class Habit(models.Model): 6 | habit_name = models.CharField(max_length=30) 7 | creation_date = models.DateTimeField() 8 | 9 | user = models.ForeignKey(User, on_delete=models.CASCADE) 10 | 11 | def __str__(self): 12 | return self.habit_name 13 | 14 | 15 | class HabitUsage(models.Model): 16 | usage_time = models.DateTimeField() 17 | status = models.BooleanField(default=False) 18 | 19 | habit = models.ForeignKey(Habit, on_delete=models.CASCADE) 20 | 21 | def __str__(self): 22 | return f"{self.habit} usage: {self.usage_time}, status {self.status}" 23 | -------------------------------------------------------------------------------- /biproductive/habits/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.forms import RadioSelect 3 | 4 | from .models import Habit 5 | 6 | 7 | class AddHabitForm(forms.Form): 8 | habit_name = forms.CharField(label='Habit name', max_length=100) 9 | 10 | 11 | class HabitTrackingForm(forms.Form): 12 | def __init__(self, user, *args, **kwargs): 13 | super().__init__(*args, **kwargs) 14 | habits = Habit.objects.filter(user=user) 15 | for i, _ in enumerate(habits): 16 | field_name = habits[i].habit_name 17 | self.fields[field_name] = forms.ChoiceField( 18 | widget=RadioSelect(), 19 | choices=[('True', 'Yes'), ('False', 'No')], 20 | ) 21 | -------------------------------------------------------------------------------- /biproductive/templates/static/styles/base.css: -------------------------------------------------------------------------------- 1 | .custom-header-button { 2 | background-color: #52ab98; 3 | } 4 | 5 | .custom-footer-color { 6 | background-color: #2b6777; 7 | color: #ffffff; 8 | } 9 | 10 | html, 11 | body { 12 | margin: 0; 13 | height: 100%; 14 | min-height: 100%; 15 | } 16 | 17 | body { 18 | display: flex; 19 | flex-direction: column; 20 | } 21 | 22 | header, 23 | footer { 24 | flex: none; 25 | } 26 | 27 | main { 28 | -webkit-overflow-scrolling: touch; 29 | flex: auto; 30 | background-color: #c8d8e4; 31 | } 32 | 33 | .custom-list-size { 34 | width: 50%; 35 | } 36 | 37 | .page-container { 38 | display: flex; 39 | min-height: 100vh; 40 | flex-direction: column; 41 | } 42 | 43 | .main-container { 44 | flex: 1; 45 | } 46 | -------------------------------------------------------------------------------- /biproductive/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "biproductive.settings") 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == "__main__": 22 | main() 23 | -------------------------------------------------------------------------------- /biproductive/home/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.contrib.auth.decorators import login_required 4 | from django.shortcuts import render 5 | from habits.scripts import load_last_n_days_habit_usage 6 | from productivity.scripts import load_last_n_days_productivity_checks 7 | 8 | 9 | @login_required(login_url="login") 10 | def home_view(request): 11 | week_habit_usage = load_last_n_days_habit_usage(request.user, n=7) 12 | week_productivity = load_last_n_days_productivity_checks(request.user, n=7) 13 | return render( 14 | request=request, 15 | template_name="home.html", 16 | context={ 17 | "brain_chart": json.dumps(week_productivity), 18 | "habit_table_data": json.dumps(week_habit_usage), 19 | "habit_names": list(week_habit_usage[0].keys()), 20 | }, 21 | ) 22 | -------------------------------------------------------------------------------- /biproductive/habits/migrations/0002_auto_20211004_1206.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.2 on 2021-10-04 12:06 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("habits", "0001_initial"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="habit", 14 | name="id", 15 | field=models.AutoField( 16 | auto_created=True, primary_key=True, serialize=False, verbose_name="ID" 17 | ), 18 | ), 19 | migrations.AlterField( 20 | model_name="habitusage", 21 | name="id", 22 | field=models.AutoField( 23 | auto_created=True, primary_key=True, serialize=False, verbose_name="ID" 24 | ), 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | services: 3 | db: 4 | image: postgres 5 | container_name: deploy-postgres 6 | restart: always 7 | env_file: 8 | - biproductive/biproductive/.env 9 | ports: 10 | - "5432:5432" 11 | volumes: 12 | - ./postgres_data:/var/lib/postgresql/data 13 | environment: 14 | - POSTGRES_DB=postgres 15 | - POSTGRES_USER=postgres 16 | - POSTGRES_PASSWORD=postgres 17 | - PGDATA=/tmp 18 | web: 19 | build: . 20 | command: bash -c 'while !``` 4 | - Go to the cloned repository directory 5 | - Check that your fork is the ```origin``` remote by running ```git remote -v``` and checking that the URL of your fork is next to the word ```origin``` 6 | - Add this project repository to the ```upstream``` remote by ```git remote add upstream https://github.com/rizvansky/biproductive.git``` 7 | - Create a new branch from ```dev``` branch 8 | - Fix or create and implement a new feature in your branch 9 | - Follow the code style of the project anc comment the code you write 10 | - Modify the documentation as needed 11 | - Commit your changes. Write meaningful commit message 12 | - Push your branch to your fork on GitHub (to the ```origin```) 13 | - Go to your fork's GitHub page and open the pull request to the ```dev``` branch 14 | -------------------------------------------------------------------------------- /biproductive/productivity/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.2 on 2021-09-30 17:29 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 | initial = True 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='ProductivityCheck', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('date', models.DateTimeField()), 22 | ('productivity_value', models.IntegerField()), 23 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 24 | ], 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /biproductive/productivity/scripts.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from utils import last_n_days 4 | 5 | from .models import ProductivityCheck 6 | 7 | 8 | def load_last_n_days_productivity_checks(user, n: int): 9 | checks = ProductivityCheck.objects.filter(user=user) 10 | days = last_n_days(n) 11 | data = {"date": list(map(str, days)), "brain-activity": []} 12 | for day in days: 13 | if checks.filter(date=day): 14 | data["brain-activity"].append(checks.filter(date=day)[0].productivity_value) 15 | else: 16 | data["brain-activity"].append(0) 17 | return data 18 | 19 | 20 | def add_productivity_specified_day(user, day: datetime.date, value: int): 21 | if ProductivityCheck.objects.filter(user=user, date=day): 22 | check: ProductivityCheck = ProductivityCheck.objects.get(user=user, date=day) 23 | check.productivity_value = value 24 | check.save() 25 | else: 26 | check = ProductivityCheck(user=user, date=day, productivity_value=value) 27 | check.save() 28 | -------------------------------------------------------------------------------- /biproductive/report_generator/tests.py: -------------------------------------------------------------------------------- 1 | # Create your tests here. 2 | 3 | from django.contrib.auth.models import User 4 | from django.http import FileResponse 5 | from django.test import TestCase 6 | from django.urls import reverse 7 | 8 | 9 | class ReportGenerationTestCase(TestCase): 10 | def setUp(self): 11 | self.username = "ufaiisluntvia" 12 | self.password = "kneJe^fBgGR#" 13 | 14 | user = User.objects.create_user( 15 | username=self.username, 16 | password=self.password, 17 | is_active=True 18 | ) 19 | user.save() 20 | self.assertTrue(User.objects.filter(username=self.username).exists()) 21 | 22 | def test_report_generation(self): 23 | response = self.client.post( 24 | reverse('login'), 25 | data={'username': self.username, 'password': self.password} 26 | ) 27 | 28 | self.assertEqual(response.url, reverse('home'), 'failed to login') 29 | 30 | response = self.client.get(reverse('report')) 31 | self.assertTrue(type(response) == FileResponse) 32 | -------------------------------------------------------------------------------- /biproductive/habits/scripts.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from utils import last_n_days 4 | 5 | from .models import Habit, HabitUsage 6 | 7 | 8 | def load_last_n_days_habit_usage(user, n: int): 9 | habits = Habit.objects.filter(user=user) 10 | days = last_n_days(n) 11 | data = [] 12 | for day in days: 13 | row = {"date": f"{day}"} 14 | for habit in habits: 15 | if habit.habitusage_set.filter(usage_time__date=day, status=True): 16 | row[f"{habit}"] = "+" 17 | elif habit.habitusage_set.filter(usage_time__date=day, status=False): 18 | row[f"{habit}"] = "-" 19 | else: 20 | row[f"{habit}"] = ' ' 21 | data.append(row) 22 | return data 23 | 24 | 25 | def track_habits_specified_day(user, day: datetime.date, fill: str): 26 | habits = Habit.objects.filter(user=user) 27 | for habit, status in zip(habits, fill): 28 | if status == ' ': 29 | continue 30 | else: 31 | status = (status == '+') 32 | usage = HabitUsage(usage_time=day, status=status, habit=habit) 33 | usage.save() 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Rizvan Iskaliev, Maxim Faleev, Shamil Arslanov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /biproductive/home/scripts.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | 4 | from habits.scripts import load_last_n_days_habit_usage 5 | from productivity.scripts import load_last_n_days_productivity_checks 6 | 7 | 8 | def discretize_correlation(coefficient: np.float64): 9 | if coefficient > 0.8: 10 | return "Positive" 11 | elif coefficient < -0.8: 12 | return "Negative" 13 | elif np.isnan(coefficient): 14 | return "Lacks data" 15 | else: 16 | return "No correlation found" 17 | 18 | 19 | def correlation(user): 20 | habit_usage = load_last_n_days_habit_usage(user, 7) 21 | habit_usage = pd.DataFrame(habit_usage) 22 | productivity = load_last_n_days_productivity_checks(user, 7) 23 | productivity = pd.DataFrame(productivity) 24 | binary_habit_usage: pd.DataFrame = habit_usage.replace(['+', '-', ' '], [1, 0, np.NaN]) 25 | correlation_df = binary_habit_usage.corrwith(productivity['brain-activity']) 26 | correlation_df = pd.DataFrame(correlation_df, columns=['Correlation']) 27 | correlation_df.index.name = 'Habit' 28 | correlation_df['Correlation'] = correlation_df['Correlation'].apply(discretize_correlation) 29 | return habit_usage, productivity, correlation_df 30 | -------------------------------------------------------------------------------- /biproductive/biproductive/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | biproductive URL Configuration 3 | 4 | The `urlpatterns` list routes URLs to views. For more information please see: 5 | https://docs.djangoproject.com/en/3.2/topics/http/urls/ 6 | Examples: 7 | Function views 8 | 1. Add an import: from my_app import views 9 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 10 | Class-based views 11 | 1. Add an import: from other_app.views import Home 12 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 13 | Including another URLconf 14 | 1. Import the include() function: from django.urls import include, path 15 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 16 | """ 17 | from django.contrib import admin 18 | from django.urls import include, path 19 | from django.views.generic import RedirectView 20 | 21 | from report_generator.views import download_view 22 | 23 | urlpatterns = [ 24 | path("admin/", admin.site.urls), 25 | path("home/", include("home.urls")), 26 | path('habits/', include("habits.urls")), 27 | path("", RedirectView.as_view(url="/home/", permanent=True)), 28 | path("account/", include("account.urls")), 29 | path("productivity/", include("productivity.urls")), 30 | path('report/', download_view, name='report') 31 | ] 32 | -------------------------------------------------------------------------------- /biproductive/productivity/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | from datetime import date 3 | 4 | from django.contrib.auth.decorators import login_required 5 | from django.http import JsonResponse 6 | from django.shortcuts import render 7 | from django.views.decorators.csrf import csrf_exempt 8 | 9 | from .models import ProductivityCheck 10 | 11 | 12 | @csrf_exempt 13 | @login_required(login_url="login") 14 | def productivity(request): 15 | if request.method == "GET": 16 | checks = ProductivityCheck.objects.filter(date=date.today(), user=request.user) 17 | permission = len(checks) == 0 18 | if permission: 19 | return render(request, "game.html", {"permission": permission}, status=200) 20 | 21 | else: 22 | return render( 23 | request, 24 | "index.html", 25 | {"message": "You have completed the test today, come back tomorrow"}, 26 | status=403, 27 | ) 28 | elif request.method == "POST": 29 | post_data = json.loads(request.body.decode("utf-8")) 30 | score = int(post_data["prod_score"]) 31 | user = request.user 32 | check = ProductivityCheck( 33 | user=user, productivity_value=score, date=date.today() 34 | ) 35 | check.save() 36 | return JsonResponse({}, status=200) 37 | -------------------------------------------------------------------------------- /biproductive/templates/static/scripts/brain-activity-chart.js: -------------------------------------------------------------------------------- 1 | var ctx = document.getElementById('brainChart').getContext('2d'); 2 | 3 | const brain_chart_data = JSON.parse(JSON.parse(document.getElementById('brain_chart').textContent)) 4 | 5 | var myChart = new Chart(ctx, { 6 | type: 'bar', 7 | data: { 8 | labels: brain_chart_data['date'], 9 | datasets: [{ 10 | label: 'Brain activity', 11 | data: brain_chart_data['brain-activity'], 12 | backgroundColor: [ 13 | 'rgba(255, 99, 132, 0.2)', 14 | 'rgba(54, 162, 235, 0.2)', 15 | 'rgba(255, 206, 86, 0.2)', 16 | 'rgba(75, 192, 192, 0.2)', 17 | 'rgba(153, 102, 255, 0.2)', 18 | 'rgba(255, 159, 64, 0.2)' 19 | ], 20 | borderColor: [ 21 | 'rgba(255, 99, 132, 1)', 22 | 'rgba(54, 162, 235, 1)', 23 | 'rgba(255, 206, 86, 1)', 24 | 'rgba(75, 192, 192, 1)', 25 | 'rgba(153, 102, 255, 1)', 26 | 'rgba(255, 159, 64, 1)' 27 | ], 28 | borderWidth: 1 29 | }] 30 | }, 31 | options: { 32 | responsive: true, 33 | scales: { 34 | y: { 35 | beginAtZero: true 36 | } 37 | } 38 | } 39 | }); -------------------------------------------------------------------------------- /docs/HEROKU.MD: -------------------------------------------------------------------------------- 1 | # How to deploy to the Heroku 2 | - Register on Heroku if you are not registered (https://signup.heroku.com/) 3 | - Install Heroku CLI (https://devcenter.heroku.com/articles/heroku-cli) 4 | - Login to Heroku terminal by ```heroku login``` 5 | - Create heroku app by ```heroku create``` or ```heroku create ``` for custom name of the application 6 | - Run ```heroku git:remote -a ``` 7 | - Open ```settings.py``` file and modify ```ALLOWED_HOSTS``` by adding `````` 8 | - Commit changes by ```git commit -m ``` 9 | - Create PostgreSQL database on Heroku by ```heroku addons:create heroku-postgresql:hobby-dev``` 10 | - Go to ```https://dashboard.heroku.com/apps//settings``` and click on the ```Reveal Config Vars``` and copy 11 | the ```DATABASE URL```. It has the following structure: 12 | ```postgres://:@:/``` 13 | - Set these variables as Heroku environment variables below ```DATABASE_URL``` with the keys ```DB_USER```, 14 | ```DB_PASSWORD```, ```DB_HOST```, ```DB_PORT```, ```DB_NAME``` and the values retrieved from the ```DATABASE_URL``` 15 | - Add the ```SECRET_KEY``` environment variable and set it to some value 16 | - Add the ```DISABLE_COLLECTSTATIC``` environment variable and set it to 1 17 | - Add the ```HEROKU``` environment variable and set it to 1 18 | - if you are on a main branch then run ```git push heroku main```, otherwise run 19 | ```git push heroku :main``` -------------------------------------------------------------------------------- /docs/PATTERNS.MD: -------------------------------------------------------------------------------- 1 | # SOLID principles 2 | - **(S)** Every application has only one role. Habit tracking application knows nothing about productivity or correlation, 3 | Productivity application knows nothing about habit tracking and correlation, correlation analysis knows nothing about 4 | habit app and productivity app and just operates on the data, stored in the database. 5 | The only reason for the habit app to change is the user updating habits, the only reason for the productivity app to 6 | change is the user, testing their productivities, the only reason for the correlation app to change is the user, 7 | querying the analysis. 8 | 9 | - **(O) (L)** Our project is very flat, and there is no actual hierarchy of concepts and inheritance used =) 10 | 11 | - **(I)** In every app, where the database is used, Django models define the interface for the database fields and 12 | methods. In every app, where the communication between frontend and backend is used, Django forms define the interface 13 | for the message structure and access methods. 14 | 15 | - **(D)** The database interface does not depend on its structure, but the structure depends on the interface. 16 | Applications' implementation depends on abstraction in the architecture design. 17 | 18 | # Design patterns used 19 | - Template method - correlation analysis is a skeleton of the algorithm, that calls other procedures. Redefining these 20 | procedures will change the algorithm's behavior. 21 | 22 | - Module - every app is a module with a single goal and several highly related methods to achieve that goal. 23 | - ORM - operations on all databases are performed through abstract ORM classes. 24 | -------------------------------------------------------------------------------- /biproductive/habits/tests.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.test import TestCase 3 | from django.urls import reverse 4 | 5 | from .scripts import load_last_n_days_habit_usage 6 | 7 | 8 | class TestHabitTable(TestCase): 9 | def setUp(self) -> None: 10 | self.username = "testUser" 11 | self.email = "testUser" 12 | self.password = "+AV/*~K4u3k^5HC" 13 | 14 | self.user = User.objects.create_user( 15 | username=self.username, email=self.email, password=self.password 16 | ) 17 | self.user.save() 18 | 19 | def test_can_track_habits(self): 20 | self.assertTrue( 21 | self.client.login(username=self.username, password=self.password), 22 | "login failed", 23 | ) 24 | response = self.client.post(path=reverse("habits:add_habit"), 25 | data={"habit_name": "test_habit1"}) 26 | self.assertEqual(response.url, '/home/') 27 | response = self.client.post(path=reverse("habits:add_habit"), 28 | data={"habit_name": "test_habit2"}) 29 | self.assertEqual(response.url, '/home/') 30 | response = self.client.get(path=reverse("habits:track_habits")) 31 | self.assertEqual(response.status_code, 200) 32 | response = self.client.post( 33 | path=reverse("habits:track_habits"), 34 | data={ 35 | 'test_habit1': True, 36 | 'test_habit2': False, 37 | } 38 | ) 39 | self.assertEqual(response.url, '/home/') 40 | table = load_last_n_days_habit_usage(self.user, 7) 41 | self.assertEqual(len(table), 7) 42 | self.assertEqual(len(list(table[0].keys())), 3) 43 | -------------------------------------------------------------------------------- /docs/DESIGN_DEVELOPMENT.MD: -------------------------------------------------------------------------------- 1 | # Design development process 2 | ## Timeline 3 | ### 25.08.2021. Created a use case diagram 4 | We planned that the logged-in user would have three options: go to the habit tracking page, go to the productivity check 5 | page, or log out. It was assumed that on the habit tracking page, the user could create the habits and look at their 6 | usage. 7 | 8 | The guest user could proceed to the login or registration pages. 9 | 10 | ![](../docs/use-case-diagram.jpg) 11 | 12 | ### 01.09.2021. Created a [sketch of an application design](../docs/design_sketch.pdf) 13 | ### 06.09.2021. Created a low fidelity prototype of an application 14 | Initially, in the low-quality prototype, it was assumed that we would have the main page from which you can go to 3 15 | other pages 16 | * Habit tracking 17 | * Productivity test 18 | * Correlation analysis on productivity and habits 19 | 20 | We assumed that the habits page would contain a usage table, the productivity test page would contain a brain 21 | performance game, the productivity page will contain a graph and a table. We also assumed that there would be the 22 | possibility of creating a report. 23 | ![](../docs/low_fidelity_prototype.png) 24 | 25 | ### 25.09.2021. Created a high fidelity prototype of an application 26 | * Login page 27 | 28 | It was assumed that the user would be able to log in by email and password 29 | 30 | ![](../docs/login_page_figma.png) 31 | 32 | * Home page 33 | 34 | Compared to the low-fidelity prototype, the structure has changed a little and on the main page we will have all the 35 | main statistics of the user for the last few days 36 | 37 | ![](../docs/home_page_figma.png) 38 | 39 | ### 10.09.2021. Refactoring use case diagram 40 | 41 | ![](../docs/use-case-diagram-updated.png) 42 | *Adding user stories with playing web game and downloading report* -------------------------------------------------------------------------------- /biproductive/home/templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends "base_generic.html" %} 2 | {% load static %} 3 | {% block title %} Home page {% endblock %} 4 | 5 | 6 | 7 | {% block content %} 8 | 9 | 10 | {{ brain_chart|json_script:"brain_chart" }} 11 | {{ habit_table_data|json_script:'habit_table_data' }} 12 | 13 | 35 | 36 | {% endblock %} 37 | 38 | {% block scripts %} 39 | {{ block.super }} 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | {% endblock %} -------------------------------------------------------------------------------- /biproductive/habits/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.5 on 2021-09-22 16:31 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | initial = True 10 | 11 | dependencies = [ 12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name="Habit", 18 | fields=[ 19 | ( 20 | "id", 21 | models.BigAutoField( 22 | auto_created=True, 23 | primary_key=True, 24 | serialize=False, 25 | verbose_name="ID", 26 | ), 27 | ), 28 | ("habit_name", models.CharField(max_length=30)), 29 | ("creation_date", models.DateTimeField()), 30 | ( 31 | "user", 32 | models.ForeignKey( 33 | on_delete=django.db.models.deletion.CASCADE, 34 | to=settings.AUTH_USER_MODEL, 35 | ), 36 | ), 37 | ], 38 | ), 39 | migrations.CreateModel( 40 | name="HabitUsage", 41 | fields=[ 42 | ( 43 | "id", 44 | models.BigAutoField( 45 | auto_created=True, 46 | primary_key=True, 47 | serialize=False, 48 | verbose_name="ID", 49 | ), 50 | ), 51 | ("usage_time", models.DateTimeField()), 52 | ( 53 | "habit", 54 | models.ForeignKey( 55 | on_delete=django.db.models.deletion.CASCADE, to="habits.habit" 56 | ), 57 | ), 58 | ], 59 | ), 60 | ] 61 | -------------------------------------------------------------------------------- /biproductive/report_generator/views.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import datetime 3 | from pathlib import Path 4 | 5 | import seaborn as sns 6 | import temppathlib 7 | from django.contrib.auth.decorators import login_required 8 | from django.http import FileResponse, Http404 9 | from matplotlib import pyplot as plt 10 | 11 | from home.scripts import correlation 12 | 13 | 14 | # Create your views here. 15 | 16 | 17 | def download_file(request, file_path): 18 | try: 19 | file_name = f'habit-report-{datetime.now().date()}.pdf' 20 | response = FileResponse(open(file_path, 'rb')) 21 | response['content_type'] = "application/octet-stream" 22 | response['Content-Disposition'] = 'attachment; filename=' + file_name 23 | return response 24 | except Exception: 25 | raise Http404 26 | 27 | 28 | def generate_md_report(path_dir: Path, user): 29 | habit_table, productivity_table, corr = correlation(user) 30 | plt.figure(figsize=(6, 3)) 31 | plt.xticks(rotation=20) 32 | fig = sns.lineplot(data=productivity_table, x='date', y='brain-activity') 33 | plot_path = str(path_dir / 'plot.png') 34 | plt.tight_layout() 35 | fig.figure.savefig(plot_path) 36 | 37 | with open(path_dir / "output.md", 'w') as file: 38 | file.write(f'# Weekly report, generated in {datetime.now().date()}\n') 39 | file.write("### Weekly habit statistics\n") 40 | file.write(habit_table.to_markdown() + "\n") 41 | file.write("### Weekly brain activity plot\n") 42 | file.write(f"![plot]({plot_path})\n") 43 | file.write("### Weekly correlation analysis of habits\n") 44 | file.write(corr.to_markdown() + "\n") 45 | 46 | 47 | @login_required(redirect_field_name='login') 48 | def download_view(request): 49 | with temppathlib.TemporaryDirectory() as folder: 50 | generate_md_report(path_dir=folder.path, user=request.user) 51 | 52 | md_path = str(folder.path / 'output.md') 53 | pdf_path = str(folder.path / 'output.pdf') 54 | os.system(f'md2pdf {md_path} {pdf_path}') 55 | 56 | return download_file(request, pdf_path) 57 | -------------------------------------------------------------------------------- /biproductive/habits/views.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from django.contrib.auth.decorators import login_required 4 | from django.shortcuts import redirect, render 5 | from django.views.decorators.csrf import csrf_exempt 6 | 7 | from .forms import AddHabitForm, HabitTrackingForm 8 | from .models import Habit, HabitUsage 9 | 10 | 11 | @csrf_exempt 12 | @login_required(login_url="login") 13 | def add_habit(request): 14 | if request.method == "POST": 15 | form = AddHabitForm(request.POST) 16 | if form.is_valid(): 17 | habit_name = form.cleaned_data["habit_name"] 18 | habit = Habit( 19 | habit_name=habit_name, user=request.user, creation_date=datetime.now() 20 | ) 21 | habit.save() 22 | return redirect("home") 23 | else: 24 | return render(request, "add_habit.html", context={"form": form}, status=401) 25 | else: 26 | form = AddHabitForm() 27 | return render(request, "add_habit.html", {"form": form}, status=200) 28 | 29 | 30 | @csrf_exempt 31 | @login_required(login_url="login") 32 | def track_habits(request): 33 | if request.method == 'POST': 34 | form = HabitTrackingForm(request.user, request.POST) 35 | if form.is_valid(): 36 | for item in form.cleaned_data.items(): 37 | if item[1] == 'True': 38 | habit = Habit.objects.filter(user=request.user, habit_name=item[0])[0] 39 | usage = HabitUsage(habit=habit, usage_time=datetime.now(), status=True) 40 | usage.save() 41 | elif item[1] == 'False': 42 | habit = Habit.objects.filter(user=request.user, habit_name=item[0])[0] 43 | usage = HabitUsage(habit=habit, usage_time=datetime.now(), status=False) 44 | usage.save() 45 | return redirect('home') 46 | else: 47 | return render(request, "track_habits.html", context={"form": form}, status=401) 48 | else: 49 | form = HabitTrackingForm(request.user) 50 | return render(request, "track_habits.html", {'form': form}, status=200) 51 | -------------------------------------------------------------------------------- /biproductive/productivity/tests.py: -------------------------------------------------------------------------------- 1 | import json 2 | from datetime import date 3 | from http import HTTPStatus 4 | 5 | from django.contrib.auth.models import User 6 | from django.test import TestCase 7 | from django.urls import reverse 8 | from productivity.models import ProductivityCheck 9 | 10 | 11 | class TestProductivityGame(TestCase): 12 | def setUp(self) -> None: 13 | self.username = "testUser" 14 | self.email = "testUser" 15 | self.password = "+AV/*~K\4u3k^5HC" 16 | 17 | self.user = User.objects.create_user( 18 | username=self.username, email=self.email, password=self.password 19 | ) 20 | self.user.save() 21 | 22 | def test_can_play(self): 23 | self.assertTrue( 24 | self.client.login(username=self.username, password=self.password), 25 | "login failed", 26 | ) 27 | response = self.client.get(path=reverse("game"), data={}) 28 | self.assertEqual(response.status_code, 200) 29 | 30 | def test_already_played(self): 31 | productivity = ProductivityCheck.objects.create( 32 | user=self.user, productivity_value=50, date=date.today() 33 | ) 34 | productivity.save() 35 | 36 | self.assertTrue( 37 | self.client.login(username=self.username, password=self.password), 38 | "login failed", 39 | ) 40 | response = self.client.get(path=reverse("game"), data={}) 41 | self.assertEqual(response.status_code, 403) 42 | 43 | def test_not_logged_in(self): 44 | response = self.client.get(path=reverse("game"), data={}) 45 | self.assertEqual(response.status_code, 302) # Redirect to login page 46 | 47 | def test_can_send_fetch_query(self): 48 | self.assertTrue( 49 | self.client.login(username=self.username, password=self.password), 50 | "login failed", 51 | ) 52 | json_data = json.dumps({"prod_score": 50}) 53 | response = self.client.generic( 54 | method="POST", path=reverse("game"), data=json_data 55 | ) 56 | self.assertEqual(response.status_code, HTTPStatus.OK) 57 | 58 | response = self.client.get(path=reverse("game"), data={}) 59 | self.assertEqual(response.status_code, HTTPStatus.FORBIDDEN) 60 | -------------------------------------------------------------------------------- /biproductive/habits/templates/add_habit.html: -------------------------------------------------------------------------------- 1 | {% extends "base_generic.html" %} 2 | 3 | {% block title %} Add habit {% endblock %} 4 | 5 | {% block content %} 6 | 7 | {% if message %}

{{ message }}

{% endif %} 8 | 9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |

Add habit

17 | 18 |
19 | {% csrf_token %} 20 | {{ form.non_field_errors }} 21 | 22 |
23 | 24 | 26 | 29 | {{ form.habit_name.errors }} 30 | 31 |
32 | 33 | 34 |
35 | 39 |
40 |
41 | 42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | 50 | 51 | {% endblock %} -------------------------------------------------------------------------------- /biproductive/productivity/templates/game.html: -------------------------------------------------------------------------------- 1 | {% extends "base_generic.html" %} 2 | 3 | {% load static %} 4 | 5 | {% block title %} Productivity check {% endblock %} 6 | 7 | {% block scripts %} 8 | {{ block.super }} 9 | 10 | 11 | {% endblock %} 12 | 13 | {% block content %} 14 | 15 | 16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | 38 |
39 | 0 40 | 0 41 |
42 |
43 | 44 |
45 |
46 | Chose your theme ! 47 |

Pokemon

48 |

Star Wars

49 |

Lord of the Ring

50 |

Disney

51 |

Pixar

52 |

Harry Potter

53 |
54 |
55 | 56 | 57 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | {% endblock %} 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '28 3 * * 0' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v2 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /biproductive/account/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import authenticate, login, logout 2 | from django.contrib.auth.decorators import login_required 3 | from django.contrib.auth.forms import AuthenticationForm 4 | from django.shortcuts import redirect, render 5 | from django.views.decorators.csrf import csrf_exempt 6 | 7 | from .forms import SignUpForm 8 | 9 | 10 | @csrf_exempt 11 | def signup(request): 12 | if request.method == "POST": 13 | form = SignUpForm(request.POST) 14 | if form.is_valid(): 15 | user = form.save() 16 | 17 | # load the profile instance created by the signal 18 | user.refresh_from_db() 19 | user.save() 20 | raw_password = form.cleaned_data.get("password1") 21 | user = authenticate(username=user.username, password=raw_password) 22 | login(request, user) 23 | return redirect("home") 24 | 25 | else: 26 | return render(request, "signup.html", context={"form": form}, status=401) 27 | else: 28 | form = SignUpForm() 29 | return render(request, "signup.html", {"form": form}, status=200) 30 | 31 | 32 | @login_required(login_url="login") 33 | @csrf_exempt 34 | def logout_view(request): 35 | username = "No idea" 36 | if request.user.is_authenticated: 37 | username = request.user.username 38 | 39 | logout(request) 40 | 41 | return render( 42 | request, 43 | "index.html", 44 | context={"message": f"{username} has logged out!"}, 45 | status=200, 46 | ) 47 | 48 | 49 | @csrf_exempt 50 | def login_view(request): 51 | if request.method == "POST": 52 | form = AuthenticationForm(data=request.POST) 53 | # print(f"cleaned data: {form.cleaned_data}") 54 | if form.is_valid(): 55 | 56 | data = form.cleaned_data 57 | 58 | user = authenticate(username=data["username"], password=data["password"]) 59 | 60 | if user is not None: 61 | login(request, user) 62 | return redirect("home") 63 | else: 64 | return render( 65 | request, 66 | "login.html", 67 | { 68 | "form": AuthenticationForm(), 69 | "message": "Invalid login", 70 | }, 71 | status=401, 72 | ) 73 | else: 74 | return render( 75 | request, 76 | "login.html", 77 | {"form": AuthenticationForm()}, 78 | status=401, 79 | ) 80 | 81 | else: 82 | form = AuthenticationForm() 83 | 84 | return render(request, "login.html", {"form": form}) 85 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | .env 7 | .idea 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | cover/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | .pybuilder/ 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | # For a library or package, you might want to ignore these files since the code is 89 | # intended to run in multiple environments; otherwise, check them in: 90 | # .python-version 91 | 92 | # pipenv 93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 96 | # install all needed dependencies. 97 | #Pipfile.lock 98 | 99 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 100 | __pypackages__/ 101 | 102 | # Celery stuff 103 | celerybeat-schedule 104 | celerybeat.pid 105 | 106 | # SageMath parsed files 107 | *.sage.py 108 | 109 | # Environments 110 | .venv 111 | env/ 112 | venv/ 113 | ENV/ 114 | env.bak/ 115 | venv.bak/ 116 | 117 | # Spyder project settings 118 | .spyderproject 119 | .spyproject 120 | 121 | # Rope project settings 122 | .ropeproject 123 | 124 | # mkdocs documentation 125 | /site 126 | 127 | # mypy 128 | .mypy_cache/ 129 | .dmypy.json 130 | dmypy.json 131 | 132 | # Pyre type checker 133 | .pyre/ 134 | 135 | # pytype static type analyzer 136 | .pytype/ 137 | 138 | # Cython debug symbols 139 | cython_debug/ 140 | 141 | # Static files 142 | biproductive/static/ 143 | -------------------------------------------------------------------------------- /biproductive/account/tests.py: -------------------------------------------------------------------------------- 1 | # Create your tests here. 2 | from django.contrib import auth 3 | from django.contrib.auth.models import User 4 | from django.test import TestCase 5 | from django.urls import reverse 6 | 7 | 8 | class RegisterTestCase(TestCase): 9 | def setUp(self) -> None: 10 | self.email = "ufai@isluntvia.com" 11 | self.username = "ufaiisluntvia" 12 | self.password = "kneJe^fBgGR#" 13 | 14 | def test_can_register(self) -> None: 15 | response = self.client.post( 16 | reverse("signup"), 17 | data={ 18 | "email": self.email, 19 | "username": self.username, 20 | "password1": self.password, 21 | "password2": self.password, 22 | }, 23 | ) 24 | 25 | self.assertEqual(response.url, reverse("home")) 26 | 27 | user = auth.get_user(self.client) 28 | self.assertTrue(user.is_authenticated) 29 | 30 | def test_can_not_register_if_username_exists(self): 31 | User.objects.create( 32 | username=self.username, email=self.email, password=self.password 33 | ) 34 | 35 | response = self.client.post( 36 | reverse("signup"), 37 | data={ 38 | "email": self.email, 39 | "username": self.username, 40 | "password1": self.password, 41 | "password2": self.password, 42 | }, 43 | ) 44 | 45 | self.assertEqual(response.status_code, 401) 46 | 47 | user = auth.get_user(self.client) 48 | self.assertFalse(user.is_authenticated) 49 | 50 | 51 | class LoginTestCase(TestCase): 52 | def setUp(self) -> None: 53 | self.username = "ufaiisluntvia" 54 | self.password = "kneJe^fBgGR#" 55 | 56 | user = User.objects.create_user( 57 | username=self.username, password=self.password, is_active=True 58 | ) 59 | user.save() 60 | self.assertTrue(User.objects.filter(username=self.username).exists()) 61 | 62 | def test_can_login(self) -> None: 63 | response = self.client.post( 64 | reverse("login"), 65 | data={"username": self.username, "password": self.password}, 66 | ) 67 | 68 | self.assertEqual(response.url, reverse("home")) 69 | user = auth.get_user(self.client) 70 | self.assertTrue(user.is_authenticated) 71 | 72 | def test_can_not_login_with_invalid_username(self): 73 | response = self.client.post( 74 | reverse("login"), 75 | data={"username": "erweirufhuierfhfheiourh", "password": self.password}, 76 | ) 77 | 78 | self.assertEqual(response.status_code, 401) 79 | 80 | user = auth.get_user(self.client) 81 | self.assertFalse(user.is_authenticated) 82 | 83 | def test_can_not_login_with_invalid_password(self): 84 | response = self.client.post( 85 | reverse("login"), 86 | data={"username": self.username, "password": "K\a&~jY2cdAEXks"}, 87 | ) 88 | 89 | self.assertEqual(response.status_code, 401) 90 | user = auth.get_user(self.client) 91 | self.assertFalse(user.is_authenticated) 92 | -------------------------------------------------------------------------------- /biproductive/account/templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base_generic.html" %} 2 | 3 | {% block title %} Sign Up {% endblock %} 4 | 5 | {% block content %} 6 | 7 | {% if message %}

{{ message }}

{% endif %} 8 | 9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |

Login

17 | 18 |
19 | {% csrf_token %} 20 | {{ form.non_field_errors }} 21 | 22 |
23 | 24 | 26 | 29 | {{ form.username.errors }} 30 | 31 |
32 | 33 | 34 |
35 | 36 | 38 | 41 | {{ form.password.errors }} 42 | 43 |
44 | 45 | 46 |
47 | 51 |
52 | 53 |

Don't have an account yet? Register here 55 |

56 | 57 |
58 | 59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | 67 | 68 | {% endblock %} -------------------------------------------------------------------------------- /biproductive/habits/templates/track_habits.html: -------------------------------------------------------------------------------- 1 | {% extends "base_generic.html" %} 2 | 3 | {% block title %} Sign Up {% endblock %} 4 | 5 | {% block content %} 6 | 7 | {% if message %}

{{ message }}

{% endif %} 8 | 9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |

Fill habits

17 | 18 |
19 | {% csrf_token %} 20 | {{ form.non_field_errors }} 21 | 22 | {% for field in form %} 23 | 24 |
25 |
26 | {{ field.name }} 27 |
28 | {% for radio in field %} 29 | {{ radio }} 30 | {#
#} 31 | {# #} 32 | {# #} 33 | {#
#} 34 | 35 | {% endfor %} 36 |
37 |
38 |
39 | 40 | {% endfor %} 41 | 42 | 43 |
44 | 48 |
49 | 50 | 51 |
52 | 53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | 61 | 62 | {% endblock %} -------------------------------------------------------------------------------- /biproductive/account/templates/signup.html: -------------------------------------------------------------------------------- 1 | {% extends "base_generic.html" %} 2 | 3 | {% block title %} Sign Up {% endblock %} 4 | 5 | {% block content %} 6 | 7 | {% if message %}

{{ message }}

{% endif %} 8 | 9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |

Create an account

17 | 18 |
19 | {% csrf_token %} 20 | {{ form.non_field_errors }} 21 | 22 |
23 | 24 | 26 | 29 | {{ form.username.errors }} 30 | 31 |
32 | 33 |
34 | 35 | 36 | 39 | {{ form.email.errors }} 40 |
41 | 42 |
43 | 44 | 46 | 49 | {{ form.password1.errors }} 50 | 51 |
52 | 53 |
54 | 55 | 57 | 60 | {{ form.password2.errors }} 61 | 62 |
63 | 64 | 65 |
66 | 70 |
71 | 72 |

Have already an account? Login here

74 | 75 |
76 | 77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | 85 | 86 | {% endblock %} -------------------------------------------------------------------------------- /biproductive/templates/static/styles/game.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | /* HEADER */ 8 | header { 9 | color: white; 10 | letter-spacing: 10px; 11 | font-size: 22px; 12 | padding: 10px 5px; 13 | } 14 | 15 | @media only screen and (min-width: 768px) { 16 | header { 17 | font-size: 28px; 18 | } 19 | } 20 | 21 | @media only screen and (min-width: 1024px) { 22 | header { 23 | font-size: 32px; 24 | padding: 10px; 25 | } 26 | 27 | header h1 { 28 | font-weight: 400; 29 | } 30 | } 31 | 32 | /* !* main SECTION *! */ 33 | .main { 34 | margin: 70px auto; 35 | background: white; 36 | -webkit-user-select: none; 37 | -moz-user-select: none; 38 | -ms-user-select: none; 39 | user-select: none; 40 | } 41 | 42 | @media only screen and (min-width: 768px) { 43 | .main { 44 | width: 310px; 45 | height: 285px; 46 | } 47 | } 48 | 49 | @media only screen and (min-width: 1024px) { 50 | .main { 51 | width: 410px; 52 | height: 375px; 53 | } 54 | } 55 | 56 | .box { 57 | background: #6186aa; 58 | width: 50px; 59 | height: 50px; 60 | float: left; 61 | margin: 10px 0 0 10px; 62 | } 63 | 64 | @media only screen and (min-width: 1024px) { 65 | .box { 66 | width: 70px; 67 | height: 70px; 68 | } 69 | } 70 | 71 | .box.play:hover { 72 | opacity: 0.7; 73 | cursor: pointer; 74 | } 75 | 76 | .box img { 77 | width: 100%; 78 | display: block; 79 | border: solid 1px transparent; 80 | } 81 | 82 | .box .outlined { 83 | border: solid 1px #34495e; 84 | background: #1abc9c; 85 | } 86 | 87 | /* STATUS bar */ 88 | #state { 89 | background: #6186aa; 90 | background-size: 250%; 91 | width: 230px; 92 | line-height: 25px; 93 | float: left; 94 | margin: 10px 0 0 10px; 95 | padding: 0 10px; 96 | color: white; 97 | font-size: 16px; 98 | } 99 | 100 | @media only screen and (min-width: 768px) { 101 | #state { 102 | width: 290px; 103 | } 104 | } 105 | 106 | @media only screen and (min-width: 1024px) { 107 | #state { 108 | width: 390px; 109 | line-height: 35px; 110 | } 111 | } 112 | 113 | #time { 114 | float: left; 115 | } 116 | 117 | #time::after { 118 | content: " sec"; 119 | } 120 | 121 | #score { 122 | float: right; 123 | } 124 | 125 | #score::after { 126 | content: " points"; 127 | } 128 | 129 | /* FOOTER */ 130 | footer p { 131 | color: white; 132 | padding: 5px; 133 | font-size: 14px; 134 | letter-spacing: 1px; 135 | } 136 | 137 | footer p a { 138 | color: #ddd; 139 | text-decoration: none; 140 | } 141 | 142 | footer p a:hover { 143 | color: white; 144 | text-decoration: underline; 145 | } 146 | 147 | .hidden { 148 | display: none !important; 149 | } 150 | 151 | .show { 152 | display: block; 153 | } 154 | 155 | /* PRE modal window */ 156 | #pre { 157 | position: fixed; 158 | top: 0; 159 | left: 0; 160 | height: 100vh; 161 | width: 100%; 162 | display: flex; 163 | flex-direction: column; 164 | justify-content: center; 165 | align-items: center; 166 | background: rgba(0, 0, 0, 0.5); 167 | text-align: center; 168 | } 169 | 170 | #themes { 171 | margin: auto; 172 | padding: 20px; 173 | width: 250px; 174 | background: white; 175 | color: #6186aa; 176 | font-size: 20px; 177 | letter-spacing: 1px; 178 | } 179 | 180 | #themes p { 181 | margin-top: 10px; 182 | padding: 5px 20px; 183 | border: solid 1px; 184 | background: white; 185 | color: #6186aa; 186 | cursor: pointer; 187 | font-size: 16px; 188 | } 189 | 190 | #themes p:hover { 191 | background: #6186aa; 192 | background-size: 250%; 193 | color: white; 194 | } 195 | 196 | /* POST modal window */ 197 | #post { 198 | position: fixed; 199 | top: 0; 200 | left: 0; 201 | height: 100vh; 202 | width: 100%; 203 | display: flex; 204 | flex-direction: column; 205 | justify-content: center; 206 | align-items: center; 207 | background: rgba(0, 0, 0, 0.5); 208 | } 209 | 210 | #post > div { 211 | width: 300px; 212 | padding: 20px 0 40px; 213 | background: white; 214 | color: #6186aa; 215 | } 216 | 217 | #post p:first-child, 218 | #post #final { 219 | font-weight: bold; 220 | letter-spacing: 2px; 221 | margin: auto; 222 | padding: 10px 20px; 223 | } 224 | 225 | #post #again { 226 | color: #6186aa; 227 | text-decoration: none; 228 | padding: 10px 20px; 229 | width: 160px; 230 | border: solid 1px; 231 | } 232 | 233 | #post #again:hover { 234 | background: #6186aa; 235 | background-size: 250%; 236 | color: white; 237 | cursor: pointer; 238 | } 239 | -------------------------------------------------------------------------------- /biproductive/biproductive/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for biproductive project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.2.7. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.2/ref/settings/ 11 | """ 12 | import os 13 | import environ 14 | from pathlib import Path 15 | 16 | env = environ.Env() 17 | environ.Env.read_env() 18 | 19 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 20 | BASE_DIR = Path(__file__).resolve().parent.parent 21 | 22 | SECRET_KEY = os.environ.get('SECRET_KEY') if os.environ.get('HEROKU') else env('SECRET_KEY') 23 | DB_NAME = os.environ.get('DB_NAME') if os.environ.get('HEROKU') else 'postgres' 24 | DB_HOST = os.environ.get('DB_HOST') if os.environ.get('HEROKU') else 'db' 25 | DB_PORT = os.environ.get('DB_PORT') if os.environ.get('HEROKU') else env('DB_PORT') 26 | DB_USER = os.environ.get('DB_USER') if os.environ.get('HEROKU') else env('DB_USER') 27 | DB_PASSWORD = os.environ.get('DB_PASSWORD') if os.environ.get('HEROKU') else env('DB_PASSWORD') 28 | 29 | # SECURITY WARNING: don't run with debug turned on in production! 30 | DEBUG = True 31 | 32 | ALLOWED_HOSTS = [ 33 | "0.0.0.0", 34 | "127.0.0.1", 35 | "biproductive.herokuapp.com" 36 | ] 37 | 38 | # Application definition 39 | 40 | INSTALLED_APPS = [ 41 | "django.contrib.admin", 42 | "django.contrib.auth", 43 | "django.contrib.contenttypes", 44 | "django.contrib.sessions", 45 | "django.contrib.messages", 46 | "django.contrib.staticfiles", 47 | "home", 48 | "account", 49 | "habits", 50 | "productivity", 51 | "whitenoise.runserver_nostatic" 52 | ] 53 | 54 | MIDDLEWARE = [ 55 | "django.middleware.security.SecurityMiddleware", 56 | "django.contrib.sessions.middleware.SessionMiddleware", 57 | "django.middleware.common.CommonMiddleware", 58 | "django.middleware.csrf.CsrfViewMiddleware", 59 | "django.contrib.auth.middleware.AuthenticationMiddleware", 60 | "django.contrib.messages.middleware.MessageMiddleware", 61 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 62 | "whitenoise.middleware.WhiteNoiseMiddleware" 63 | ] 64 | 65 | ROOT_URLCONF = "biproductive.urls" 66 | 67 | TEMPLATES = [ 68 | { 69 | "BACKEND": "django.template.backends.django.DjangoTemplates", 70 | "DIRS": [os.path.join(BASE_DIR, "templates")], 71 | "APP_DIRS": True, 72 | "OPTIONS": { 73 | "context_processors": [ 74 | "django.template.context_processors.debug", 75 | "django.template.context_processors.request", 76 | "django.contrib.auth.context_processors.auth", 77 | "django.contrib.messages.context_processors.messages", 78 | ], 79 | }, 80 | }, 81 | ] 82 | 83 | WSGI_APPLICATION = "biproductive.wsgi.application" 84 | 85 | # Database 86 | # https://docs.djangoproject.com/en/3.2/ref/settings/#databases 87 | # https://medium.com/@rudipy/how-to-connecting-postgresql-with-a-django-application-f479dc949a11 88 | 89 | DATABASES = { 90 | "default": { 91 | "ENGINE": "django.db.backends.postgresql", 92 | "NAME": DB_NAME, 93 | "HOST": DB_HOST, 94 | "PORT": DB_PORT, 95 | "USER": DB_USER, 96 | "PASSWORD": DB_PASSWORD 97 | } 98 | } 99 | 100 | CSRF_COOKIE_SECURE = True 101 | SESSION_COOKIE_SECURE = False 102 | 103 | # Password validation 104 | # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators 105 | 106 | AUTH_PASSWORD_VALIDATORS = [ 107 | { 108 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 109 | }, 110 | { 111 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 112 | }, 113 | { 114 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 115 | }, 116 | { 117 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 118 | }, 119 | ] 120 | 121 | # Internationalization 122 | # https://docs.djangoproject.com/en/3.2/topics/i18n/ 123 | 124 | LANGUAGE_CODE = "en-us" 125 | 126 | TIME_ZONE = "UTC" 127 | 128 | USE_I18N = True 129 | 130 | USE_L10N = True 131 | 132 | USE_TZ = True 133 | 134 | # Static files (CSS, JavaScript, Images) 135 | # https://docs.djangoproject.com/en/3.2/howto/static-files/ 136 | 137 | STATIC_URL = "/templates/static/" 138 | 139 | STATIC_ROOT = os.path.join(BASE_DIR, 'static') 140 | 141 | # Default primary key field type 142 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field 143 | 144 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 145 | 146 | STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" 147 | 148 | # Add static file directory 149 | STATICFILES_DIRS = ( 150 | os.path.join(BASE_DIR, 'templates', 'static'), 151 | ) 152 | -------------------------------------------------------------------------------- /docs/COVERAGE.md: -------------------------------------------------------------------------------- 1 | This is report, generated by [coverage tool](https://github.com/nedbat/coveragepy). 2 | 3 | ``` 4 | root@473acb9e2a77:/biproductive/biproductive# coverage report 5 | Name Stmts Miss Cover 6 | ------------------------------------------------------------------------ 7 | account/__init__.py 0 0 100% 8 | account/admin.py 0 0 100% 9 | account/apps.py 4 4 0% 10 | account/forms.py 8 0 100% 11 | account/migrations/__init__.py 0 0 100% 12 | account/models.py 0 0 100% 13 | account/tests.py 42 0 100% 14 | account/urls.py 3 0 100% 15 | account/views.py 43 10 77% 16 | biproductive/__init__.py 0 0 100% 17 | biproductive/asgi.py 4 4 0% 18 | biproductive/settings.py 33 0 100% 19 | biproductive/urls.py 5 0 100% 20 | biproductive/wsgi.py 4 4 0% 21 | habits/__init__.py 0 0 100% 22 | habits/admin.py 4 0 100% 23 | habits/apps.py 4 4 0% 24 | habits/forms.py 12 0 100% 25 | habits/migrations/0001_initial.py 7 0 100% 26 | habits/migrations/0002_auto_20211004_1206.py 4 0 100% 27 | habits/migrations/0003_habitusage_status.py 4 0 100% 28 | habits/migrations/__init__.py 0 0 100% 29 | habits/models.py 14 1 93% 30 | habits/scripts.py 25 7 72% 31 | habits/tests.py 24 0 100% 32 | habits/urls.py 4 0 100% 33 | habits/views.py 38 4 89% 34 | home/__init__.py 0 0 100% 35 | home/admin.py 0 0 100% 36 | home/apps.py 4 4 0% 37 | home/migrations/__init__.py 0 0 100% 38 | home/models.py 0 0 100% 39 | home/scripts.py 23 7 70% 40 | home/tests.py 0 0 100% 41 | home/urls.py 3 0 100% 42 | home/views.py 16 6 62% 43 | manage.py 12 2 83% 44 | productivity/__init__.py 0 0 100% 45 | productivity/admin.py 3 0 100% 46 | productivity/apps.py 4 4 0% 47 | productivity/migrations/0001_initial.py 7 0 100% 48 | productivity/migrations/0002_auto_20210930_1943.py 4 0 100% 49 | productivity/migrations/__init__.py 0 0 100% 50 | productivity/models.py 8 1 88% 51 | productivity/scripts.py 19 7 63% 52 | productivity/tests.py 34 0 100% 53 | productivity/urls.py 3 0 100% 54 | productivity/views.py 23 0 100% 55 | report_generator/__init__.py 0 0 100% 56 | report_generator/admin.py 0 0 100% 57 | report_generator/apps.py 3 3 0% 58 | report_generator/migrations/__init__.py 0 0 100% 59 | report_generator/models.py 0 0 100% 60 | report_generator/tests.py 16 0 100% 61 | report_generator/views.py 42 2 95% 62 | utils.py 9 0 100% 63 | ------------------------------------------------------------------------ 64 | TOTAL 519 74 86% 65 | ``` 66 | 67 | How to test our program (reproduce the results): 68 | 69 | 1. Run `$ docker-compose up --build` 70 | 2. After the application is launched run in separate terminal: `$ docker exec -it bash` 71 | 72 | - name of container could be found by command `$ docker ps` (in my case. name of container is biproductive_web_1). 73 | 74 | 3. Inside of container 75 | 76 | - run `pip install coverage` 77 | - run `cd biproductive; coverage run --source='.' manage.py test` 78 | - run `coverage report` 79 | -------------------------------------------------------------------------------- /biproductive/templates/base_generic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% load static %} 5 | 6 | {% block title %}{% endblock %} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 65 | 66 | 67 |
68 | 69 | {% block content %}{% endblock %} 70 | 71 |
72 | 73 | 74 | 75 | 125 | 126 | 127 | 128 | {% block scripts %} 129 | 130 | 131 | 133 | 136 | 139 | 142 | 143 | {% endblock %} 144 | 145 | 146 | 147 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BiProductive 2 | 3 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/1b8fcfb3465a4f02ab9a2d6dc445dfed)](https://app.codacy.com/gh/rizvansky/biproductive?utm_source=github.com&utm_medium=referral&utm_content=rizvansky/biproductive&utm_campaign=Badge_Grade_Settings) 4 | [![Stargazers][stars-shield]][stars-url] 5 | [![Issues][issues-shield]][issues-url] 6 | [![MIT License][license-shield]][license-url] 7 | 8 | ## Description 9 | 10 | This repository contains the application BiProductive, which analyzes the habits of the person, tests his productivity, 11 | and defines dependencies between habits and productivity. Each day user enters the actions they made today (sports 12 | activity, meditation, smoking, etc.) and tests their brain performance. Then the application computes the correlation 13 | between habits and user's performance, and makes personal recommendations. 14 | 15 | - This project is deployed on Heroku: https://biproductive.herokuapp.com. 16 | 17 | - The video with demo is available 18 | [here](https://drive.google.com/file/d/1q6ioV4W50Un--JNKLv6IV0rFbHXYZwCy/view?usp=sharing). 19 | 20 | ![1](./docs/demo.gif) 21 | 22 | ## How to deploy the application 23 | 24 | - Locally 25 | - You should have Docker and Docker Compose installed. 26 | 27 | - Go to the project folder. 28 | 29 | - Rename .env.example to .env and fill the ```SECRET_KEY``` environment variable. 30 | For example, you can use (https://djecrety.ir) to generate the secret key. 31 | 32 | - Put .env file to ```./biproductive/biproductive``` directory. 33 | 34 | - Run ```docker-compose up --build```. 35 | 36 | - An application will be launched at ```0.0.0.0:8000``` address. 37 | 38 | - Heroku 39 | - If you want to deploy this application on your own Heroku host, read [HEROKU.MD](docs/HEROKU.MD). 40 | 41 | ## Functionality of our application 42 | 43 | - You can register to the website, providing any unused username, email, and password. You can log in using his username 44 | and password. 45 | 46 | - After login at the home page you can see the dashboard with weekly statistics of habits usage and his brain activity 47 | during the productivity testing. 48 | 49 | - Also, on request, you can request from the system weekly report with analysis of his habits (in future we will add 50 | sending report by email each week). 51 | 52 | - You can add as many habits as you want for tracking them. 53 | 54 | - During the day or at the end of the day you can mark habits you completed and not completed (e.x. Swimming - Yes, 55 | Smoking - No) only once a day (no refilling can be done). 56 | 57 | - Once a day (preferably in the evening) you can test how well your brain works after the day by playing a memory game. 58 | 59 | - So, data about the brain activity after the day and statistics of habit usage could provide personal recommendations 60 | about habits (which habits increase brain activity, which not). 61 | 62 | ## Main components of our application 63 | 64 | We deployed our project or Heroku, so as a web server that handles client requests we used `gunicorn`. 65 | 66 | We organized business logic of application into 4 main components/modules: 67 | 68 | - User habit tracker - django application that is responsible for tracking habits (user can start tracking his habit by 69 | adding it and each day at the special form mark the habit completed or not (e.x. did you read a book today or not). 70 | 71 | - Productivity testing tool - small django application with javascript game that aims to track everyday brain activity 72 | after completing (or not completing) habit activities. 73 | 74 | - Habit analyzer tool - ML application that calculates how well habits affects your brain. 75 | 76 | - Report generation tool - wraps data, received from habit analyzer, and prepares a small pdf report with charts & 77 | tables. 78 | 79 | In the storage layer we have used 2 databased: 80 | 81 | - Habit history database, that stores user's tracking habits and their usage. 82 | - Productivity history database, that tracks user's everyday brain activity. 83 | 84 | ![](docs/dynamic-view-updated.png) 85 | *Dynamic view, describing main components of our application (static/dynamic view can be found in our artifact)* 86 | 87 | ## Stack of technologies 88 | 89 | - Django 90 | - PostgreSQL 91 | - JavaScript 92 | - HTML 93 | - Bootstrap 94 | 95 | ## SOLID principles and design patterns 96 | [Here](./docs/PATTERNS.MD) you can see a description of how we used SOLID principles and the information about the 97 | design patterns. 98 | 99 | ## RUP Artifact 100 | 101 | [Here](https://docs.google.com/document/d/14AMeCV4WJotkQ8lvZcl2u_bB66lMKmu4/edit?usp=sharing&ouid=109541784549585358096&rtpof=true&sd=true) 102 | is the link to the RUP Artifact where you can find the list of stakeholders and their roles, functional and 103 | non-functional requirements planned features, and other design specifications. 104 | 105 | Also, you can see the design development history [here](./docs/DESIGN_DEVELOPMENT.MD). 106 | 107 | ## Contributing 108 | 109 | We appreciate all contributions. If you are planning to contribute back bug-fixes, please do so without any further 110 | discussion. 111 | 112 | If you plan to contribute new features, utility functions, or extensions to the core, please first open an issue and 113 | discuss the feature with us. Sending a PR without discussion might end up resulting in a rejected PR because we might be 114 | taking the core in a different direction than you might be aware of. 115 | 116 | Check the [CONTRIBUTING.MD](./docs/CONTRIBUTING.MD) to learn more about making a contribution to our project. 117 | 118 | ## Used linters in our project 119 | 120 | During the development of our project we have configured git pre-commit checks, defined in `.pre-commit-config.yaml`: 121 | 122 | - `isort` for sorting names of imported libraries 123 | - `black` - Python code formatter 124 | - `flake8` - combination of various code refactor tools like `pyflakes`, `pycodestyle`, checks for code styles. 125 | 126 | ## Code coverage 127 | 128 | Code coverage of our web-application - 86%. The application was tested locally with python package - `coverage`. 129 | Generated report and instruction how to test are described in [COVERAGE.md](docs/COVERAGE.md). 130 | 131 | ## The BiProductive team 132 | 133 | The original BiProductive code contributors can be found in [AUTHORS.MD](./docs/AUTHORS.MD). 134 | 135 | ### Special Thanks To 136 | 137 | [Rémy Beumier](https://github.com/beumsk) - developer of the 138 | [memory game for productivity check](https://github.com/beumsk/Memory) 139 | 140 | 141 | 142 | 143 | [stars-shield]: https://img.shields.io/github/stars/rizvansky/biproductive.svg?style=flat&logo=appveyor 144 | 145 | [stars-url]: https://github.com/rizvansky/biproductive/stargazers 146 | 147 | [issues-shield]: https://img.shields.io/github/issues/rizvansky/biproductive.svg?style=flat&logo=appveyor 148 | [issues-url]: https://github.com/rizvansky/biproductive/issues 149 | [license-shield]: https://img.shields.io/github/license/rizvansky/biproductive.svg?style=flat 150 | [license-url]: https://github.com/rizvansky/biproductive/blob/main/LICENSE 151 | -------------------------------------------------------------------------------- /biproductive/templates/static/scripts/game.js: -------------------------------------------------------------------------------- 1 | // other themes to add ? 2 | // bigger memory? 3 | 4 | const library = { 5 | pokemon: [ 6 | "https://res.cloudinary.com/beumsk/image/upload/v1547980025/memory/Pokemon/Bulbasaur.png", 7 | "https://res.cloudinary.com/beumsk/image/upload/v1547980083/memory/Pokemon/Charmander.png", 8 | "https://res.cloudinary.com/beumsk/image/upload/v1547980101/memory/Pokemon/Squirtle.png", 9 | "https://res.cloudinary.com/beumsk/image/upload/v1547980116/memory/Pokemon/Pikachu.png", 10 | "https://res.cloudinary.com/beumsk/image/upload/v1547980129/memory/Pokemon/Mewtwo.png", 11 | "https://res.cloudinary.com/beumsk/image/upload/v1547980142/memory/Pokemon/Mew.png", 12 | "https://res.cloudinary.com/beumsk/image/upload/v1547980154/memory/Pokemon/Articuno.png", 13 | "https://res.cloudinary.com/beumsk/image/upload/v1547980164/memory/Pokemon/Zapdos.png", 14 | "https://res.cloudinary.com/beumsk/image/upload/v1547980175/memory/Pokemon/Moltres.png", 15 | "https://res.cloudinary.com/beumsk/image/upload/v1547980186/memory/Pokemon/Eevee.png", 16 | "https://res.cloudinary.com/beumsk/image/upload/v1547980025/memory/Pokemon/Bulbasaur.png", 17 | "https://res.cloudinary.com/beumsk/image/upload/v1547980083/memory/Pokemon/Charmander.png", 18 | "https://res.cloudinary.com/beumsk/image/upload/v1547980101/memory/Pokemon/Squirtle.png", 19 | "https://res.cloudinary.com/beumsk/image/upload/v1547980116/memory/Pokemon/Pikachu.png", 20 | "https://res.cloudinary.com/beumsk/image/upload/v1547980129/memory/Pokemon/Mewtwo.png", 21 | "https://res.cloudinary.com/beumsk/image/upload/v1547980142/memory/Pokemon/Mew.png", 22 | "https://res.cloudinary.com/beumsk/image/upload/v1547980154/memory/Pokemon/Articuno.png", 23 | "https://res.cloudinary.com/beumsk/image/upload/v1547980164/memory/Pokemon/Zapdos.png", 24 | "https://res.cloudinary.com/beumsk/image/upload/v1547980175/memory/Pokemon/Moltres.png", 25 | "https://res.cloudinary.com/beumsk/image/upload/v1547980186/memory/Pokemon/Eevee.png" 26 | ], 27 | starwars: [ 28 | "https://res.cloudinary.com/beumsk/image/upload/v1547980981/memory/starwars/anakin%20skywalker.jpg", 29 | "https://res.cloudinary.com/beumsk/image/upload/v1547981009/memory/starwars/luke%20skywalker.jpg", 30 | "https://res.cloudinary.com/beumsk/image/upload/v1547981022/memory/starwars/Obi%20wann.jpg", 31 | "https://res.cloudinary.com/beumsk/image/upload/v1547981054/memory/starwars/Han%20solo.jpg", 32 | "https://res.cloudinary.com/beumsk/image/upload/v1547981074/memory/starwars/chewbacca.jpg", 33 | "https://res.cloudinary.com/beumsk/image/upload/v1547981095/memory/starwars/yoda.jpg", 34 | "https://res.cloudinary.com/beumsk/image/upload/v1547981117/memory/starwars/dark%20sidious.jpg", 35 | "https://res.cloudinary.com/beumsk/image/upload/v1547981141/memory/starwars/dark%20vador.jpg", 36 | "https://res.cloudinary.com/beumsk/image/upload/v1547981165/memory/starwars/padme.jpg", 37 | "https://res.cloudinary.com/beumsk/image/upload/v1547981190/memory/starwars/leia.jpg", 38 | "https://res.cloudinary.com/beumsk/image/upload/v1547980981/memory/starwars/anakin%20skywalker.jpg", 39 | "https://res.cloudinary.com/beumsk/image/upload/v1547981009/memory/starwars/luke%20skywalker.jpg", 40 | "https://res.cloudinary.com/beumsk/image/upload/v1547981022/memory/starwars/Obi%20wann.jpg", 41 | "https://res.cloudinary.com/beumsk/image/upload/v1547981054/memory/starwars/Han%20solo.jpg", 42 | "https://res.cloudinary.com/beumsk/image/upload/v1547981074/memory/starwars/chewbacca.jpg", 43 | "https://res.cloudinary.com/beumsk/image/upload/v1547981095/memory/starwars/yoda.jpg", 44 | "https://res.cloudinary.com/beumsk/image/upload/v1547981117/memory/starwars/dark%20sidious.jpg", 45 | "https://res.cloudinary.com/beumsk/image/upload/v1547981141/memory/starwars/dark%20vador.jpg", 46 | "https://res.cloudinary.com/beumsk/image/upload/v1547981165/memory/starwars/padme.jpg", 47 | "https://res.cloudinary.com/beumsk/image/upload/v1547981190/memory/starwars/leia.jpg" 48 | ], 49 | lotr: [ 50 | "https://res.cloudinary.com/beumsk/image/upload/v1547981408/memory/lotr/gandalf.jpg", 51 | "https://res.cloudinary.com/beumsk/image/upload/v1547981438/memory/lotr/sauron.jpg", 52 | "https://res.cloudinary.com/beumsk/image/upload/v1547981469/memory/lotr/Aragorn.jpg", 53 | "https://res.cloudinary.com/beumsk/image/upload/v1547981501/memory/lotr/legolas.jpg", 54 | "https://res.cloudinary.com/beumsk/image/upload/v1547981535/memory/lotr/Gimli.jpg", 55 | "https://res.cloudinary.com/beumsk/image/upload/v1547981603/memory/lotr/golum.jpg", 56 | "https://res.cloudinary.com/beumsk/image/upload/v1547981645/memory/lotr/sam.jpg", 57 | "https://res.cloudinary.com/beumsk/image/upload/v1547981686/memory/lotr/saroumane.jpg", 58 | "https://res.cloudinary.com/beumsk/image/upload/v1547981738/memory/lotr/bilbo.jpg", 59 | "https://res.cloudinary.com/beumsk/image/upload/v1547981802/memory/lotr/frodo.jpg", 60 | "https://res.cloudinary.com/beumsk/image/upload/v1547981408/memory/lotr/gandalf.jpg", 61 | "https://res.cloudinary.com/beumsk/image/upload/v1547981438/memory/lotr/sauron.jpg", 62 | "https://res.cloudinary.com/beumsk/image/upload/v1547981469/memory/lotr/Aragorn.jpg", 63 | "https://res.cloudinary.com/beumsk/image/upload/v1547981501/memory/lotr/legolas.jpg", 64 | "https://res.cloudinary.com/beumsk/image/upload/v1547981535/memory/lotr/Gimli.jpg", 65 | "https://res.cloudinary.com/beumsk/image/upload/v1547981603/memory/lotr/golum.jpg", 66 | "https://res.cloudinary.com/beumsk/image/upload/v1547981645/memory/lotr/sam.jpg", 67 | "https://res.cloudinary.com/beumsk/image/upload/v1547981686/memory/lotr/saroumane.jpg", 68 | "https://res.cloudinary.com/beumsk/image/upload/v1547981738/memory/lotr/bilbo.jpg", 69 | "https://res.cloudinary.com/beumsk/image/upload/v1547981802/memory/lotr/frodo.jpg" 70 | ], 71 | disney: [ 72 | "https://res.cloudinary.com/beumsk/image/upload/v1547982044/memory/disney/mickey.jpg", 73 | "https://res.cloudinary.com/beumsk/image/upload/v1547982088/memory/disney/mowgli.jpg", 74 | "https://res.cloudinary.com/beumsk/image/upload/v1547982610/memory/disney/tarzan.jpg", 75 | "https://res.cloudinary.com/beumsk/image/upload/v1547982620/memory/disney/simba.jpg", 76 | "https://res.cloudinary.com/beumsk/image/upload/v1547982628/memory/disney/aladin.jpg", 77 | "https://res.cloudinary.com/beumsk/image/upload/v1547982636/memory/disney/blanche%20neige.jpg", 78 | "https://res.cloudinary.com/beumsk/image/upload/v1547982644/memory/disney/alice.png", 79 | "https://res.cloudinary.com/beumsk/image/upload/v1547982653/memory/disney/peter%20pan.jpg", 80 | "https://res.cloudinary.com/beumsk/image/upload/v1547982663/memory/disney/pinocchio.jpg", 81 | "https://res.cloudinary.com/beumsk/image/upload/v1547982738/memory/disney/raiponce.jpg", 82 | "https://res.cloudinary.com/beumsk/image/upload/v1547982044/memory/disney/mickey.jpg", 83 | "https://res.cloudinary.com/beumsk/image/upload/v1547982088/memory/disney/mowgli.jpg", 84 | "https://res.cloudinary.com/beumsk/image/upload/v1547982610/memory/disney/tarzan.jpg", 85 | "https://res.cloudinary.com/beumsk/image/upload/v1547982620/memory/disney/simba.jpg", 86 | "https://res.cloudinary.com/beumsk/image/upload/v1547982628/memory/disney/aladin.jpg", 87 | "https://res.cloudinary.com/beumsk/image/upload/v1547982636/memory/disney/blanche%20neige.jpg", 88 | "https://res.cloudinary.com/beumsk/image/upload/v1547982644/memory/disney/alice.png", 89 | "https://res.cloudinary.com/beumsk/image/upload/v1547982653/memory/disney/peter%20pan.jpg", 90 | "https://res.cloudinary.com/beumsk/image/upload/v1547982663/memory/disney/pinocchio.jpg", 91 | "https://res.cloudinary.com/beumsk/image/upload/v1547982738/memory/disney/raiponce.jpg" 92 | ], 93 | pixar: [ 94 | "https://res.cloudinary.com/beumsk/image/upload/v1547982971/memory/pixar/up.jpg", 95 | "https://res.cloudinary.com/beumsk/image/upload/v1547982987/memory/pixar/buzz.jpg", 96 | "https://res.cloudinary.com/beumsk/image/upload/v1547983000/memory/pixar/woody.jpg", 97 | "https://res.cloudinary.com/beumsk/image/upload/v1547983016/memory/pixar/Remy.jpg", 98 | "https://res.cloudinary.com/beumsk/image/upload/v1547983032/memory/pixar/Mike.jpg", 99 | "https://res.cloudinary.com/beumsk/image/upload/v1547983077/memory/pixar/Nemo.jpg", 100 | "https://res.cloudinary.com/beumsk/image/upload/v1547983114/memory/pixar/wall-e.png", 101 | "https://res.cloudinary.com/beumsk/image/upload/v1547983169/memory/pixar/Mr-Incredible.jpg", 102 | "https://res.cloudinary.com/beumsk/image/upload/v1547983381/memory/pixar/sully.jpg", 103 | "https://res.cloudinary.com/beumsk/image/upload/v1547983403/memory/pixar/flash%20mcqueen.jpg", 104 | "https://res.cloudinary.com/beumsk/image/upload/v1547982971/memory/pixar/up.jpg", 105 | "https://res.cloudinary.com/beumsk/image/upload/v1547982987/memory/pixar/buzz.jpg", 106 | "https://res.cloudinary.com/beumsk/image/upload/v1547983000/memory/pixar/woody.jpg", 107 | "https://res.cloudinary.com/beumsk/image/upload/v1547983016/memory/pixar/Remy.jpg", 108 | "https://res.cloudinary.com/beumsk/image/upload/v1547983032/memory/pixar/Mike.jpg", 109 | "https://res.cloudinary.com/beumsk/image/upload/v1547983077/memory/pixar/Nemo.jpg", 110 | "https://res.cloudinary.com/beumsk/image/upload/v1547983114/memory/pixar/wall-e.png", 111 | "https://res.cloudinary.com/beumsk/image/upload/v1547983169/memory/pixar/Mr-Incredible.jpg", 112 | "https://res.cloudinary.com/beumsk/image/upload/v1547983381/memory/pixar/sully.jpg", 113 | "https://res.cloudinary.com/beumsk/image/upload/v1547983403/memory/pixar/flash%20mcqueen.jpg" 114 | ], 115 | harrypotter: [ 116 | "https://res.cloudinary.com/beumsk/image/upload/v1547998926/memory/harrypotter/harry.jpg", 117 | "https://res.cloudinary.com/beumsk/image/upload/v1547998958/memory/harrypotter/ron.jpg", 118 | "https://res.cloudinary.com/beumsk/image/upload/v1547998992/memory/harrypotter/hermione.jpg", 119 | "https://res.cloudinary.com/beumsk/image/upload/v1547999106/memory/harrypotter/dumbledore.jpg", 120 | "https://res.cloudinary.com/beumsk/image/upload/v1547999032/memory/harrypotter/malfoy.jpg", 121 | "https://res.cloudinary.com/beumsk/image/upload/v1547999143/memory/harrypotter/voldemort.jpg", 122 | "https://res.cloudinary.com/beumsk/image/upload/v1547999401/memory/harrypotter/rogue.jpg", 123 | "https://res.cloudinary.com/beumsk/image/upload/v1547999196/memory/harrypotter/hagrid.jpg", 124 | "https://res.cloudinary.com/beumsk/image/upload/v1547999271/memory/harrypotter/sirius.jpg", 125 | "https://res.cloudinary.com/beumsk/image/upload/v1547999577/memory/harrypotter/neville.jpg", 126 | "https://res.cloudinary.com/beumsk/image/upload/v1547998926/memory/harrypotter/harry.jpg", 127 | "https://res.cloudinary.com/beumsk/image/upload/v1547998958/memory/harrypotter/ron.jpg", 128 | "https://res.cloudinary.com/beumsk/image/upload/v1547998992/memory/harrypotter/hermione.jpg", 129 | "https://res.cloudinary.com/beumsk/image/upload/v1547999106/memory/harrypotter/dumbledore.jpg", 130 | "https://res.cloudinary.com/beumsk/image/upload/v1547999032/memory/harrypotter/malfoy.jpg", 131 | "https://res.cloudinary.com/beumsk/image/upload/v1547999143/memory/harrypotter/voldemort.jpg", 132 | "https://res.cloudinary.com/beumsk/image/upload/v1547999401/memory/harrypotter/rogue.jpg", 133 | "https://res.cloudinary.com/beumsk/image/upload/v1547999196/memory/harrypotter/hagrid.jpg", 134 | "https://res.cloudinary.com/beumsk/image/upload/v1547999271/memory/harrypotter/sirius.jpg", 135 | "https://res.cloudinary.com/beumsk/image/upload/v1547999577/memory/harrypotter/neville.jpg" 136 | ] 137 | }; 138 | 139 | let images = [], 140 | tempElt1 = "", 141 | tempElt2 = "", 142 | click = -1, 143 | win = 0, 144 | score = 0, 145 | time = 0; 146 | 147 | const preElt = document.querySelector("#pre"), 148 | themesElt = document.querySelector("#themes"), 149 | boxElts = document.getElementsByClassName("box"), 150 | mainElt = document.querySelector(".main"), 151 | timeElt = document.querySelector("#time"), 152 | scoreElt = document.querySelector("#score"), 153 | postElt = document.querySelector("#post"), 154 | finalElt = document.querySelector("#final"), 155 | againElt = document.querySelector("#again"); 156 | 157 | // initiate the game with chosen theme 158 | themesElt.addEventListener("click", function (e) { 159 | if (e.target.classList.contains("themes")) { 160 | activateTheme(e.target.id); 161 | preElt.classList.add("hidden"); 162 | } 163 | }); 164 | 165 | async function postScore(url = "", data = {}) { 166 | const response = await fetch(url, { 167 | method: "POST", 168 | "mode": "cors", 169 | body: JSON.stringify(data) 170 | }); 171 | return await response.json(); 172 | } 173 | 174 | function activateTheme(theme) { 175 | // insert theme in images array 176 | switch (theme) { 177 | case "pokemon": 178 | for (let i = 0; i < 20; i++) { 179 | images.push(library.pokemon[i]); 180 | } 181 | break; 182 | case "starwars": 183 | for (let i = 0; i < 20; i++) { 184 | images.push(library.starwars[i]); 185 | } 186 | break; 187 | case "lotr": 188 | for (let i = 0; i < 20; i++) { 189 | images.push(library.lotr[i]); 190 | } 191 | break; 192 | case "disney": 193 | for (let i = 0; i < 20; i++) { 194 | images.push(library.disney[i]); 195 | } 196 | break; 197 | case "pixar": 198 | for (let i = 0; i < 20; i++) { 199 | images.push(library.pixar[i]); 200 | } 201 | break; 202 | case "harrypotter": 203 | for (let i = 0; i < 20; i++) { 204 | images.push(library.harrypotter[i]); 205 | } 206 | break; 207 | } 208 | // insert images in memory game 209 | for (let i = 0; i < 20; i++) { 210 | var rand = Math.floor(Math.random() * (images.length - 1)); 211 | boxElts[i].innerHTML = ""; 212 | images.splice(rand, 1); 213 | } 214 | } 215 | 216 | 217 | // Handle the play 218 | mainElt.addEventListener("click", gameLogic); 219 | 220 | function gameLogic(e) { 221 | // make sure the box is playable 222 | if (e.target.classList.contains("play")) { 223 | e.target.firstChild.classList.remove("hidden"); 224 | // first of two click 225 | if (click < 1) { 226 | tempElt1 = e.target; 227 | // timer 228 | if (click === -1) { 229 | timer = setInterval(function () { 230 | time++; 231 | timeElt.innerHTML = time; 232 | }, 1000); 233 | } 234 | click = 1; 235 | } 236 | 237 | // second click 238 | else if (e.target !== tempElt1) { 239 | tempElt2 = e.target; 240 | 241 | // different images 242 | if (tempElt1.firstChild.src !== tempElt2.firstChild.src) { 243 | mainElt.removeEventListener("click", gameLogic); 244 | setTimeout(function () { 245 | tempElt1.firstChild.classList.add("hidden"); 246 | tempElt2.firstChild.classList.add("hidden"); 247 | mainElt.addEventListener("click", gameLogic); 248 | }, 400); 249 | if (score > 0) { 250 | score -= 2; 251 | } 252 | scoreElt.innerHTML = score; 253 | } 254 | 255 | // same images 256 | else { 257 | score += 10; 258 | win += 2; 259 | tempElt1.firstChild.classList.add("outlined"); 260 | tempElt2.firstChild.classList.add("outlined"); 261 | tempElt1.classList.remove("play"); 262 | tempElt2.classList.remove("play"); 263 | scoreElt.innerHTML = score; 264 | 265 | // game won 266 | if (win === 20) { 267 | clearTimeout(timer); 268 | e.preventDefault(); 269 | finalElt.innerHTML = "You won " + score + " points
in " + time + " seconds"; 270 | 271 | postScore("/productivity/", {"prod_score": score}).then((data) => {}) 272 | postElt.classList.remove("hidden"); 273 | } 274 | } 275 | click = 0; 276 | } 277 | } 278 | } 279 | 280 | againElt.addEventListener("click", resetGame); 281 | 282 | function resetGame() { 283 | // reset game 284 | tempElt1 = ""; 285 | tempElt2 = ""; 286 | click = -1; 287 | win = 0; 288 | score = 0; 289 | time = 0; 290 | postElt.classList.add("hidden"); 291 | preElt.classList.remove("hidden"); 292 | for (let i = 0; i < 20; i++) { 293 | boxElts[i].classList.add("play"); 294 | boxElts[i].firstChild.classList.add("hidden"); 295 | } 296 | timeElt.textContent = time; 297 | scoreElt.textContent = score; 298 | } --------------------------------------------------------------------------------