├── .DS_Store
├── .gitignore
├── LICENSE
├── README.md
├── apps
├── core
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── migrations
│ │ └── __init__.py
│ ├── models.py
│ ├── templates
│ │ └── core
│ │ │ ├── base.html
│ │ │ ├── frontpage.html
│ │ │ ├── login.html
│ │ │ ├── plans.html
│ │ │ ├── privacy.html
│ │ │ ├── signup.html
│ │ │ └── terms.html
│ ├── tests.py
│ └── views.py
├── dashboard
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── migrations
│ │ └── __init__.py
│ ├── models.py
│ ├── templates
│ │ └── dashboard
│ │ │ ├── dashboard.html
│ │ │ └── view_user.html
│ ├── templatetags
│ │ ├── __init__.py
│ │ └── dashboardextras.py
│ ├── tests.py
│ ├── urls.py
│ ├── utilities.py
│ └── views.py
├── project
│ ├── __init__.py
│ ├── admin.py
│ ├── api.py
│ ├── apps.py
│ ├── context_processors.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ ├── 0002_task.py
│ │ ├── 0003_entry.py
│ │ ├── 0004_auto_20201126_1920.py
│ │ ├── 0005_auto_20201206_1924.py
│ │ └── __init__.py
│ ├── models.py
│ ├── templates
│ │ └── project
│ │ │ ├── edit_entry.html
│ │ │ ├── edit_project.html
│ │ │ ├── edit_task.html
│ │ │ ├── project.html
│ │ │ ├── projects.html
│ │ │ ├── task.html
│ │ │ └── track_entry.html
│ ├── tests.py
│ ├── urls.py
│ └── views.py
├── team
│ ├── __init__.py
│ ├── admin.py
│ ├── api.py
│ ├── apps.py
│ ├── context_processors.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ ├── 0002_invitation.py
│ │ ├── 0003_plan.py
│ │ ├── 0004_auto_20201209_1955.py
│ │ └── __init__.py
│ ├── models.py
│ ├── templates
│ │ └── team
│ │ │ ├── add.html
│ │ │ ├── edit.html
│ │ │ ├── email_accepted_invitation.html
│ │ │ ├── email_invitation.html
│ │ │ ├── invite.html
│ │ │ ├── plans.html
│ │ │ ├── plans_thankyou.html
│ │ │ └── team.html
│ ├── tests.py
│ ├── urls.py
│ ├── utilities.py
│ └── views.py
└── userprofile
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── migrations
│ ├── 0001_initial.py
│ ├── 0002_auto_20201122_1210.py
│ ├── 0003_userprofile_avatar.py
│ └── __init__.py
│ ├── models.py
│ ├── templates
│ └── userprofile
│ │ ├── accept_invitation.html
│ │ ├── edit_profile.html
│ │ └── myaccount.html
│ ├── tests.py
│ ├── urls.py
│ └── views.py
├── manage.py
├── media
└── uploads
│ └── avatars
│ └── lamp.jpeg
├── minutos
├── __init__.py
├── asgi.py
├── settings.py
├── urls.py
└── wsgi.py
├── scripts
└── check_subscriptions.py
├── serverfiles
├── gunicorn_start
├── nginx_minutos.conf
└── supervisor_minutos.conf
└── static
├── .DS_Store
└── images
├── .DS_Store
├── avatar.png
├── features-happyboss.jpg
├── features-team.jpg
└── features-tracktime.jpg
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SteinOveHelset/minutos/73cdcb44409f97b05680c6a048f80eb4bd3f1f46/.DS_Store
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | db.sqlite3
2 | *.pyc
3 | */*.pyc
4 | */*/*.pyc
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 SteinOveHelset
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Minutos
2 |
3 | This repository is a part of a video tutorial on my YouTube channel: Code With Stein
4 |
5 | [Code With Stein - Website](https://codewithstein.com)
6 |
--------------------------------------------------------------------------------
/apps/core/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SteinOveHelset/minutos/73cdcb44409f97b05680c6a048f80eb4bd3f1f46/apps/core/__init__.py
--------------------------------------------------------------------------------
/apps/core/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | # Register your models here.
4 |
--------------------------------------------------------------------------------
/apps/core/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class CoreConfig(AppConfig):
5 | name = 'core'
6 |
--------------------------------------------------------------------------------
/apps/core/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SteinOveHelset/minutos/73cdcb44409f97b05680c6a048f80eb4bd3f1f46/apps/core/migrations/__init__.py
--------------------------------------------------------------------------------
/apps/core/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 | # Create your models here.
4 |
--------------------------------------------------------------------------------
/apps/core/templates/core/base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | {% block title %}{% endblock %}Minutos
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
22 |
23 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
76 |
77 |
78 |
You have tracked [[ readableSeconds ]]
79 |
80 |
81 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 | {% if messages %}
94 | {% for message in messages %}
95 | {{ message }}
96 | {% endfor %}
97 | {% endif %}
98 |
99 | {% block content %}
100 | {% endblock %}
101 |
102 |
103 |
104 |
105 |
124 |
125 |
126 |
127 |
128 |
129 |
230 | {% block scripts %}
231 | {% endblock %}
232 |
233 |
234 |
--------------------------------------------------------------------------------
/apps/core/templates/core/frontpage.html:
--------------------------------------------------------------------------------
1 | {% extends 'core/base.html' %}
2 |
3 | {% load static %}
4 |
5 | {% block title %}Home | {% endblock %}
6 |
7 | {% block content %}
8 |
9 |
10 |
11 |
Welcome to Minutos
12 | Time tracking made simple
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
Simple time tracking
27 |
28 |
Time tracking made simple. Time tracking made simple. Time tracking made simple. Time tracking made simple. Time tracking made simple.
29 |
30 |
31 |
32 |
33 |
34 |
Simple time tracking
35 |
36 |
Time tracking made simple. Time tracking made simple. Time tracking made simple. Time tracking made simple. Time tracking made simple.
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
Simple time tracking
51 |
52 |
Time tracking made simple. Time tracking made simple. Time tracking made simple. Time tracking made simple. Time tracking made simple.
53 |
54 |
55 |
56 |
57 |
58 |
65 |
66 | {% endblock %}
--------------------------------------------------------------------------------
/apps/core/templates/core/login.html:
--------------------------------------------------------------------------------
1 | {% extends 'core/base.html' %}
2 |
3 | {% block title %}Log in | {% endblock %}
4 |
5 | {% block content %}
6 |
7 |
8 |
9 |
Log in
10 | Log in to your account and start tracking time
11 |
12 |
13 |
14 |
15 |
60 | {% endblock %}
--------------------------------------------------------------------------------
/apps/core/templates/core/plans.html:
--------------------------------------------------------------------------------
1 | {% extends 'core/base.html' %}
2 |
3 | {% block title %}Plans | {% endblock %}
4 |
5 | {% block content %}
6 |
7 |
8 |
9 |
Plans
10 | Find the most suitable plan for your team
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
25 |
26 |
27 |
28 | 3 projects
29 | 1 member
30 | 10 tasks per project
31 |
32 |
33 |
34 |
35 |
Get started
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
50 |
51 |
52 |
53 | 10 projects
54 | 15 member
55 | 30 tasks per project
56 |
57 |
58 |
59 |
60 |
Get started
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
75 |
76 |
77 |
78 | 25 projects
79 | 50 member
80 | 100 tasks per project
81 |
82 |
83 |
84 |
85 |
Get started
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
100 |
101 |
102 |
103 | Need more projects?
104 | Need more members?
105 | Need more tasks?
106 |
107 |
108 |
109 |
110 |
Contact us
111 |
112 |
113 |
114 |
115 |
116 | {% endblock %}
--------------------------------------------------------------------------------
/apps/core/templates/core/privacy.html:
--------------------------------------------------------------------------------
1 | {% extends 'core/base.html' %}
2 |
3 | {% block title %}Privacy policy | {% endblock %}
4 |
5 | {% block content %}
6 |
7 |
8 |
9 |
Privacy policy
10 | Some random information....
11 |
12 |
13 |
14 | {% endblock %}
--------------------------------------------------------------------------------
/apps/core/templates/core/signup.html:
--------------------------------------------------------------------------------
1 | {% extends 'core/base.html' %}
2 |
3 | {% block title %}Sign up | {% endblock %}
4 |
5 | {% block content %}
6 |
7 |
8 |
9 |
Sign up
10 | Get a free account and start tracking time
11 |
12 |
13 |
14 |
15 |
79 | {% endblock %}
80 |
81 | {% block scripts %}
82 |
137 | {% endblock %}
--------------------------------------------------------------------------------
/apps/core/templates/core/terms.html:
--------------------------------------------------------------------------------
1 | {% extends 'core/base.html' %}
2 |
3 | {% block title %}Terms of service | {% endblock %}
4 |
5 | {% block content %}
6 |
7 |
8 |
9 |
Terms of service
10 | Some random information....
11 |
12 |
13 |
14 | {% endblock %}
--------------------------------------------------------------------------------
/apps/core/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/apps/core/views.py:
--------------------------------------------------------------------------------
1 | #
2 | # Import functionality from Django
3 |
4 | from django.contrib.auth import login
5 | from django.contrib.auth.decorators import login_required
6 | from django.contrib.auth.forms import UserCreationForm
7 | from django.shortcuts import render, redirect
8 |
9 |
10 | #
11 | #
12 |
13 | from apps.team.models import Invitation
14 | from apps.userprofile.models import Userprofile
15 |
16 |
17 | #
18 | # Views
19 |
20 | def frontpage(request):
21 | return render(request, 'core/frontpage.html')
22 |
23 | def privacy(request):
24 | return render(request, 'core/privacy.html')
25 |
26 | def terms(request):
27 | return render(request, 'core/terms.html')
28 |
29 | def plans(request):
30 | return render(request, 'core/plans.html')
31 |
32 | def signup(request):
33 | if request.method == 'POST':
34 | form = UserCreationForm(request.POST)
35 |
36 | if form.is_valid():
37 | user = form.save()
38 | user.email = user.username
39 | user.save()
40 |
41 | userprofile = Userprofile.objects.create(user=user)
42 |
43 | login(request, user)
44 |
45 | invitations = Invitation.objects.filter(email=user.email, status=Invitation.INVITED)
46 |
47 | if invitations:
48 | return redirect('accept_invitation')
49 | else:
50 | return redirect('dashboard')
51 | else:
52 | form = UserCreationForm()
53 |
54 | return render(request, 'core/signup.html', {'form': form})
--------------------------------------------------------------------------------
/apps/dashboard/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SteinOveHelset/minutos/73cdcb44409f97b05680c6a048f80eb4bd3f1f46/apps/dashboard/__init__.py
--------------------------------------------------------------------------------
/apps/dashboard/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | # Register your models here.
4 |
--------------------------------------------------------------------------------
/apps/dashboard/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class DashboardConfig(AppConfig):
5 | name = 'dashboard'
6 |
--------------------------------------------------------------------------------
/apps/dashboard/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SteinOveHelset/minutos/73cdcb44409f97b05680c6a048f80eb4bd3f1f46/apps/dashboard/migrations/__init__.py
--------------------------------------------------------------------------------
/apps/dashboard/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 | # Create your models here.
4 |
--------------------------------------------------------------------------------
/apps/dashboard/templates/dashboard/dashboard.html:
--------------------------------------------------------------------------------
1 | {% extends 'core/base.html' %}
2 |
3 | {% load dashboardextras %}
4 |
5 | {% block title %}Dashboard | {% endblock %}
6 |
7 | {% block content %}
8 |
9 |
10 |
11 |
Your time {% if num_days is 0 %}today{% else %}{{ date_user|date:"Y-m-d" }}{% endif %}
12 |
13 | {% if date_entries %}
14 |
15 |
16 |
17 | Task
18 | Project
19 | Time
20 |
21 |
22 |
23 |
24 | {% for entry in date_entries %}
25 |
26 | {{ entry.task.title }}
27 | {{ entry.project.title }}
28 | {{ entry.minutes|format_minutes }}
29 |
30 | {% endfor %}
31 |
32 |
33 |
34 |
35 | Total
36 | {{ time_for_user_and_date|format_minutes }}
37 |
38 |
39 |
40 | {% else %}
41 |
No entries today...
42 | {% endif %}
43 |
44 |
Previous
45 | {% if num_days > 0 %}
46 |
Next
47 | {% endif %}
48 |
49 |
50 |
51 |
52 |
53 |
Your time {% if user_num_months is 0 %}this month{% else %}{{ user_month|date:"Y-m" }}{% endif %}
54 |
55 | {% if time_for_user_and_month %}
56 |
57 |
58 |
59 | Project
60 | Time
61 |
62 |
63 |
64 |
65 | {% for project in all_projects %}
66 | {% if project.time_for_user_and_project_and_month > 0 %}
67 |
68 | {{ project.title }}
69 | {{ project.time_for_user_and_project_and_month|format_minutes }}
70 |
71 | {% endif %}
72 | {% endfor %}
73 |
74 |
75 |
76 |
77 | Total
78 | {{ time_for_user_and_month|format_minutes }}
79 |
80 |
81 |
82 | {% else %}
83 |
No entries this month...
84 | {% endif %}
85 |
86 |
Previous
87 | {% if user_num_months > 0 %}
88 |
Next
89 | {% endif %}
90 |
91 |
92 |
93 |
94 |
95 |
Your untracked entries
96 |
97 |
98 |
99 |
100 | Started
101 | Time
102 | Status
103 | Add to task
104 | Discard
105 |
106 |
107 |
108 |
109 | {% for entry in untracked_entries %}
110 |
111 | {{ entry.created_at }}
112 |
113 | {% if entry.minutes == 0 %}
114 | {{ entry.minutes_since|format_minutes }}
115 | {% else %}
116 | {{ entry.minutes|format_minutes }}
117 | {% endif %}
118 |
119 | {% if entry.minutes == 0 %}In progress{% else %}Untracked{% endif %}
120 | Add to task
121 | Delete
122 |
123 | {% endfor %}
124 |
125 |
126 |
127 |
128 |
129 | {% if request.user == team.created_by %}
130 |
131 |
132 |
Your team {% if team_num_months is 0 %}this month{% else %}{{ team_month|date:"Y-m" }}{% endif %}
133 |
134 | {% if time_for_team_and_month > 0 %}
135 |
136 |
137 |
138 | Name
139 | Time
140 |
141 |
142 |
143 |
144 | {% for member in members %}
145 |
146 | {% firstof member.get_full_name member.username %}
147 | {{ member.time_for_user_and_team_and_month|format_minutes }}
148 |
149 | {% endfor %}
150 |
151 |
152 | {% else %}
153 |
No registred time for your team this month...
154 | {% endif %}
155 |
156 |
Previous
157 | {% if team_num_months > 0 %}
158 |
Next
159 | {% endif %}
160 |
161 |
162 | {% endif %}
163 |
164 |
165 |
166 |
167 |
Newest projects
168 |
169 |
170 | {% for project in projects %}
171 |
172 |
173 |
{{ project.title }}
174 |
175 |
Registered time: {{ project.registered_time|format_minutes }}
176 |
Tasks: {{ project.num_tasks_todo }}
177 |
178 |
179 |
180 |
Details
181 |
182 |
183 | {% empty %}
184 |
185 |
186 |
No projects yet...
187 |
188 |
189 | {% endfor %}
190 |
191 | {% endblock %}
--------------------------------------------------------------------------------
/apps/dashboard/templates/dashboard/view_user.html:
--------------------------------------------------------------------------------
1 | {% extends 'core/base.html' %}
2 |
3 | {% load dashboardextras %}
4 |
5 | {% block title %}{% firstof user.get_full_name user.username %} | {% endblock %}
6 |
7 | {% block content %}
8 |
9 |
13 |
14 |
15 |
16 |
30 |
31 |
32 |
33 |
{% firstof user.get_full_name user.username %}'s time {% if num_days is 0 %}today{% else %}{{ date_user|date:"Y-m-d" }}{% endif %}
34 |
35 | {% if date_entries %}
36 |
37 |
38 |
39 | Task
40 | Project
41 | Time
42 |
43 |
44 |
45 |
46 | {% for entry in date_entries %}
47 |
48 | {{ entry.task.title }}
49 | {{ entry.project.title }}
50 | {{ entry.minutes|format_minutes }}
51 |
52 | {% endfor %}
53 |
54 |
55 |
56 |
57 | Total
58 | {{ time_for_user_and_date|format_minutes }}
59 |
60 |
61 |
62 | {% else %}
63 |
No entries today...
64 | {% endif %}
65 |
66 |
Previous
67 | {% if num_days > 0 %}
68 |
Next
69 | {% endif %}
70 |
71 |
72 |
73 |
74 |
75 |
{% firstof user.get_full_name user.username %}'s time {% if user_num_months is 0 %}this month{% else %}{{ user_month|date:"Y-m" }}{% endif %}
76 |
77 | {% if time_for_user_and_month %}
78 |
79 |
80 |
81 | Project
82 | Time
83 |
84 |
85 |
86 |
87 | {% for project in all_projects %}
88 | {% if project.time_for_user_and_project_and_month > 0 %}
89 |
90 | {{ project.title }}
91 | {{ project.time_for_user_and_project_and_month|format_minutes }}
92 |
93 | {% endif %}
94 | {% endfor %}
95 |
96 |
97 |
98 |
99 | Total
100 | {{ time_for_user_and_month|format_minutes }}
101 |
102 |
103 |
104 | {% else %}
105 |
No entries this month...
106 | {% endif %}
107 |
108 |
Previous
109 | {% if user_num_months > 0 %}
110 |
Next
111 | {% endif %}
112 |
113 |
114 |
115 | {% endblock %}
--------------------------------------------------------------------------------
/apps/dashboard/templatetags/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SteinOveHelset/minutos/73cdcb44409f97b05680c6a048f80eb4bd3f1f46/apps/dashboard/templatetags/__init__.py
--------------------------------------------------------------------------------
/apps/dashboard/templatetags/dashboardextras.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 |
3 | from django import template
4 |
5 | def format_minutes(value):
6 | return '{:2d}h {:2d}m'.format(*divmod(value, 60))
7 |
8 | register = template.Library()
9 | register.filter('format_minutes', format_minutes)
--------------------------------------------------------------------------------
/apps/dashboard/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/apps/dashboard/urls.py:
--------------------------------------------------------------------------------
1 | # Import Django
2 |
3 | from django.urls import path, include
4 |
5 | # Import Views
6 |
7 | from .views import dashboard, view_user
8 |
9 | #
10 |
11 | urlpatterns = [
12 | path('', dashboard, name='dashboard'),
13 | path('/', view_user, name='view_user'),
14 | path('projects/', include('apps.project.urls')),
15 | path('myaccount/', include('apps.userprofile.urls')),
16 | path('myaccount/teams/', include('apps.team.urls')),
17 | ]
--------------------------------------------------------------------------------
/apps/dashboard/utilities.py:
--------------------------------------------------------------------------------
1 | # Import Python
2 |
3 | from datetime import datetime
4 |
5 | # Models
6 |
7 | from apps.project.models import Entry
8 |
9 | # Utility functions
10 |
11 | def get_time_for_user_and_date(team, user, date):
12 | entries = Entry.objects.filter(team=team, created_by=user, created_at__date=date, is_tracked=True)
13 |
14 | return sum(entry.minutes for entry in entries)
15 |
16 | def get_time_for_team_and_month(team, month):
17 | entries = Entry.objects.filter(team=team, created_at__year=month.year, created_at__month=month.month, is_tracked=True)
18 |
19 | return sum(entry.minutes for entry in entries)
20 |
21 | def get_time_for_user_and_month(team, user, month):
22 | entries = Entry.objects.filter(team=team, created_by=user, created_at__year=month.year, created_at__month=month.month, is_tracked=True)
23 |
24 | return sum(entry.minutes for entry in entries)
25 |
26 | def get_time_for_user_and_project_and_month(team, project, user, month):
27 | entries = Entry.objects.filter(team=team, project=project, created_by=user, created_at__year=month.year, created_at__month=month.month, is_tracked=True)
28 |
29 | return sum(entry.minutes for entry in entries)
30 |
31 | def get_time_for_user_and_team_month(team, user, month):
32 | entries = Entry.objects.filter(team=team, created_by=user, created_at__year=month.year, created_at__month=month.month, is_tracked=True)
33 |
34 | return sum(entry.minutes for entry in entries)
--------------------------------------------------------------------------------
/apps/dashboard/views.py:
--------------------------------------------------------------------------------
1 | # Import Python
2 |
3 | from datetime import datetime, timedelta, timezone
4 | from dateutil.relativedelta import relativedelta
5 |
6 | # Import Django
7 |
8 | from django.contrib.auth.decorators import login_required
9 | from django.contrib.auth.models import User
10 | from django.shortcuts import render, redirect, get_object_or_404
11 |
12 | # Import models
13 |
14 | from apps.project.models import Entry
15 | from apps.team.models import Team
16 |
17 | # Import utilities
18 |
19 | from .utilities import get_time_for_user_and_date, get_time_for_team_and_month, get_time_for_user_and_month, get_time_for_user_and_project_and_month, get_time_for_user_and_team_month
20 |
21 | # Views
22 |
23 | @login_required
24 | def dashboard(request):
25 | # Check if active team
26 |
27 | if not request.user.userprofile.active_team_id:
28 | return redirect('myaccount')
29 |
30 | # Get team and set variable
31 |
32 | team = get_object_or_404(Team, pk=request.user.userprofile.active_team_id, status=Team.ACTIVE)
33 | all_projects = team.projects.all()
34 | members = team.members.all()
35 |
36 | # User date, pagination
37 |
38 | num_days = int(request.GET.get('num_days', 0))
39 | date_user = datetime.now() - timedelta(days=num_days)
40 |
41 | date_entries = Entry.objects.filter(team=team, created_by=request.user, created_at__date=date_user, is_tracked=True)
42 |
43 | # User month, pagination
44 |
45 | user_num_months = int(request.GET.get('user_num_months', 0))
46 | user_month = datetime.now()-relativedelta(months=user_num_months)
47 |
48 | for project in all_projects:
49 | project.time_for_user_and_project_and_month = get_time_for_user_and_project_and_month(team, project, request.user, user_month)
50 |
51 | # Team month, pagination
52 |
53 | team_num_months = int(request.GET.get('team_num_months', 0))
54 | team_month = datetime.now()-relativedelta(months=team_num_months)
55 |
56 | for member in members:
57 | member.time_for_user_and_team_and_month = get_time_for_user_and_team_month(team, member, team_month)
58 |
59 | #
60 |
61 | untracked_entries = Entry.objects.filter(team=team, created_by=request.user, is_tracked=False).order_by('-created_at')
62 |
63 | for untracked_entry in untracked_entries:
64 | untracked_entry.minutes_since = int((datetime.now(timezone.utc) - untracked_entry.created_at).total_seconds() / 60)
65 |
66 | # Context
67 |
68 | context = {
69 | 'team': team,
70 | 'all_projects': all_projects,
71 | 'projects': all_projects[0:4],
72 | 'date_entries': date_entries,
73 | 'num_days': num_days,
74 | 'date_user': date_user,
75 | 'members': members,
76 | 'untracked_entries': untracked_entries,
77 | 'user_num_months': user_num_months,
78 | 'user_month': user_month,
79 | 'time_for_user_and_month': get_time_for_user_and_month(team, request.user, user_month),
80 | 'time_for_user_and_date': get_time_for_user_and_date(team, request.user, date_user),
81 | 'time_for_team_and_month': get_time_for_team_and_month(team, team_month),
82 | 'team_num_months': team_num_months,
83 | 'team_month': team_month
84 | }
85 |
86 | return render(request, 'dashboard/dashboard.html', context)
87 |
88 | @login_required
89 | def view_user(request, user_id):
90 | # Get team, user and set variables
91 |
92 | team = get_object_or_404(Team, pk=request.user.userprofile.active_team_id, status=Team.ACTIVE)
93 | all_projects = team.projects.all()
94 | user = team.members.all().get(id=user_id)
95 |
96 | # User date, pagination
97 |
98 | num_days = int(request.GET.get('num_days', 0))
99 | date_user = datetime.now() - timedelta(days=num_days)
100 |
101 | date_entries = Entry.objects.filter(team=team, created_by=request.user, created_at__date=date_user, is_tracked=True)
102 |
103 | # User month, pagination
104 |
105 | user_num_months = int(request.GET.get('user_num_months', 0))
106 | user_month = datetime.now()-relativedelta(months=user_num_months)
107 |
108 | for project in all_projects:
109 | project.time_for_user_and_project_and_month = get_time_for_user_and_project_and_month(team, project, request.user, user_month)
110 |
111 | # Context
112 |
113 | context = {
114 | 'team': team,
115 | 'all_projects': all_projects,
116 | 'date_entries': date_entries,
117 | 'num_days': num_days,
118 | 'date_user': date_user,
119 | 'user_num_months': user_num_months,
120 | 'user_month': user_month,
121 | 'time_for_user_and_month': get_time_for_user_and_month(team, request.user, user_month),
122 | 'time_for_user_and_date': get_time_for_user_and_date(team, request.user, date_user),
123 | }
124 |
125 | return render(request, 'dashboard/view_user.html', context)
--------------------------------------------------------------------------------
/apps/project/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SteinOveHelset/minutos/73cdcb44409f97b05680c6a048f80eb4bd3f1f46/apps/project/__init__.py
--------------------------------------------------------------------------------
/apps/project/admin.py:
--------------------------------------------------------------------------------
1 | #
2 | # Import Django
3 |
4 | from django.contrib import admin
5 |
6 |
7 | #
8 | # Import models
9 |
10 | from .models import Project, Task, Entry
11 |
12 |
13 | #
14 | # Register
15 |
16 | admin.site.register(Project)
17 | admin.site.register(Task)
18 | admin.site.register(Entry)
--------------------------------------------------------------------------------
/apps/project/api.py:
--------------------------------------------------------------------------------
1 | # Import Python
2 |
3 | import json
4 |
5 | from datetime import datetime, timezone
6 |
7 | # Import Django
8 |
9 | from django.http import JsonResponse
10 | from django.shortcuts import get_object_or_404
11 |
12 | # Import Models
13 |
14 | from .models import Project, Entry
15 | from apps.team.models import Team
16 |
17 | # API View
18 |
19 | def api_start_timer(request):
20 | print(request.user)
21 | print(datetime.now())
22 | entry = Entry.objects.create(
23 | team_id=request.user.userprofile.active_team_id,
24 | minutes=0,
25 | created_by=request.user,
26 | is_tracked=False,
27 | created_at=datetime.now()
28 | )
29 |
30 | print(entry)
31 |
32 | return JsonResponse({'success': True})
33 |
34 | def api_stop_timer(request):
35 | entry = Entry.objects.get(
36 | team_id=request.user.userprofile.active_team_id,
37 | created_by=request.user,
38 | minutes=0,
39 | is_tracked=False
40 | )
41 |
42 | tracked_minutes = int((datetime.now(timezone.utc) - entry.created_at).total_seconds() / 60)
43 |
44 | if tracked_minutes < 1:
45 | tracked_minutes = 1
46 |
47 | entry.minutes = tracked_minutes
48 | entry.is_tracked = False
49 | entry.save()
50 |
51 | return JsonResponse({'success': True, 'entryID': entry.id})
52 |
53 | def api_discard_timer(request):
54 | entries = Entry.objects.filter(team_id=request.user.userprofile.active_team_id, created_by=request.user, is_tracked=False).order_by('-created_at')
55 |
56 | if entries:
57 | entry = entries.first()
58 | entry.delete()
59 |
60 | return JsonResponse({'success': True})
61 |
62 | def api_get_tasks(request):
63 | project_id = request.GET.get('project_id', '')
64 |
65 | if project_id:
66 | tasks = []
67 | team = get_object_or_404(Team, pk=request.user.userprofile.active_team_id, status=Team.ACTIVE)
68 | project = get_object_or_404(Project, pk=project_id, team=team)
69 |
70 | for task in project.tasks.all():
71 | obj = {
72 | 'id': task.id,
73 | 'title': task.title
74 | }
75 | tasks.append(obj)
76 |
77 | return JsonResponse({'success': True, 'tasks': tasks})
78 |
79 | return JsonResponse({'success': False})
80 |
--------------------------------------------------------------------------------
/apps/project/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class ProjectConfig(AppConfig):
5 | name = 'project'
6 |
--------------------------------------------------------------------------------
/apps/project/context_processors.py:
--------------------------------------------------------------------------------
1 | # Import Python
2 |
3 | from datetime import datetime, timezone
4 |
5 | # Import Django
6 |
7 | from django.shortcuts import get_object_or_404
8 |
9 | # Import Models
10 |
11 | from apps.project.models import Entry
12 | from apps.team.models import Team
13 |
14 | # Context processors
15 |
16 | def active_entry(request):
17 | if request.user.is_authenticated:
18 | if request.user.userprofile.active_team_id:
19 | team = get_object_or_404(Team, pk=request.user.userprofile.active_team_id, status=Team.ACTIVE)
20 | untracked_entries = Entry.objects.filter(team=team, created_by=request.user, minutes=0, is_tracked=False)
21 |
22 | if untracked_entries:
23 | active_entry = untracked_entries.first()
24 | active_entry.seconds_since = int((datetime.now(timezone.utc) - active_entry.created_at).total_seconds())
25 |
26 | return {'active_entry_seconds': active_entry.seconds_since, 'start_time': active_entry.created_at.isoformat()}
27 |
28 | return {'active_entry_seconds': 0, 'start_time': datetime.now().isoformat()}
--------------------------------------------------------------------------------
/apps/project/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2020-11-24 19:28
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 | ('team', '0001_initial'),
14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
15 | ]
16 |
17 | operations = [
18 | migrations.CreateModel(
19 | name='Project',
20 | fields=[
21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
22 | ('title', models.CharField(max_length=255)),
23 | ('created_at', models.DateTimeField(auto_now_add=True)),
24 | ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='projects', to=settings.AUTH_USER_MODEL)),
25 | ('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='projects', to='team.team')),
26 | ],
27 | options={
28 | 'ordering': ['title'],
29 | },
30 | ),
31 | ]
32 |
--------------------------------------------------------------------------------
/apps/project/migrations/0002_task.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2020-11-24 19:59
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('team', '0001_initial'),
12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
13 | ('project', '0001_initial'),
14 | ]
15 |
16 | operations = [
17 | migrations.CreateModel(
18 | name='Task',
19 | fields=[
20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21 | ('title', models.CharField(max_length=255)),
22 | ('created_at', models.DateTimeField(auto_now_add=True)),
23 | ('status', models.CharField(choices=[('todo', 'Todo'), ('done', 'Done'), ('archived', 'Archived')], default='todo', max_length=20)),
24 | ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tasks', to=settings.AUTH_USER_MODEL)),
25 | ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tasks', to='project.project')),
26 | ('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tasks', to='team.team')),
27 | ],
28 | options={
29 | 'ordering': ['-created_at'],
30 | },
31 | ),
32 | ]
33 |
--------------------------------------------------------------------------------
/apps/project/migrations/0003_entry.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2020-11-26 19:12
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('team', '0001_initial'),
12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
13 | ('project', '0002_task'),
14 | ]
15 |
16 | operations = [
17 | migrations.CreateModel(
18 | name='Entry',
19 | fields=[
20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21 | ('minutes', models.IntegerField(default=0)),
22 | ('is_tracked', models.BooleanField(default=False)),
23 | ('created_at', models.DateTimeField(auto_now_add=True)),
24 | ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='entries', to=settings.AUTH_USER_MODEL)),
25 | ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='entries', to='project.project')),
26 | ('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='entries', to='project.task')),
27 | ('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='entries', to='team.team')),
28 | ],
29 | options={
30 | 'ordering': ['-created_at'],
31 | },
32 | ),
33 | ]
34 |
--------------------------------------------------------------------------------
/apps/project/migrations/0004_auto_20201126_1920.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2020-11-26 19:20
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('project', '0003_entry'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='entry',
15 | name='created_at',
16 | field=models.DateTimeField(),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/apps/project/migrations/0005_auto_20201206_1924.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2020-12-06 19:24
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('project', '0004_auto_20201126_1920'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name='entry',
16 | name='project',
17 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='entries', to='project.project'),
18 | ),
19 | migrations.AlterField(
20 | model_name='entry',
21 | name='task',
22 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='entries', to='project.task'),
23 | ),
24 | ]
25 |
--------------------------------------------------------------------------------
/apps/project/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SteinOveHelset/minutos/73cdcb44409f97b05680c6a048f80eb4bd3f1f46/apps/project/migrations/__init__.py
--------------------------------------------------------------------------------
/apps/project/models.py:
--------------------------------------------------------------------------------
1 | #
2 | # Import Django
3 |
4 | from django.contrib.auth.models import User
5 | from django.db import models
6 |
7 |
8 | #
9 | # Import models
10 |
11 | from apps.team.models import Team
12 |
13 |
14 | #
15 | # Models
16 |
17 | class Project(models.Model):
18 | team = models.ForeignKey(Team, related_name='projects', on_delete=models.CASCADE)
19 | title = models.CharField(max_length=255)
20 |
21 | created_by = models.ForeignKey(User, related_name='projects', on_delete=models.CASCADE)
22 | created_at = models.DateTimeField(auto_now_add=True)
23 |
24 | class Meta:
25 | ordering = ['title']
26 |
27 | def __str__(self):
28 | return self.title
29 |
30 | def registered_time(self):
31 | return sum(entry.minutes for entry in self.entries.all())
32 |
33 | def num_tasks_todo(self):
34 | return self.tasks.filter(status=Task.TODO).count()
35 |
36 | class Task(models.Model):
37 | #
38 | # Status choices
39 |
40 | TODO = 'todo'
41 | DONE = 'done'
42 | ARCHIVED = 'archived'
43 |
44 | CHOICES_STATUS = (
45 | (TODO, 'Todo'),
46 | (DONE, 'Done'),
47 | (ARCHIVED, 'Archived')
48 | )
49 |
50 | team = models.ForeignKey(Team, related_name='tasks', on_delete=models.CASCADE)
51 | project = models.ForeignKey(Project, related_name='tasks', on_delete=models.CASCADE)
52 | title = models.CharField(max_length=255)
53 | created_by = models.ForeignKey(User, related_name='tasks', on_delete=models.CASCADE)
54 | created_at = models.DateTimeField(auto_now_add=True)
55 | status = models.CharField(max_length=20, choices=CHOICES_STATUS, default=TODO)
56 |
57 | class Meta:
58 | ordering = ['-created_at']
59 |
60 | def __str__(self):
61 | return self.title
62 |
63 | def registered_time(self):
64 | return sum(entry.minutes for entry in self.entries.all())
65 |
66 | class Entry(models.Model):
67 | team = models.ForeignKey(Team, related_name='entries', on_delete=models.CASCADE)
68 | project = models.ForeignKey(Project, related_name='entries', on_delete=models.CASCADE, blank=True, null=True)
69 | task = models.ForeignKey(Task, related_name='entries', on_delete=models.CASCADE, blank=True, null=True)
70 | minutes = models.IntegerField(default=0)
71 | is_tracked = models.BooleanField(default=False)
72 | created_by = models.ForeignKey(User, related_name='entries', on_delete=models.CASCADE)
73 | created_at = models.DateTimeField()
74 |
75 | class Meta:
76 | ordering = ['-created_at']
77 |
78 | def __str__(self):
79 | if self.task:
80 | return '%s - %s' % (self.task.title, self.created_at)
81 |
82 | return '%s' % self.created_at
83 |
--------------------------------------------------------------------------------
/apps/project/templates/project/edit_entry.html:
--------------------------------------------------------------------------------
1 | {% extends 'core/base.html' %}
2 |
3 | {% block title %}Edit entry | {% endblock %}
4 |
5 | {% block content %}
6 |
7 |
8 |
14 |
15 |
16 |
17 |
18 |
Edit entry
19 |
20 |
21 |
22 |
67 |
68 | {% endblock %}
--------------------------------------------------------------------------------
/apps/project/templates/project/edit_project.html:
--------------------------------------------------------------------------------
1 | {% extends 'core/base.html' %}
2 |
3 | {% block title %}Edit project | {% endblock %}
4 |
5 | {% block content %}
6 |
7 |
8 |
14 |
15 |
16 |
17 |
18 |
Edit project
19 |
20 |
21 |
22 |
43 |
44 | {% endblock %}
--------------------------------------------------------------------------------
/apps/project/templates/project/edit_task.html:
--------------------------------------------------------------------------------
1 | {% extends 'core/base.html' %}
2 |
3 | {% block title %}Edit task | {% endblock %}
4 |
5 | {% block content %}
6 |
7 |
8 |
14 |
15 |
16 |
17 |
18 |
Edit task
19 |
20 |
21 |
22 |
56 |
57 | {% endblock %}
--------------------------------------------------------------------------------
/apps/project/templates/project/project.html:
--------------------------------------------------------------------------------
1 | {% extends 'core/base.html' %}
2 |
3 | {% block title %}{{ project.title }} | {% endblock %}
4 |
5 | {% block content %}
6 |
7 |
8 |
13 |
14 |
15 |
16 |
17 |
{{ project.title }}
18 |
19 |
36 |
37 |
38 |
39 | {% if not tasks_todo and not tasks_done %}
40 |
41 |
42 |
43 |
No tasks yet...
44 |
45 |
46 |
47 | {% endif %}
48 |
49 |
50 | {% if tasks_todo %}
51 |
52 |
Todo
53 |
54 |
55 | {% for task in tasks_todo %}
56 |
57 |
58 |
{{ task.title }}
59 |
60 |
Registered time: {{ task.registered_time }}
61 |
62 |
63 |
64 |
Details
65 |
66 |
67 | {% endfor %}
68 | {% endif %}
69 |
70 | {% if tasks_done %}
71 |
72 |
Done
73 |
74 |
75 | {% for task in tasks_done %}
76 |
77 |
78 |
{{ task.title }}
79 |
80 |
Registered time: {{ task.registered_time }}
81 |
82 |
83 |
84 |
Details
85 |
86 |
87 | {% endfor %}
88 | {% endif %}
89 |
90 |
91 |
122 |
123 | {% endblock %}
124 |
125 | {% block scripts %}
126 |
153 | {% endblock %}
--------------------------------------------------------------------------------
/apps/project/templates/project/projects.html:
--------------------------------------------------------------------------------
1 | {% extends 'core/base.html' %}
2 |
3 | {% load dashboardextras %}
4 |
5 | {% block title %}Projects | {% endblock %}
6 |
7 | {% block content %}
8 |
9 |
10 |
14 |
15 |
16 |
17 |
18 |
Projects
19 |
20 | {% if team.plan.max_projects_per_team > team.projects.count %}
21 |
Add project
22 | {% else %}
23 |
24 | This team has reached the limit of projects.
25 |
26 | {% if team.created_by == request.user %}
27 |
Upgrade plan
28 | {% else %}
29 | Contact your team owner...
30 | {% endif %}
31 |
32 | {% endif %}
33 |
34 |
35 |
36 |
37 | {% for project in projects %}
38 |
39 |
40 |
{{ project.title }}
41 |
Registered time: {{ project.registered_time|format_minutes }}
42 |
Tasks: {{ project.num_tasks_todo }}
43 |
44 |
45 |
46 |
Details
47 |
48 |
49 | {% empty %}
50 |
51 |
52 | No projects yet...
53 |
54 |
55 | {% endfor %}
56 |
57 |
58 |
89 |
90 | {% endblock %}
91 |
92 | {% block scripts %}
93 |
120 | {% endblock %}
--------------------------------------------------------------------------------
/apps/project/templates/project/task.html:
--------------------------------------------------------------------------------
1 | {% extends 'core/base.html' %}
2 |
3 | {% block title %}{{ task.title }} | {% endblock %}
4 |
5 | {% block content %}
6 |
7 |
8 |
14 |
15 |
16 |
17 |
18 |
{{ task.title }} ({{ task.get_status_display }})
19 |
20 |
Edit task
21 |
22 |
Register time
23 |
24 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
Logged entries
73 |
74 | {% if task.entries.all %}
75 |
76 |
77 |
78 | Date
79 | User
80 | Time
81 |
82 |
83 |
84 |
85 |
86 | {% for entry in task.entries.all %}
87 |
88 | {{ entry.created_at }}
89 | {% firstof entry.created_by.get_full_name entry.created_by.username %}
90 | {{ entry.minutes }}
91 |
92 | Edit
93 | Delete
94 |
95 |
96 | {% endfor %}
97 |
98 |
99 |
100 |
101 | Total
102 | {{ task.registered_time }}
103 |
104 |
105 |
106 | {% else %}
107 |
No entries yet...
108 | {% endif %}
109 |
110 |
111 |
112 | {% endblock %}
--------------------------------------------------------------------------------
/apps/project/templates/project/track_entry.html:
--------------------------------------------------------------------------------
1 | {% extends 'core/base.html' %}
2 |
3 | {% block title %}Track entry | {% endblock %}
4 |
5 | {% block content %}
6 |
7 |
12 |
13 |
14 |
15 |
16 |
Track entry
17 |
18 |
88 |
89 |
90 | {% endblock %}
91 |
92 | {% block scripts %}
93 |
131 | {% endblock %}
132 |
--------------------------------------------------------------------------------
/apps/project/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/apps/project/urls.py:
--------------------------------------------------------------------------------
1 | #
2 | # Import Django
3 |
4 | from django.urls import path
5 |
6 |
7 | #
8 | # Import views
9 |
10 | from .api import api_start_timer, api_stop_timer, api_discard_timer, api_get_tasks
11 | from .views import projects, project, edit_project, task, edit_task, edit_entry, delete_entry, delete_untracked_entry, track_entry
12 |
13 |
14 | #
15 | # Url pattern
16 |
17 | app_name = 'project'
18 |
19 | urlpatterns = [
20 | path('', projects, name='projects'),
21 | path('/', project, name='project'),
22 | path('//', task, name='task'),
23 | path('//edit/', edit_task, name='edit_task'),
24 | path('///edit/', edit_entry, name='edit_entry'),
25 | path('///delete/', delete_entry, name='delete_entry'),
26 | path('/edit/', edit_project, name='edit_project'),
27 | path('delete_untracked_entry//', delete_untracked_entry, name='delete_untracked_entry'),
28 | path('track_entry//', track_entry, name='track_entry'),
29 |
30 | # API
31 |
32 | path('api/start_timer/', api_start_timer, name='api_start_timer'),
33 | path('api/stop_timer/', api_stop_timer, name='api_stop_timer'),
34 | path('api/discard_timer/', api_discard_timer, name='api_discard_timer'),
35 | path('api/get_tasks/', api_get_tasks, name='api_get_tasks'),
36 | ]
--------------------------------------------------------------------------------
/apps/project/views.py:
--------------------------------------------------------------------------------
1 | #
2 | # Import Python
3 |
4 | from datetime import datetime
5 |
6 |
7 | #
8 | # Import Django
9 |
10 | from django.contrib import messages
11 | from django.contrib.auth.decorators import login_required
12 | from django.shortcuts import render, redirect, get_object_or_404
13 |
14 |
15 | #
16 | # Import models
17 |
18 | from .models import Project, Task, Entry
19 | from apps.team.models import Team
20 |
21 |
22 | #
23 | # View
24 |
25 | @login_required
26 | def projects(request):
27 | team = get_object_or_404(Team, pk=request.user.userprofile.active_team_id, status=Team.ACTIVE)
28 | projects = team.projects.all()
29 |
30 | if request.method == 'POST':
31 | title = request.POST.get('title')
32 |
33 | if title:
34 | project = Project.objects.create(team=team, title=title, created_by=request.user)
35 |
36 | messages.info(request, 'The project was added!')
37 |
38 | return redirect('project:projects')
39 |
40 | return render(request, 'project/projects.html', {'team': team, 'projects': projects})
41 |
42 | @login_required
43 | def project(request, project_id):
44 | team = get_object_or_404(Team, pk=request.user.userprofile.active_team_id, status=Team.ACTIVE)
45 | project = get_object_or_404(Project, team=team, pk=project_id)
46 |
47 | if request.method == 'POST':
48 | title = request.POST.get('title')
49 |
50 | if title:
51 | task = Task.objects.create(team=team, project=project, created_by=request.user, title=title)
52 |
53 | messages.info(request, 'The task was added!')
54 |
55 | return redirect('project:project', project_id=project.id)
56 |
57 | tasks_todo = project.tasks.filter(status=Task.TODO)
58 | tasks_done = project.tasks.filter(status=Task.DONE)
59 |
60 | return render(request, 'project/project.html', {'team': team, 'project': project, 'tasks_todo': tasks_todo, 'tasks_done': tasks_done})
61 |
62 | @login_required
63 | def edit_project(request, project_id):
64 | team = get_object_or_404(Team, pk=request.user.userprofile.active_team_id, status=Team.ACTIVE)
65 | project = get_object_or_404(Project, team=team, pk=project_id)
66 |
67 | if request.method == 'POST':
68 | title = request.POST.get('title')
69 |
70 | if title:
71 | project.title = title
72 | project.save()
73 |
74 | messages.info(request, 'The changes was saved!')
75 |
76 | return redirect('project:project', project_id=project.id)
77 |
78 | return render(request, 'project/edit_project.html', {'team': team, 'project': project})
79 |
80 | @login_required
81 | def task(request, project_id, task_id):
82 | team = get_object_or_404(Team, pk=request.user.userprofile.active_team_id, status=Team.ACTIVE)
83 | project = get_object_or_404(Project, team=team, pk=project_id)
84 | task = get_object_or_404(Task, pk=task_id, team=team)
85 |
86 | if request.method == 'POST':
87 | hours = int(request.POST.get('hours', 0))
88 | minutes = int(request.POST.get('minutes', 0))
89 | date = '%s %s' % (request.POST.get('date'), datetime.now().time())
90 | minutes_total = (hours * 60) + minutes
91 |
92 | entry = Entry.objects.create(team=team, project=project, task=task, minutes=minutes_total, created_by=request.user, created_at=date, is_tracked=True)
93 |
94 | return render(request, 'project/task.html', {'today': datetime.today(), 'team': team, 'project': project, 'task': task})
95 |
96 | @login_required
97 | def edit_task(request, project_id, task_id):
98 | team = get_object_or_404(Team, pk=request.user.userprofile.active_team_id, status=Team.ACTIVE)
99 | project = get_object_or_404(Project, team=team, pk=project_id)
100 | task = get_object_or_404(Task, pk=task_id, team=team)
101 |
102 | if request.method == 'POST':
103 | title = request.POST.get('title')
104 | status = request.POST.get('status')
105 |
106 | if title:
107 | task.title = title
108 | task.status = status
109 | task.save()
110 |
111 | messages.info(request, 'The changes was saved!')
112 |
113 | return redirect('project:task', project_id=project.id, task_id=task.id)
114 |
115 | return render(request, 'project/edit_task.html', {'team': team, 'project': project, 'task': task})
116 |
117 | @login_required
118 | def edit_entry(request, project_id, task_id, entry_id):
119 | team = get_object_or_404(Team, pk=request.user.userprofile.active_team_id, status=Team.ACTIVE)
120 | project = get_object_or_404(Project, team=team, pk=project_id)
121 | task = get_object_or_404(Task, pk=task_id, team=team)
122 | entry = get_object_or_404(Entry, pk=entry_id, team=team)
123 |
124 | if request.method == 'POST':
125 | hours = int(request.POST.get('hours', 0))
126 | minutes = int(request.POST.get('minutes', 0))
127 |
128 | date = '%s %s' % (request.POST.get('date'), datetime.now().time())
129 |
130 | entry.created_at = date
131 | entry.minutes = (hours * 60) + minutes
132 | entry.save()
133 |
134 | messages.info(request, 'The changes was saved!')
135 |
136 | return redirect('project:task', project_id=project.id, task_id=task.id)
137 |
138 | hours, minutes = divmod(entry.minutes, 60)
139 |
140 | context = {
141 | 'team': team,
142 | 'project': project,
143 | 'task': task,
144 | 'entry': entry,
145 | 'hours': hours,
146 | 'minutes': minutes
147 | }
148 |
149 | return render(request, 'project/edit_entry.html', context)
150 |
151 | @login_required
152 | def delete_entry(request, project_id, task_id, entry_id):
153 | team = get_object_or_404(Team, pk=request.user.userprofile.active_team_id, status=Team.ACTIVE)
154 | project = get_object_or_404(Project, team=team, pk=project_id)
155 | task = get_object_or_404(Task, pk=task_id, team=team)
156 | entry = get_object_or_404(Entry, pk=entry_id, team=team)
157 | entry.delete()
158 |
159 | messages.info(request, 'The entry was deleted!')
160 |
161 | return redirect('project:task', project_id=project.id, task_id=task.id)
162 |
163 | @login_required
164 | def delete_untracked_entry(request, entry_id):
165 | team = get_object_or_404(Team, pk=request.user.userprofile.active_team_id, status=Team.ACTIVE)
166 | entry = get_object_or_404(Entry, pk=entry_id, team=team)
167 | entry.delete()
168 |
169 | messages.info(request, 'The entry was deleted!')
170 |
171 | return redirect('dashboard')
172 |
173 | @login_required
174 | def track_entry(request, entry_id):
175 | team = get_object_or_404(Team, pk=request.user.userprofile.active_team_id, status=Team.ACTIVE)
176 | entry = get_object_or_404(Entry, pk=entry_id, team=team)
177 | projects = team.projects.all()
178 |
179 | if request.method == 'POST':
180 | hours = int(request.POST.get('hours', 0))
181 | minutes = int(request.POST.get('minutes', 0))
182 | project = request.POST.get('project')
183 | task = request.POST.get('task')
184 |
185 | if project and task:
186 | entry.project_id = project
187 | entry.task_id = task
188 | entry.minutes = (hours * 60) + minutes
189 | entry.created_at = '%s %s' % (request.POST.get('date'), entry.created_at.time())
190 | entry.is_tracked = True
191 | entry.save()
192 |
193 | messages.info(request, 'The time was tracked')
194 |
195 | return redirect('dashboard')
196 |
197 | hours, minutes = divmod(entry.minutes, 60)
198 |
199 | context = {
200 | 'hours': hours,
201 | 'minutes': minutes,
202 | 'team': team,
203 | 'projects': projects,
204 | 'entry': entry
205 | }
206 |
207 | return render(request, 'project/track_entry.html', context)
--------------------------------------------------------------------------------
/apps/team/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SteinOveHelset/minutos/73cdcb44409f97b05680c6a048f80eb4bd3f1f46/apps/team/__init__.py
--------------------------------------------------------------------------------
/apps/team/admin.py:
--------------------------------------------------------------------------------
1 | #
2 | #
3 |
4 | from django.contrib import admin
5 |
6 | #
7 | #
8 |
9 | from .models import Team, Invitation, Plan
10 |
11 | #
12 | #
13 |
14 | admin.site.register(Team)
15 | admin.site.register(Invitation)
16 | admin.site.register(Plan)
--------------------------------------------------------------------------------
/apps/team/api.py:
--------------------------------------------------------------------------------
1 | # Import functionality from Python
2 |
3 | import json
4 | import stripe
5 |
6 | # Import functionality from Django
7 |
8 | from django.conf import settings
9 | from django.contrib.auth.decorators import login_required
10 | from django.http.response import JsonResponse, HttpResponse
11 | from django.urls import reverse
12 | from django.views.decorators.csrf import csrf_exempt
13 |
14 | # Import models
15 |
16 | from .models import Team
17 |
18 | # Views
19 |
20 | @login_required
21 | def create_checkout_session(request):
22 | stripe.api_key = settings.STRIPE_SECRET_KEY
23 |
24 | data = json.loads(request.body)
25 | plan = data['plan']
26 |
27 | if plan == 'basic':
28 | price_id = settings.STRIPE_BASIC_PRICE_ID
29 | else:
30 | price_id = settings.STRIPE_PRO_PRICE_ID
31 |
32 | try:
33 | checkout_session = stripe.checkout.Session.create(
34 | client_reference_id = request.user.userprofile.active_team_id,
35 | success_url = '%s%s?session_id={CHECKOUT_SESSION_ID}' % (settings.WEBSITE_URL, reverse('team:plans_thankyou')),
36 | cancel_url = '%s%s' % (settings.WEBSITE_URL, reverse('team:plans')),
37 | payment_method_types = ['card'],
38 | mode = 'subscription',
39 | line_items = [
40 | {
41 | 'price': price_id,
42 | 'quantity': 1
43 | }
44 | ]
45 | )
46 | return JsonResponse({'sessionId': checkout_session['id']})
47 | except Exception as e:
48 | return JsonResponse({'error': str(e)})
49 |
50 | @csrf_exempt
51 | def stripe_webhook(request):
52 | stripe.api_key = settings.STRIPE_SECRET_KEY
53 | webhook_key = settings.STRIPE_WEBHOOK_KEY
54 | payload = request.body
55 | sig_header = request.META['HTTP_STRIPE_SIGNATURE']
56 | event = None
57 |
58 | try:
59 | event = stripe.Webhook.construct_event(
60 | payload, sig_header, webhook_key
61 | )
62 | except ValueError as e:
63 | # Invalid payload
64 | return HttpResponse(status=400)
65 | except stripe.error.SignatureVerificationError as e:
66 | # Invalid signature
67 | return HttpResponse(status=400)
68 |
69 | if event['type'] == 'checkout.session.completed':
70 | session = event['data']['object']
71 | team = Team.objects.get(pk=session.get('client_reference_id'))
72 | team.stripe_customer_id = session.get('customer')
73 | team.stripe_subscription_id = session.get('subscription')
74 | team.save()
75 |
76 | print('Team %s subscribed to a plan' % team.title)
77 |
78 | return HttpResponse(status=200)
--------------------------------------------------------------------------------
/apps/team/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class TeamConfig(AppConfig):
5 | name = 'team'
6 |
--------------------------------------------------------------------------------
/apps/team/context_processors.py:
--------------------------------------------------------------------------------
1 | #
2 | #
3 |
4 | from .models import Team
5 |
6 | #
7 | #
8 |
9 | def active_team(request):
10 | if request.user.is_authenticated:
11 | if request.user.userprofile.active_team_id:
12 | team = Team.objects.get(pk=request.user.userprofile.active_team_id)
13 |
14 | return {'active_team': team}
15 |
16 | return {'active_team': None}
--------------------------------------------------------------------------------
/apps/team/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2020-11-22 11:55
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='Team',
19 | fields=[
20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21 | ('title', models.CharField(max_length=255)),
22 | ('created_at', models.DateTimeField(auto_now_add=True)),
23 | ('status', models.CharField(choices=[('active', 'Active'), ('deleted', 'Deleted')], default='active', max_length=10)),
24 | ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='created_teams', to=settings.AUTH_USER_MODEL)),
25 | ('members', models.ManyToManyField(related_name='teams', to=settings.AUTH_USER_MODEL)),
26 | ],
27 | options={
28 | 'ordering': ['title'],
29 | },
30 | ),
31 | ]
32 |
--------------------------------------------------------------------------------
/apps/team/migrations/0002_invitation.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2020-11-29 11:49
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('team', '0001_initial'),
11 | ]
12 |
13 | operations = [
14 | migrations.CreateModel(
15 | name='Invitation',
16 | fields=[
17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18 | ('email', models.EmailField(max_length=254)),
19 | ('code', models.CharField(max_length=20)),
20 | ('status', models.CharField(choices=[('invited', 'Invited'), ('accepted', 'Accepted')], default='invited', max_length=20)),
21 | ('date_sent', models.DateTimeField(auto_now_add=True)),
22 | ('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invitations', to='team.team')),
23 | ],
24 | ),
25 | ]
26 |
--------------------------------------------------------------------------------
/apps/team/migrations/0003_plan.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2020-12-09 19:52
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('team', '0002_invitation'),
10 | ]
11 |
12 | operations = [
13 | migrations.CreateModel(
14 | name='Plan',
15 | fields=[
16 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
17 | ('title', models.CharField(max_length=255)),
18 | ('max_projects_per_team', models.IntegerField(default=0)),
19 | ('max_members_per_team', models.IntegerField(default=0)),
20 | ('max_tasks_per_project', models.IntegerField(default=0)),
21 | ('price', models.IntegerField(default=0)),
22 | ('is_default', models.BooleanField(default=False)),
23 | ],
24 | ),
25 | ]
26 |
--------------------------------------------------------------------------------
/apps/team/migrations/0004_auto_20201209_1955.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2020-12-09 19:55
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('team', '0003_plan'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='team',
16 | name='plan',
17 | field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='teams', to='team.plan'),
18 | preserve_default=False,
19 | ),
20 | migrations.AddField(
21 | model_name='team',
22 | name='plan_end_date',
23 | field=models.DateTimeField(blank=True, null=True),
24 | ),
25 | migrations.AddField(
26 | model_name='team',
27 | name='plan_status',
28 | field=models.CharField(choices=[('active', 'Active'), ('canceled', 'Canceled')], default='active', max_length=20),
29 | ),
30 | migrations.AddField(
31 | model_name='team',
32 | name='stripe_customer_id',
33 | field=models.CharField(blank=True, max_length=255, null=True),
34 | ),
35 | migrations.AddField(
36 | model_name='team',
37 | name='stripe_subscription_id',
38 | field=models.CharField(blank=True, max_length=255, null=True),
39 | ),
40 | ]
41 |
--------------------------------------------------------------------------------
/apps/team/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SteinOveHelset/minutos/73cdcb44409f97b05680c6a048f80eb4bd3f1f46/apps/team/migrations/__init__.py
--------------------------------------------------------------------------------
/apps/team/models.py:
--------------------------------------------------------------------------------
1 | # Import Django
2 |
3 | from django.contrib.auth.models import User
4 | from django.db import models
5 |
6 | # Models
7 |
8 | class Plan(models.Model):
9 | title = models.CharField(max_length=255)
10 | max_projects_per_team = models.IntegerField(default=0)
11 | max_members_per_team = models.IntegerField(default=0)
12 | max_tasks_per_project = models.IntegerField(default=0)
13 | price = models.IntegerField(default=0)
14 | is_default = models.BooleanField(default=False)
15 |
16 | def __str__(self):
17 | return self.title
18 |
19 | class Team(models.Model):
20 | #
21 | # Status
22 |
23 | ACTIVE = 'active'
24 | DELETED = 'deleted'
25 |
26 | CHOICES_STATUS = (
27 | (ACTIVE, 'Active'),
28 | (DELETED, 'Deleted')
29 | )
30 |
31 | #
32 | # Plan status
33 |
34 | PLAN_ACTIVE = 'active'
35 | PLAN_CANCELED = 'canceled'
36 |
37 | CHOICES_PLAN_STATUS = (
38 | (PLAN_ACTIVE, 'Active'),
39 | (PLAN_CANCELED, 'Canceled')
40 | )
41 |
42 | #
43 | # Fields
44 |
45 | title = models.CharField(max_length=255)
46 | members = models.ManyToManyField(User, related_name='teams')
47 | created_by = models.ForeignKey(User, related_name='created_teams', on_delete=models.CASCADE)
48 | created_at = models.DateTimeField(auto_now_add=True)
49 | status = models.CharField(max_length=10, choices=CHOICES_STATUS, default=ACTIVE)
50 | plan = models.ForeignKey(Plan, related_name='teams', on_delete=models.CASCADE)
51 | plan_end_date = models.DateTimeField(blank=True, null=True)
52 | plan_status = models.CharField(max_length=20, choices=CHOICES_PLAN_STATUS, default=PLAN_ACTIVE)
53 | stripe_customer_id = models.CharField(max_length=255, blank=True, null=True)
54 | stripe_subscription_id = models.CharField(max_length=255, blank=True, null=True)
55 |
56 | class Meta:
57 | ordering = ['title']
58 |
59 | def __str__(self):
60 | return self.title
61 |
62 | class Invitation(models.Model):
63 | #
64 | # Status
65 |
66 | INVITED = 'invited'
67 | ACCEPTED = 'accepted'
68 |
69 | CHOICES_STATUS = (
70 | (INVITED, 'Invited'),
71 | (ACCEPTED, 'Accepted')
72 | )
73 |
74 | team = models.ForeignKey(Team, related_name='invitations', on_delete=models.CASCADE)
75 | email = models.EmailField()
76 | code = models.CharField(max_length=20)
77 | status = models.CharField(max_length=20, choices=CHOICES_STATUS, default=INVITED)
78 | date_sent = models.DateTimeField(auto_now_add=True)
79 |
80 | def __str__(self):
81 | return self.email
--------------------------------------------------------------------------------
/apps/team/templates/team/add.html:
--------------------------------------------------------------------------------
1 | {% extends 'core/base.html' %}
2 |
3 | {% block title %}Add team | {% endblock %}
4 |
5 | {% block content %}
6 |
7 |
12 |
13 |
14 |
15 |
16 |
Add team
17 |
18 |
39 |
40 |
41 | {% endblock %}
42 |
43 | {% block scripts %}
44 |
72 | {% endblock %}
--------------------------------------------------------------------------------
/apps/team/templates/team/edit.html:
--------------------------------------------------------------------------------
1 | {% extends 'core/base.html' %}
2 |
3 | {% block title %}Edit team | {% endblock %}
4 |
5 | {% block content %}
6 |
7 |
12 |
13 |
14 |
15 |
16 |
Edit team
17 |
18 |
39 |
40 |
41 | {% endblock %}
42 |
43 | {% block scripts %}
44 |
72 | {% endblock %}
--------------------------------------------------------------------------------
/apps/team/templates/team/email_accepted_invitation.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Minutos
7 |
8 |
25 |
26 |
27 |
28 |
29 |
30 | Minutos
31 |
32 |
33 |
34 |
35 | {{ invitation.email }} accepted your invitation.
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/apps/team/templates/team/email_invitation.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Minutos
7 |
8 |
25 |
26 |
27 |
28 |
29 |
30 | Minutos - Invitation to {{ team.title }}
31 |
32 |
33 |
34 |
35 | You ahve been invited to join {{ team.title }}
36 | Click the link below to sign up or log in. Your team activation code is {{ code }}
37 |
38 |
39 |
40 |
41 |
42 | Accept invitation
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/apps/team/templates/team/invite.html:
--------------------------------------------------------------------------------
1 | {% extends 'core/base.html' %}
2 |
3 | {% block title %}Invite member | {% endblock %}
4 |
5 | {% block content %}
6 |
7 |
13 |
14 |
15 |
16 |
17 |
Invite member
18 |
19 |
22 |
23 | {% if team.plan.max_members_per_team > team.members.count %}
24 |
41 | {% else %}
42 |
43 | This team has reached the limit of members.
44 |
45 | {% if team.created_by == request.user %}
46 |
Upgrade plan
47 | {% else %}
48 | Contact your team owner...
49 | {% endif %}
50 |
51 | {% endif %}
52 |
53 |
54 | {% endblock %}
55 |
56 | {% block scripts %}
57 |
90 | {% endblock %}
--------------------------------------------------------------------------------
/apps/team/templates/team/plans.html:
--------------------------------------------------------------------------------
1 | {% extends 'core/base.html' %}
2 |
3 | {% block title %}Plans | {% endblock %}
4 |
5 | {% block content %}
6 |
7 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
25 |
26 |
27 |
28 | 3 projects
29 | 1 member
30 | 10 tasks per project
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
46 |
47 |
48 |
49 | 10 projects
50 | 15 member
51 | 30 tasks per project
52 |
53 |
54 |
55 |
56 | {% if team.plan.title == 'Basic' %}
57 |
Cancel
58 | {% else %}
59 |
Start
60 | {% endif %}
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
75 |
76 |
77 |
78 | 25 projects
79 | 50 member
80 | 100 tasks per project
81 |
82 |
83 |
84 |
85 | {% if team.plan.title == 'Pro' %}
86 |
Cancel
87 | {% else %}
88 |
Start
89 | {% endif %}
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
104 |
105 |
106 |
107 | Need more projects?
108 | Need more members?
109 | Need more tasks?
110 |
111 |
112 |
113 |
114 |
Contact us
115 |
116 |
117 |
118 |
119 |
120 | {% endblock %}
121 |
122 | {% block scripts %}
123 |
124 |
168 | {% endblock %}
--------------------------------------------------------------------------------
/apps/team/templates/team/plans_thankyou.html:
--------------------------------------------------------------------------------
1 | {% extends 'core/base.html' %}
2 |
3 | {% block title %}Thank you | {% endblock %}
4 |
5 | {% block content %}
6 |
7 |
13 |
14 |
15 |
16 |
17 | {% if error %}
18 |
Woops, something went wrong
19 |
20 | Go back to
plans and try again or
contact us !
21 | {% else %}
22 |
Thank you for purchasing a plan
23 |
24 |
Go to dashboard
25 | {% endif %}
26 |
27 |
28 | {% endblock %}
--------------------------------------------------------------------------------
/apps/team/templates/team/team.html:
--------------------------------------------------------------------------------
1 | {% extends 'core/base.html' %}
2 |
3 | {% block title %}{{ team.title }} | {% endblock %}
4 |
5 | {% block content %}
6 |
7 |
12 |
13 |
14 |
15 |
16 |
{{ team.title }}
17 |
18 |
Current plan: {{ team.plan.title }}
19 |
20 | {% if team.plan.title != 'Free' %}
21 |
Plan end date: {{ team.plan_end_date|date:"Y-m-d" }}{% if team.plan_status == 'active' %} (auto renewing){% endif %}
22 | {% endif %}
23 |
24 | {% if request.user == team.created_by %}
25 |
Invite users
26 |
Change plan
27 | {% endif %}
28 |
29 |
30 |
31 |
Members
32 |
33 | {% for member in team.members.all %}
34 |
{{ member.username }}
35 | {% endfor %}
36 |
37 |
Invitated members
38 |
39 | {% for member in invitations %}
40 |
{{ member.email }} - {{ member.date_sent|timesince }} ago
41 | {% endfor %}
42 |
43 |
44 | {% if team.created_by == request.user %}
45 |
46 |
47 |
48 |
Owner actions
49 |
50 |
Edit team
51 |
52 | {% endif %}
53 |
54 | {% endblock %}
--------------------------------------------------------------------------------
/apps/team/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/apps/team/urls.py:
--------------------------------------------------------------------------------
1 | # Import Django
2 |
3 | from django.urls import path
4 |
5 | #
6 |
7 | from .api import create_checkout_session, stripe_webhook
8 | from .views import team, add, edit, activate_team, invite, plans, plans_thankyou
9 |
10 | #
11 |
12 | app_name = 'team'
13 |
14 | #
15 |
16 | urlpatterns = [
17 | path('add/', add, name='add'),
18 | path('edit/', edit, name='edit'),
19 | path('invite/', invite, name='invite'),
20 | path('plans/', plans, name='plans'),
21 | path('plans/thank_you/', plans_thankyou, name='plans_thankyou'),
22 | path('activate_team//', activate_team, name='activate_team'),
23 | path('/', team, name='team'),
24 |
25 | # API
26 |
27 | path('api/stripe_webhook/', stripe_webhook, name='stripe_webhook'),
28 | path('api/create_checkout_session/', create_checkout_session, name='create_checkout_session'),
29 | ]
--------------------------------------------------------------------------------
/apps/team/utilities.py:
--------------------------------------------------------------------------------
1 | # Import Django
2 |
3 | from django.conf import settings
4 | from django.core.mail import EmailMultiAlternatives
5 | from django.template.loader import render_to_string
6 |
7 | #
8 |
9 | def send_invitation(to_email, code, team):
10 | from_email = settings.DEFAULT_EMAIL_FROM
11 | acceptation_url = settings.ACCEPTATION_URL
12 |
13 | subject = 'Invitation to Minutos'
14 | text_content = 'Invitation to Minutos. Your code is: %s' % code
15 | html_content = render_to_string('team/email_invitation.html', {'code': code, 'team': team, 'acceptation_url': acceptation_url})
16 |
17 | msg = EmailMultiAlternatives(subject, text_content, from_email, [to_email])
18 | msg.attach_alternative(html_content, 'text/html')
19 | msg.send()
20 |
21 | def send_invitation_accepted(team, invitation):
22 | from_email = settings.DEFAULT_EMAIL_FROM
23 | subject = 'Invitation accepted'
24 | text_content = 'Your invitation was accepted'
25 | html_content = render_to_string('team/email_accepted_invitation.html', {'team': team, 'invitation': invitation})
26 |
27 | msg = EmailMultiAlternatives(subject, text_content, from_email, [team.created_by.email])
28 | msg.attach_alternative(html_content, 'text/html')
29 | msg.send()
--------------------------------------------------------------------------------
/apps/team/views.py:
--------------------------------------------------------------------------------
1 | #
2 | # Import Python
3 |
4 | import random
5 | import stripe
6 |
7 | from datetime import datetime
8 |
9 |
10 | #
11 | # Import Django
12 |
13 | from django.conf import settings
14 | from django.contrib import messages
15 | from django.contrib.auth.decorators import login_required
16 | from django.shortcuts import render, redirect, get_object_or_404
17 |
18 |
19 | #
20 | # Import models
21 |
22 | from .models import Team, Invitation, Plan
23 |
24 |
25 | #
26 | # Import helpers
27 |
28 | from .utilities import send_invitation, send_invitation_accepted
29 |
30 |
31 | #
32 | # Views
33 |
34 | @login_required
35 | def team(request, team_id):
36 | team = get_object_or_404(Team, pk=team_id, status=Team.ACTIVE, members__in=[request.user])
37 | invitations = team.invitations.filter(status=Invitation.INVITED)
38 |
39 | return render(request, 'team/team.html', {'team': team, 'invitations': invitations})
40 |
41 | @login_required
42 | def activate_team(request, team_id):
43 | team = get_object_or_404(Team, pk=team_id, status=Team.ACTIVE, members__in=[request.user])
44 | userprofile = request.user.userprofile
45 | userprofile.active_team_id = team.id
46 | userprofile.save()
47 |
48 | messages.info(request, 'The team was activated')
49 |
50 | return redirect('team:team', team_id=team.id)
51 |
52 | @login_required
53 | def add(request):
54 | if request.method == 'POST':
55 | title = request.POST.get('title')
56 |
57 | if title:
58 | team = Team.objects.create(title=title, created_by=request.user)
59 | team.members.add(request.user)
60 | team.save()
61 |
62 | userprofile = request.user.userprofile
63 | userprofile.active_team_id = team.id
64 | userprofile.save()
65 |
66 | return redirect('myaccount')
67 |
68 | return render(request, 'team/add.html')
69 |
70 | @login_required
71 | def edit(request):
72 | team = get_object_or_404(Team, pk=request.user.userprofile.active_team_id, status=Team.ACTIVE, members__in=[request.user])
73 |
74 | if request.method == 'POST':
75 | title = request.POST.get('title')
76 |
77 | if title:
78 | team.title = title
79 | team.save()
80 |
81 | messages.info(request, 'The changes was saved')
82 |
83 | return redirect('team:team', team_id=team.id)
84 |
85 | return render(request, 'team/edit.html', {'team': team})
86 |
87 | @login_required
88 | def invite(request):
89 | team = get_object_or_404(Team, pk=request.user.userprofile.active_team_id, status=Team.ACTIVE)
90 |
91 | if request.method == 'POST':
92 | email = request.POST.get('email')
93 |
94 | if email:
95 | invitations = Invitation.objects.filter(team=team, email=email)
96 |
97 | if not invitations:
98 | code = ''.join(random.choice('abcdefghijklmnopqrstuvwxyz123456789') for i in range(4))
99 | invitation = Invitation.objects.create(team=team, email=email, code=code)
100 |
101 | messages.info(request, 'The user was invited')
102 |
103 | send_invitation(email, code, team)
104 |
105 | return redirect('team:team', team_id=team.id)
106 | else:
107 | messages.info(request, 'The users has already been invited')
108 |
109 | return render(request, 'team/invite.html', {'team': team})
110 |
111 | @login_required
112 | def plans(request):
113 | team = get_object_or_404(Team, pk=request.user.userprofile.active_team_id, status=Team.ACTIVE)
114 | error = ''
115 |
116 | if request.GET.get('cancel_plan', ''):
117 | try:
118 | plan_default = Plan.objects.get(is_default=True)
119 |
120 | team.plan = plan_default
121 | team.plan_status = Team.PLAN_CANCELED
122 | team.save()
123 |
124 | stripe.api_key = settings.STRIPE_SECRET_KEY
125 | stripe.Subscription.delete(team.stripe_subscription_id)
126 | except Exception:
127 | error = 'Something went wrong with the cancelation. Please try again!'
128 |
129 | context = {
130 | 'team': team,
131 | 'error': error,
132 | 'stripe_pub_key': settings.STRIPE_PUBLISHABLE_KEY
133 | }
134 |
135 | return render(request, 'team/plans.html', context)
136 |
137 | @login_required
138 | def plans_thankyou(request):
139 | error = ''
140 |
141 | try:
142 | team = get_object_or_404(Team, pk=request.user.userprofile.active_team_id, status=Team.ACTIVE)
143 | stripe.api_key = settings.STRIPE_SECRET_KEY
144 | subscription = stripe.Subscription.retrieve(team.stripe_subscription_id)
145 | product = stripe.Product.retrieve(subscription.plan.product)
146 |
147 | team.plan_status = Team.PLAN_ACTIVE
148 | team.plan_end_date = datetime.fromtimestamp(subscription.current_period_end)
149 | team.plan = Plan.objects.get(title=product.name)
150 | team.save()
151 | except Exception:
152 | error = 'There something wrong. Please try again!'
153 |
154 | return render(request, 'team/plans_thankyou.html', {'error': error})
--------------------------------------------------------------------------------
/apps/userprofile/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SteinOveHelset/minutos/73cdcb44409f97b05680c6a048f80eb4bd3f1f46/apps/userprofile/__init__.py
--------------------------------------------------------------------------------
/apps/userprofile/admin.py:
--------------------------------------------------------------------------------
1 | #
2 | # Import Django
3 |
4 | from django.contrib import admin
5 |
6 |
7 | #
8 | # Import models
9 |
10 | from .models import Userprofile
11 |
12 |
13 | #
14 | # Register
15 |
16 | admin.site.register(Userprofile)
--------------------------------------------------------------------------------
/apps/userprofile/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class UserprofileConfig(AppConfig):
5 | name = 'userprofile'
6 |
--------------------------------------------------------------------------------
/apps/userprofile/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2020-11-19 16:51
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='Userprofile',
19 | fields=[
20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21 | ('active_team_id', models.IntegerField(default=0)),
22 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='userprofile', to=settings.AUTH_USER_MODEL)),
23 | ],
24 | ),
25 | ]
26 |
--------------------------------------------------------------------------------
/apps/userprofile/migrations/0002_auto_20201122_1210.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2020-11-22 12:10
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
12 | ('userprofile', '0001_initial'),
13 | ]
14 |
15 | operations = [
16 | migrations.AlterField(
17 | model_name='userprofile',
18 | name='user',
19 | field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='userprofile', to=settings.AUTH_USER_MODEL),
20 | ),
21 | ]
22 |
--------------------------------------------------------------------------------
/apps/userprofile/migrations/0003_userprofile_avatar.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.3 on 2020-12-01 20:08
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('userprofile', '0002_auto_20201122_1210'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='userprofile',
15 | name='avatar',
16 | field=models.ImageField(blank=True, null=True, upload_to='uploads/avatars/'),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/apps/userprofile/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SteinOveHelset/minutos/73cdcb44409f97b05680c6a048f80eb4bd3f1f46/apps/userprofile/migrations/__init__.py
--------------------------------------------------------------------------------
/apps/userprofile/models.py:
--------------------------------------------------------------------------------
1 | #
2 | #
3 |
4 | from django.contrib.auth.models import User
5 | from django.db import models
6 |
7 |
8 | #
9 | # Models
10 |
11 | class Userprofile(models.Model):
12 | user = models.OneToOneField(User, related_name='userprofile', on_delete=models.CASCADE)
13 | active_team_id = models.IntegerField(default=0)
14 | avatar = models.ImageField(upload_to='uploads/avatars/', blank=True, null=True)
15 |
16 | def get_avatar(self):
17 | if self.avatar:
18 | return self.avatar.url
19 | else:
20 | return '/static/images/avatar.png'
--------------------------------------------------------------------------------
/apps/userprofile/templates/userprofile/accept_invitation.html:
--------------------------------------------------------------------------------
1 | {% extends 'core/base.html' %}
2 |
3 | {% block title %}Accept invitation | {% endblock %}
4 |
5 | {% block content %}
6 |
7 |
12 |
13 |
14 |
15 |
16 |
Accept invitation
17 |
18 |
35 |
36 |
37 | {% endblock %}
--------------------------------------------------------------------------------
/apps/userprofile/templates/userprofile/edit_profile.html:
--------------------------------------------------------------------------------
1 | {% extends 'core/base.html' %}
2 |
3 | {% block title %}Edit profile | {% endblock %}
4 |
5 | {% block content %}
6 |
7 |
12 |
13 |
14 |
15 |
16 |
Edit profile
17 |
18 |
63 |
64 |
65 | {% endblock %}
--------------------------------------------------------------------------------
/apps/userprofile/templates/userprofile/myaccount.html:
--------------------------------------------------------------------------------
1 | {% extends 'core/base.html' %}
2 |
3 | {% block title %}My account | {% endblock %}
4 |
5 | {% block content %}
6 |
7 |
11 |
12 |
13 |
37 |
38 |
39 |
40 | {% if invitations %}
41 |
42 |
You have pending invitations
43 |
44 | {% for invitation in invitations %}
45 |
{{ invitation.team.title }}
46 | {% endfor %}
47 |
48 | {% endif %}
49 |
50 |
57 |
58 |
59 | {% if active_team %}
60 |
61 |
62 |
{{ active_team.title }}{% if active_team.created_by == request.user %}(Owner){% else %}(Member){% endif %}
63 |
64 |
SHOW PLAN HERE
65 |
66 |
67 |
68 |
Details
69 |
70 | {% if active_team.created_by == request.user %}
71 |
Invite users
72 | {% endif %}
73 |
74 |
75 | {% endif %}
76 |
77 | {% for team in teams %}
78 |
79 |
80 |
{{ team.title }}{% if team.created_by == request.user %}(Owner){% else %}(Member){% endif %}
81 |
82 |
SHOW PLAN HERE
83 |
84 |
85 |
86 |
Details
87 |
Activate
88 |
89 |
90 | {% endfor %}
91 |
92 | {% endblock %}
--------------------------------------------------------------------------------
/apps/userprofile/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/apps/userprofile/urls.py:
--------------------------------------------------------------------------------
1 | #
2 | #
3 |
4 | from django.urls import path
5 |
6 |
7 | #
8 | #
9 |
10 | from .views import myaccount, edit_profile, accept_invitation
11 |
12 | #
13 | #
14 |
15 | urlpatterns = [
16 | path('', myaccount, name='myaccount'),
17 | path('edit_profile/', edit_profile, name='edit_profile'),
18 | path('accept_invitation/', accept_invitation, name='accept_invitation'),
19 | ]
--------------------------------------------------------------------------------
/apps/userprofile/views.py:
--------------------------------------------------------------------------------
1 | #
2 | #
3 |
4 | from django.contrib import messages
5 | from django.contrib.auth.decorators import login_required
6 | from django.contrib.auth.models import User
7 | from django.shortcuts import render, redirect
8 |
9 |
10 | #
11 | # Import models
12 |
13 | from .models import Userprofile
14 | from apps.team.models import Team, Invitation
15 | from apps.team.utilities import send_invitation_accepted
16 |
17 |
18 | #
19 | # Views
20 |
21 | @login_required
22 | def myaccount(request):
23 | teams = request.user.teams.exclude(pk=request.user.userprofile.active_team_id)
24 | invitations = Invitation.objects.filter(email=request.user.email, status=Invitation.INVITED)
25 |
26 | return render(request, 'userprofile/myaccount.html', {'teams': teams, 'invitations': invitations})
27 |
28 | @login_required
29 | def edit_profile(request):
30 | if request.method == 'POST':
31 | request.user.first_name = request.POST.get('first_name', '')
32 | request.user.last_name = request.POST.get('last_name', '')
33 | request.user.email = request.POST.get('email', '')
34 | request.user.save()
35 |
36 | if request.FILES:
37 | avatar = request.FILES['avatar']
38 | userprofile = request.user.userprofile
39 | userprofile.avatar = avatar
40 | userprofile.save()
41 |
42 | messages.info(request, 'The changes was saved')
43 |
44 | return redirect('myaccount')
45 |
46 | return render(request, 'userprofile/edit_profile.html')
47 |
48 | @login_required
49 | def accept_invitation(request):
50 | if request.method == 'POST':
51 | code = request.POST.get('code')
52 |
53 | invitations = Invitation.objects.filter(code=code, email=request.user.email)
54 |
55 | if invitations:
56 | invitation = invitations[0]
57 | invitation.status = Invitation.ACCEPTED
58 | invitation.save()
59 |
60 | team = invitation.team
61 | team.members.add(request.user)
62 | team.save()
63 |
64 | userprofile = request.user.userprofile
65 | userprofile.active_team_id = team.id
66 | userprofile.save()
67 |
68 | messages.info(request, 'Invitation accepted')
69 |
70 | send_invitation_accepted(team, invitation)
71 |
72 | return redirect('team:team', team_id=team.id)
73 | else:
74 | messages.info(request, 'Invitation was not found')
75 | else:
76 | return render(request, 'userprofile/accept_invitation.html')
--------------------------------------------------------------------------------
/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', 'minutos.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 |
--------------------------------------------------------------------------------
/media/uploads/avatars/lamp.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SteinOveHelset/minutos/73cdcb44409f97b05680c6a048f80eb4bd3f1f46/media/uploads/avatars/lamp.jpeg
--------------------------------------------------------------------------------
/minutos/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SteinOveHelset/minutos/73cdcb44409f97b05680c6a048f80eb4bd3f1f46/minutos/__init__.py
--------------------------------------------------------------------------------
/minutos/asgi.py:
--------------------------------------------------------------------------------
1 | """
2 | ASGI config for minutos 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.1/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', 'minutos.settings')
15 |
16 | application = get_asgi_application()
17 |
--------------------------------------------------------------------------------
/minutos/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for minutos project.
3 |
4 | Generated by 'django-admin startproject' using Django 3.1.3.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/3.1/topics/settings/
8 |
9 | For the full list of settings and their values, see
10 | https://docs.djangoproject.com/en/3.1/ref/settings/
11 | """
12 |
13 | import os
14 |
15 | from pathlib import Path
16 |
17 | # Build paths inside the project like this: BASE_DIR / 'subdir'.
18 | BASE_DIR = Path(__file__).resolve().parent.parent
19 |
20 |
21 | # Quick-start development settings - unsuitable for production
22 | # See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
23 |
24 | # SECURITY WARNING: keep the secret key used in production secret!
25 | SECRET_KEY = 'j9eta=u(6qrfo&u1x9z8^01j-%f2s+cyozmh*#u9ec($=zd_^d'
26 |
27 | # SECURITY WARNING: don't run with debug turned on in production!
28 | DEBUG = True
29 |
30 | ALLOWED_HOSTS = []
31 |
32 | LOGIN_URL = 'login'
33 | LOGIN_REDIRECT_URL = 'myaccount'
34 | LOGOUT_REDIRECT_URL = 'frontpage'
35 |
36 | EMAIL_HOST = 'smtp.sendgrid.net'
37 | EMAIL_HOST_USER = 'apikey'
38 | EMAIL_HOST_PASSWORD = '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'
39 | EMAIL_PORT = 587
40 | EMAIL_USE_TLS = True
41 | DEFAULT_EMAIL_FROM = 'Minutos '
42 |
43 | WEBSITE_URL = 'http://127.0.0.1:8000'
44 | ACCEPTATION_URL = WEBSITE_URL + '/signup/'
45 |
46 | STRIPE_PUBLISHABLE_KEY = 'pk_test_51HIHiuKBJV2qfWbD2gQe6aqanfw6Eyul5P02KeOuSR1UMuaV4TxEtaQyzr9DbLITSZweL7XjK3p74swcGYrE2qEX00Hz7GmhMI'
47 | STRIPE_SECRET_KEY = 'sk_test_51HIHiuKBJV2qfWbD4I9pAODack7r7r9LJOY65zSFx7jUUwgy2nfKEgQGvorv1p2xP7tgMsJ5N9EW7K1lBdPnFnyK00kdrS27cj'
48 |
49 | STRIPE_BASIC_PRICE_ID = 'price_1HjJbVKBJV2qfWbDhkZdhpJL'
50 | STRIPE_PRO_PRICE_ID = 'price_1HjJcDKBJV2qfWbDiC3l4l2X'
51 |
52 | STRIPE_WEBHOOK_KEY = 'whsec_GmeT7NnXv3CsUVflcf8hq2dMQWSe3eKu'
53 |
54 | # Application definition
55 |
56 | INSTALLED_APPS = [
57 | 'django.contrib.admin',
58 | 'django.contrib.auth',
59 | 'django.contrib.contenttypes',
60 | 'django.contrib.sessions',
61 | 'django.contrib.messages',
62 | 'django.contrib.staticfiles',
63 |
64 | 'apps.core',
65 | 'apps.dashboard',
66 | 'apps.project',
67 | 'apps.team',
68 | 'apps.userprofile'
69 | ]
70 |
71 | MIDDLEWARE = [
72 | 'django.middleware.security.SecurityMiddleware',
73 | 'django.contrib.sessions.middleware.SessionMiddleware',
74 | 'django.middleware.common.CommonMiddleware',
75 | 'django.middleware.csrf.CsrfViewMiddleware',
76 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
77 | 'django.contrib.messages.middleware.MessageMiddleware',
78 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
79 | ]
80 |
81 | ROOT_URLCONF = 'minutos.urls'
82 |
83 | TEMPLATES = [
84 | {
85 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
86 | 'DIRS': [],
87 | 'APP_DIRS': True,
88 | 'OPTIONS': {
89 | 'context_processors': [
90 | 'django.template.context_processors.debug',
91 | 'django.template.context_processors.request',
92 | 'django.contrib.auth.context_processors.auth',
93 | 'django.contrib.messages.context_processors.messages',
94 | 'apps.team.context_processors.active_team',
95 | 'apps.project.context_processors.active_entry'
96 | ],
97 | },
98 | },
99 | ]
100 |
101 | WSGI_APPLICATION = 'minutos.wsgi.application'
102 |
103 |
104 | # Database
105 | # https://docs.djangoproject.com/en/3.1/ref/settings/#databases
106 |
107 | DATABASES = {
108 | 'default': {
109 | 'ENGINE': 'django.db.backends.sqlite3',
110 | 'NAME': BASE_DIR / 'db.sqlite3',
111 | }
112 | }
113 |
114 |
115 | # Password validation
116 | # https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators
117 |
118 | AUTH_PASSWORD_VALIDATORS = [
119 | {
120 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
121 | },
122 | {
123 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
124 | },
125 | ]
126 |
127 |
128 | # Internationalization
129 | # https://docs.djangoproject.com/en/3.1/topics/i18n/
130 |
131 | LANGUAGE_CODE = 'en-us'
132 |
133 | TIME_ZONE = 'UTC'
134 |
135 | USE_I18N = True
136 |
137 | USE_L10N = True
138 |
139 | USE_TZ = True
140 |
141 |
142 | # Static files (CSS, JavaScript, Images)
143 | # https://docs.djangoproject.com/en/3.1/howto/static-files/
144 |
145 | STATIC_URL = '/static/'
146 | STATICFILES_DIRS = [
147 | BASE_DIR / 'static'
148 | ]
149 |
150 | MEDIA_URL = '/media/'
151 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media/')
--------------------------------------------------------------------------------
/minutos/urls.py:
--------------------------------------------------------------------------------
1 | """minutos URL Configuration
2 |
3 | The `urlpatterns` list routes URLs to views. For more information please see:
4 | https://docs.djangoproject.com/en/3.1/topics/http/urls/
5 | Examples:
6 | Function views
7 | 1. Add an import: from my_app import views
8 | 2. Add a URL to urlpatterns: path('', views.home, name='home')
9 | Class-based views
10 | 1. Add an import: from other_app.views import Home
11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
12 | Including another URLconf
13 | 1. Import the include() function: from django.urls import include, path
14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
15 | """
16 | from django.conf import settings
17 | from django.conf.urls.static import static
18 | from django.contrib import admin
19 | from django.contrib.auth import views as auth_views
20 | from django.urls import path, include
21 |
22 | from apps.core.views import frontpage, privacy, terms, plans, signup
23 |
24 | urlpatterns = [
25 | path('', frontpage, name='frontpage'),
26 | path('privacy/', privacy, name='privacy'),
27 | path('terms/', terms, name='terms'),
28 | path('plans/', plans, name='plans'),
29 | path('admin/', admin.site.urls),
30 |
31 |
32 | #
33 | # Dashboard
34 |
35 | path('dashboard/', include('apps.dashboard.urls')),
36 |
37 | #
38 | # Auth
39 |
40 | path('signup/', signup, name='signup'),
41 | path('login/', auth_views.LoginView.as_view(template_name='core/login.html'), name='login'),
42 | path('logout/', auth_views.LogoutView.as_view(), name='logout'),
43 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
44 |
--------------------------------------------------------------------------------
/minutos/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for minutos 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.1/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', 'minutos.settings')
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/scripts/check_subscriptions.py:
--------------------------------------------------------------------------------
1 | # Import Python packages
2 |
3 | import django
4 | import os
5 | import sys
6 | import stripe
7 |
8 | # Init Django
9 |
10 | sys.path.append(os.path.join(os.path.abspath(os.path.dirname(__file__)), '..'))
11 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "minutos.settingsprod")
12 | django.setup()
13 |
14 | from django.conf import settings
15 |
16 | # Import models
17 |
18 | from apps.team.models import Team, Plan
19 |
20 | # Init Stripe
21 |
22 | stripe.api_key = settings.STRIPE_SECRET_KEY
23 |
24 | # Loop through teams
25 |
26 | for team in Team.objects.all():
27 | sub = stripe.Subscription.retrieve(team.stripe_subscription_id)
28 |
29 | if sub.status == 'canceled':
30 | plan_default = Plan.objects.get(is_default=True)
31 |
32 | team.plan = plan_default
33 | team.plan_status = Team.PLAN_CANCELED
34 | team.save()
--------------------------------------------------------------------------------
/serverfiles/gunicorn_start:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | NAME='minutos'
4 | DJANGODIR=/webapps/minutos/minutos_env/minutos
5 | SOCKFILE=/webapps/minutos/minutos_env/run/gunicorn.sock
6 | USER=minutos
7 | GROUP=webapps
8 | NUM_WORKERS=3
9 | DJANGO_SETTINGS_MODULE=minutos.settingsprod
10 | DJANGO_WSGI_MODULE=minutos.wsgi
11 | TIMEOUT=120
12 |
13 | cd $DJANGODIR
14 | source ../bin/activate
15 | export DJANGO_SETTINGS_MODULE=$DJANGO_SETTINGS_MODULE
16 | export PYTHONPATH=$DJANGODIR:$PYTHONPATH
17 |
18 | RUNDIR=$(dirname $SOCKFILE)
19 | test -d $RUNDIR || mkdir -p $RUNDIR
20 |
21 | exec ../bin/gunicorn ${DJANGO_WSGI_MODULE}:application \
22 | --name $NAME \
23 | --workers $NUM_WORKERS \
24 | --timeout $TIMEOUT \
25 | --user=$USER --group=$GROUP \
26 | --bind=unix:$SOCKFILE \
27 | --log-level=debug \
28 | --log-file=-
29 |
--------------------------------------------------------------------------------
/serverfiles/nginx_minutos.conf:
--------------------------------------------------------------------------------
1 | upstream minutos_app_server {
2 | server unix:/webapps/minutos/minutos_env/run/gunicorn.sock fail_timeout=0;
3 | }
4 |
5 | server {
6 | listen 80;
7 | server_name minutos.codewithstein.com;
8 | return 301 https://minutos.codewithstein.com$request_uri;
9 | }
10 |
11 |
12 | server {
13 | listen 443 ssl;
14 | server_name minutos.codewithstein.com;
15 |
16 | client_max_body_size 4G;
17 |
18 | access_log /webapps/minutos/minutos_env/logs/nginx-access.log;
19 | error_log /webapps/minutos/minutos_env/logs/nginx-error.log;
20 |
21 | ssl_certificate /etc/letsencrypt/live/minutos.codewithstein.com/fullchain.pem;
22 | ssl_certificate_key /etc/letsencrypt/live/minutos.codewithstein.com/privkey.pem;
23 |
24 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
25 | ssl_prefer_server_ciphers on;
26 | ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH';
27 |
28 | location /static/ {
29 | alias /webapps/minutos/minutos_env/minutos/static/;
30 | }
31 |
32 | location /media/ {
33 | alias /webapps/minutos/minutos_env/minutos/media/;
34 | }
35 |
36 | location / {
37 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
38 |
39 | proxy_set_header Host $http_host;
40 |
41 | proxy_redirect off;
42 |
43 | if (!-f $request_filename) {
44 | proxy_pass http://minutos_app_server;
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/serverfiles/supervisor_minutos.conf:
--------------------------------------------------------------------------------
1 | [program:minutos]
2 | command = /webapps/minutos/minutos_env/bin/gunicorn_start
3 | user = minutos
4 | stdout_logfile = /webapps/minutos/minutos_env/logs/supervisor.log
5 | redirect_stderr = true
6 | environment=LANG=en_US.UTF-8,LC_ALL=en_US.UTF-8
7 |
--------------------------------------------------------------------------------
/static/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SteinOveHelset/minutos/73cdcb44409f97b05680c6a048f80eb4bd3f1f46/static/.DS_Store
--------------------------------------------------------------------------------
/static/images/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SteinOveHelset/minutos/73cdcb44409f97b05680c6a048f80eb4bd3f1f46/static/images/.DS_Store
--------------------------------------------------------------------------------
/static/images/avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SteinOveHelset/minutos/73cdcb44409f97b05680c6a048f80eb4bd3f1f46/static/images/avatar.png
--------------------------------------------------------------------------------
/static/images/features-happyboss.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SteinOveHelset/minutos/73cdcb44409f97b05680c6a048f80eb4bd3f1f46/static/images/features-happyboss.jpg
--------------------------------------------------------------------------------
/static/images/features-team.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SteinOveHelset/minutos/73cdcb44409f97b05680c6a048f80eb4bd3f1f46/static/images/features-team.jpg
--------------------------------------------------------------------------------
/static/images/features-tracktime.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SteinOveHelset/minutos/73cdcb44409f97b05680c6a048f80eb4bd3f1f46/static/images/features-tracktime.jpg
--------------------------------------------------------------------------------