├── .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 | 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 |
59 |
60 |

Let's get started

61 | 62 | Get a free account 63 |
64 |
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 |
16 |
17 |
18 | {% csrf_token %} 19 | 20 | {% if form.errors %} 21 |
22 | {{ form.non_field_errors }} 23 | 24 | {% for field in form %} 25 | {% if field.errors %}{{ field.label }}: {{ field.errors|striptags }}{% endif %} 26 | {% endfor %} 27 |
28 | {% endif %} 29 | 30 |
31 | 32 | 33 |
34 | 35 |
36 |
37 | 38 |
39 | 40 | 41 |
42 | 43 |
44 |
45 | 46 |
47 |
48 | 49 |
50 |
51 |
52 | 53 |
54 | 55 |

Don't have an account?

56 | 57 | Create one for free 58 |
59 |
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 |
20 |
21 |

Free

22 |

$0

23 |
24 |
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 |
45 |
46 |

Basic

47 |

$5

48 |
49 |
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 |
70 |
71 |

Pro

72 |

$10

73 |
74 |
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 |
95 |
96 |

Need more?

97 |

$?

98 |
99 |
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 |
16 |
17 |
18 | {% csrf_token %} 19 | 20 | {% if form.errors %} 21 |
22 | {{ form.non_field_errors }} 23 | 24 | {% for field in form %} 25 | {% if field.errors %}{{ field.label }}: {{ field.errors|striptags }}{% endif %} 26 | {% endfor %} 27 |
28 | {% endif %} 29 | 30 |
31 |

32 | [[ error ]] 33 |

34 |
35 | 36 |
37 | 38 | 39 |
40 | 41 |
42 |
43 | 44 |
45 | 46 | 47 |
48 | 49 |
50 |
51 | 52 |
53 | 54 | 55 |
56 | 57 |
58 |
59 | 60 |
    61 |
  • Your password must contain at least 8 characters
  • 62 |
  • Your password can't be entirely numeric.
  • 63 |
64 | 65 |
66 |
67 | 68 |
69 |
70 |
71 | 72 |
73 | 74 |

Already have an account?

75 | 76 | Click here to log in 77 |
78 |
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 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {% for entry in date_entries %} 25 | 26 | 27 | 28 | 29 | 30 | {% endfor %} 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
TaskProjectTime
{{ entry.task.title }}{{ entry.project.title }}{{ entry.minutes|format_minutes }}
Total{{ time_for_user_and_date|format_minutes }}
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 | 60 | 61 | 62 | 63 | 64 | 65 | {% for project in all_projects %} 66 | {% if project.time_for_user_and_project_and_month > 0 %} 67 | 68 | 69 | 70 | 71 | {% endif %} 72 | {% endfor %} 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 |
ProjectTime
{{ project.title }}{{ project.time_for_user_and_project_and_month|format_minutes }}
Total{{ time_for_user_and_month|format_minutes }}
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 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | {% for entry in untracked_entries %} 110 | 111 | 112 | 119 | 120 | 121 | 122 | 123 | {% endfor %} 124 | 125 |
StartedTimeStatusAdd to taskDiscard
{{ entry.created_at }} 113 | {% if entry.minutes == 0 %} 114 | {{ entry.minutes_since|format_minutes }} 115 | {% else %} 116 | {{ entry.minutes|format_minutes }} 117 | {% endif %} 118 | {% if entry.minutes == 0 %}In progress{% else %}Untracked{% endif %}Add to taskDelete
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 | 139 | 140 | 141 | 142 | 143 | 144 | {% for member in members %} 145 | 146 | 147 | 148 | 149 | {% endfor %} 150 | 151 |
NameTime
{% firstof member.get_full_name member.username %}{{ member.time_for_user_and_team_and_month|format_minutes }}
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 | 14 | 15 |
16 |
17 |
18 |
19 |
20 | 21 |
22 |
23 | 24 |
25 |

{% firstof user.get_full_name user.username %}

26 |

{{ user.email }}

27 |
28 |
29 |
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 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | {% for entry in date_entries %} 47 | 48 | 49 | 50 | 51 | 52 | {% endfor %} 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |
TaskProjectTime
{{ entry.task.title }}{{ entry.project.title }}{{ entry.minutes|format_minutes }}
Total{{ time_for_user_and_date|format_minutes }}
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 | 82 | 83 | 84 | 85 | 86 | 87 | {% for project in all_projects %} 88 | {% if project.time_for_user_and_project_and_month > 0 %} 89 | 90 | 91 | 92 | 93 | {% endif %} 94 | {% endfor %} 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 |
ProjectTime
{{ project.title }}{{ project.time_for_user_and_project_and_month|format_minutes }}
Total{{ time_for_user_and_month|format_minutes }}
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 | 15 | 16 |
17 |
18 |

Edit entry

19 |
20 |
21 | 22 |
23 |
24 |
25 | {% csrf_token %} 26 | 27 |
28 |
29 |
30 | 36 |
37 |
38 | 39 |
40 |
41 | 47 |
48 |
49 |
50 | 51 |
52 | 53 | 54 |
55 | 56 |
57 |
58 | 59 |
60 |
61 | 62 |
63 |
64 |
65 |
66 |
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 | 15 | 16 |
17 |
18 |

Edit project

19 |
20 |
21 | 22 |
23 |
24 |
25 | {% csrf_token %} 26 | 27 |
28 | 29 | 30 |
31 | 32 |
33 |
34 | 35 |
36 |
37 | 38 |
39 |
40 |
41 |
42 |
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 | 15 | 16 |
17 |
18 |

Edit task

19 |
20 |
21 | 22 |
23 |
24 |
25 | {% csrf_token %} 26 | 27 |
28 | 29 | 30 |
31 | 32 |
33 |
34 | 35 |
36 | 37 | 38 |
39 |
40 | 44 |
45 |
46 |
47 | 48 |
49 |
50 | 51 |
52 |
53 |
54 |
55 |
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 | 14 | 15 |
16 |
17 |

{{ project.title }}

18 | 19 |
20 | {% if team.plan.max_tasks_per_project > project.tasks.count %} 21 | Add task 22 | {% else %} 23 |
24 | This team has reached the limit of tasks for this project.
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 | Edit project 35 |
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 | 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 | 15 | 16 |
17 |
18 |

{{ task.title }} ({{ task.get_status_display }})

19 | 20 | Edit task 21 | 22 |

Register time

23 | 24 |
25 | {% csrf_token %} 26 | 27 |
28 |
29 |
30 | 36 |
37 |
38 | 39 |
40 |
41 | 47 |
48 |
49 |
50 | 51 |
52 | 53 | 54 |
55 | 56 |
57 |
58 | 59 |
60 |
61 | 62 |
63 |
64 |
65 |
66 |
67 | 68 |
69 |
70 |
71 | 72 |

Logged entries

73 | 74 | {% if task.entries.all %} 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | {% for entry in task.entries.all %} 87 | 88 | 89 | 90 | 91 | 95 | 96 | {% endfor %} 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 |
DateUserTime
{{ entry.created_at }}{% firstof entry.created_by.get_full_name entry.created_by.username %}{{ entry.minutes }} 92 | Edit 93 | Delete 94 |
Total{{ task.registered_time }}
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 | 13 | 14 |
15 |
16 |

Track entry

17 | 18 |
19 | {% csrf_token %} 20 | 21 |
22 | 23 | 24 |
25 |
26 | 32 |
33 |
34 |
35 | 36 |
37 | 38 | 39 |
40 |
41 | 44 |
45 |
46 |
47 | 48 | 49 | 50 |
51 |
52 |
53 | 59 |
60 |
61 | 62 |
63 |
64 | 70 |
71 |
72 |
73 | 74 |
75 |
81 | 82 |
83 |
84 | 85 |
86 |
87 |
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 | 13 | 14 |
15 |
16 |

Add team

17 | 18 |
19 | {% csrf_token %} 20 | 21 |
22 | 23 | 24 |
25 | 26 |
27 |
28 | 29 |
30 |

[[ error ]]

31 |
32 | 33 |
34 |
35 | 36 |
37 |
38 |
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 | 13 | 14 |
15 |
16 |

Edit team

17 | 18 |
19 | {% csrf_token %} 20 | 21 |
22 | 23 | 24 |
25 | 26 |
27 |
28 | 29 |
30 |

[[ error ]]

31 |
32 | 33 |
34 |
35 | 36 |
37 |
38 |
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 | 31 | 32 | 33 | 34 | 37 | 38 |

Minutos

35 |

{{ invitation.email }} accepted your invitation.

36 |
39 | 40 | -------------------------------------------------------------------------------- /apps/team/templates/team/email_invitation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Minutos 7 | 8 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 38 | 39 | 40 | 41 | 44 | 45 |

Minutos - Invitation to {{ team.title }}

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 |
42 | Accept invitation 43 |
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 | 14 | 15 |
16 |
17 |

Invite member

18 | 19 |
20 |

[[ error ]]

21 |
22 | 23 | {% if team.plan.max_members_per_team > team.members.count %} 24 |
25 | {% csrf_token %} 26 | 27 |
28 | 29 | 30 |
31 | 32 |
33 |
34 | 35 |
36 |
37 | 38 |
39 |
40 |
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 | 14 | 15 |
16 |
17 |
18 |
19 |
20 |
21 |

Free

22 |

$0

23 |
24 |
25 | 26 |
27 |
    28 |
  • 3 projects
  • 29 |
  • 1 member
  • 30 |
  • 10 tasks per project
  • 31 |
32 |
33 |
34 |
35 |
36 | 37 |
38 |
39 |
40 |
41 |
42 |

Basic

43 |

$5

44 |
45 |
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 |
70 |
71 |

Pro

72 |

$10

73 |
74 |
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 |
99 |
100 |

Need more?

101 |

$?

102 |
103 |
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 | 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 | 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 | 13 | 14 |
15 |
16 |

Accept invitation

17 | 18 |
19 | {% csrf_token %} 20 | 21 |
22 | 23 | 24 |
25 | 26 |
27 |
28 | 29 |
30 |
31 | 32 |
33 |
34 |
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 | 13 | 14 |
15 |
16 |

Edit profile

17 | 18 |
19 | {% csrf_token %} 20 | 21 |
22 | 23 | 24 |
25 | 26 |
27 |
28 | 29 |
30 | 31 | 32 |
33 | 34 |
35 |
36 | 37 |
38 | 39 | 40 |
41 | 42 |
43 |
44 | 45 |
46 | 47 | 48 |
49 | 50 |
51 | 52 |
53 | 54 |
55 |
56 | 57 |
58 |
59 | 60 |
61 |
62 |
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 | 12 | 13 |
14 |
15 |
16 |
17 |
18 | 19 |
20 |
21 | 22 |
23 |

{% firstof user.get_full_name user.username %}

24 |

{{ user.email }}

25 |
26 |
27 | 28 | 29 | 30 | Edit profile 31 | 32 | 33 | Log out 34 | 35 |
36 |
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 |
51 |
52 |

Teams

53 | 54 | Add team 55 |
56 |
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 --------------------------------------------------------------------------------