├── 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 |
--------------------------------------------------------------------------------
/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 | 
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 | 
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 | 
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 | 
38 |
39 | ### 10.09.2021. Refactoring use case diagram
40 |
41 | 
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 |
14 |
15 |
16 |
Habit statistics of last days
17 |
18 |
19 |
20 | {% for name in habit_names %}
21 | | {{ name }} |
22 | {% endfor %}
23 |
24 |
25 |
26 |
27 |
28 |
Change of brain activity during last days
29 |
30 |
31 |
32 |
33 |
34 |
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"\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 |
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 |
58 |
59 |
BRAVO !
60 |
61 |
62 |
63 | Go home!
64 |
65 |
66 |
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 |
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 |
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 |
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 | [](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 | 
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 | 
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 | }
--------------------------------------------------------------------------------