├── .gitignore
├── LICENSE
├── README.md
├── accounts
    ├── __init__.py
    ├── admin.py
    ├── apps.py
    ├── forms.py
    ├── models.py
    ├── pipelines.py
    ├── templates
    │   ├── profile.html
    │   ├── register.html
    │   └── registration
    │   │   ├── logged_out.html
    │   │   └── login.html
    ├── tests.py
    ├── urls.py
    └── views.py
├── manage.py
├── requirements.txt
├── tracker
    ├── admin.py
    ├── apps.py
    ├── filters.py
    ├── forms.py
    ├── migrations
    │   └── __init__.py
    ├── models.py
    ├── static
    │   ├── imgs
    │   │   ├── icon_google.png
    │   │   └── landing-bg.jpg
    │   ├── main.css
    │   └── main.js
    ├── tables.py
    ├── templates
    │   ├── analytics
    │   │   └── index.html
    │   ├── landing
    │   │   └── index.html
    │   └── tracker
    │   │   ├── base.html
    │   │   ├── dashboard.html
    │   │   ├── expense_form.html
    │   │   ├── form_template.html
    │   │   └── index.html
    ├── templatetags
    │   ├── __init__.py
    │   └── month_labels.py
    ├── tests.py
    ├── urls.py
    ├── utils.py
    └── views.py
└── website
    ├── __init__.py
    ├── settings.py
    ├── urls.py
    └── wsgi.py
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__
2 | migrations
3 | db.sqlite3
4 | 
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
 1 | MIT License
 2 | 
 3 | Copyright (c) 2018 
 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 | # Squirrel
  2 | [](https://app.fossa.io/projects/git%2Bgithub.com%2Fwingkwong%2Fsquirrel?ref=badge_shield)
  3 | 
  4 | ## About
  5 | A responsive web application for expense tracking and analytics platform powered by Django
  6 | 
  7 | ## Installed apps:
  8 | * django 2.0.8
  9 | * django.contrib.admin
 10 | * django.contrib.auth
 11 | * django.contrib.contenttypes
 12 | * django.contrib.sessions
 13 | * django.contrib.messages
 14 | * django.contrib.staticfiles
 15 | * tracker.apps.TrackerConfig
 16 | * django_tables2
 17 | * django_filters
 18 | * crispy_forms
 19 | * tracker.templatetags.month_labels
 20 | * social_django
 21 | 
 22 | ## Configured URLs:
 23 | 
 24 | * ``/``
 25 | * ``/tracker``
 26 | * ``/tracker/add``
 27 | * ``/tracker/update/{id}``
 28 | * ``/analytics``
 29 | * ``/analytics/{year}``
 30 | * ``/analytics/{year}/{month}``
 31 | * ``/analytics/{year}/{month}/{day}``
 32 | * ``/accounts/register``
 33 | * ``/accounts/login``
 34 | * ``/accounts/logout``
 35 | * ``/accounts/profile/{id}``
 36 | * ``/dashboard``
 37 | * ``/auth/``
 38 | * ``/admin/``
 39 | 
 40 | ## Templates:
 41 | 
 42 | Landing:
 43 | * ``landing/index.html``
 44 | 
 45 | Tracker:
 46 | * ``tracker/expense_form.html``
 47 | * ``tracker/from_template.html``
 48 | * ``tracker/header.html``
 49 | * ``tracker/index.html``
 50 | * ``analytics/index.html``
 51 | 
 52 | Accounts:
 53 | * ``registration/login.html``
 54 | * ``registration/logged_out.html``
 55 | 
 56 | ## Features:
 57 | 
 58 | * Responsive
 59 | * Expense Overview with filtering
 60 | * Expense Addition
 61 | * Expense Analytics by year, month or day
 62 | * Google OAuth2
 63 | 
 64 | ## Prerequisites
 65 | 
 66 | - Python >= 3.5
 67 | - pip3
 68 | 
 69 | ## Setup Your Environment
 70 | 1. Fork this project
 71 | 2. Install from the given requirements file.
 72 | ```bash
 73 | pip3 install -r requirements.txt
 74 | ```
 75 | 3. Make Migrations
 76 | ```bash
 77 | python3 manage.py makemigrations
 78 | ```
 79 | 4. Migrate
 80 | ```bash
 81 | python3 manage.py migrate
 82 | ```
 83 | 5. Run
 84 | ```bash
 85 | python3 manage.py runserver
 86 | ```
 87 | 
 88 | ## Setup Your Google OAuth
 89 | 1. Go to Google Developers Console(https://console.developers.google.com/apis/library?project=_) and create a new project
 90 | 2. Go to credentials tab
 91 | 3. Create Credentials and choose OAuth Client ID
 92 | 4. Select Web Application and enter any name in 'Product name shown to users' under OAuth Consent Screen tab
 93 | 5. Set `http://127.0.0.1:8000/auth/complete/google-oauth2/` in Authorized redirect URIs
 94 | 6. Under APIs and services tab, click on Google+ API and then click Enable
 95 | 7. Copy the Client ID and Client Secret Under settings.py
 96 | ```bash
 97 | SOCIAL_AUTH_GOOGLE_OAUTH2_KEY='' # ClientKey
 98 | SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET=''# SecretKey
 99 | ```
100 | 
101 | ## Authors
102 | 
103 | * **Wing Kam WONG** -  [@wingkwong](https://github.com/wingkwong)
104 | 
105 | ## Contributor
106 | 
107 | * **Tushar Kapoor** -  [@TusharKapoor23](https://github.com/TusharKapoor23)
108 | 
109 | ## License
110 | 
111 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details
112 | 
113 | [](https://app.fossa.io/projects/git%2Bgithub.com%2Fwingkwong%2Fsquirrel?ref=badge_large)
114 | 
--------------------------------------------------------------------------------
/accounts/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wingkwong/squirrel/0841d68f744b22ed2ba85917257429359a498f9a/accounts/__init__.py
--------------------------------------------------------------------------------
/accounts/admin.py:
--------------------------------------------------------------------------------
 1 | from django.contrib import admin
 2 | from django.contrib.auth.admin import UserAdmin
 3 | from django.contrib.auth.models import User
 4 | from .models import Profile
 5 | 
 6 | 
 7 | class ProfileInline(admin.StackedInline):
 8 |     model = Profile
 9 |     can_delete = False
10 |     verbose_name_plural = 'Profile'
11 |     fk_name = 'user'
12 | 
13 | 
14 | class CustomUserAdmin(UserAdmin):
15 |     inlines = (ProfileInline, )
16 | 
17 |     def get_inline_instances(self, request, obj=None):
18 |         if not obj:
19 |             return list()
20 |         return super(CustomUserAdmin, self).get_inline_instances(request, obj)
21 | 
22 | 
23 | admin.site.unregister(User)
24 | admin.site.register(User, CustomUserAdmin)
--------------------------------------------------------------------------------
/accounts/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 | 
3 | 
4 | class AccountsConfig(AppConfig):
5 |     name = 'accounts'
6 | 
7 | 
8 | 
--------------------------------------------------------------------------------
/accounts/forms.py:
--------------------------------------------------------------------------------
 1 | from django import forms
 2 | from django.contrib.auth.forms import UserCreationForm
 3 | from django.contrib.auth.models import User
 4 | 
 5 | 
 6 | class RegisterForm(UserCreationForm):
 7 |     dob = forms.DateField(help_text='Required. Format: YYYY-MM-DD')
 8 | 
 9 |     class Meta:
10 |         model = User
11 |         fields = ('username', 'dob', 'email', 'password1', 'password2' )
--------------------------------------------------------------------------------
/accounts/models.py:
--------------------------------------------------------------------------------
 1 | from django.db import models
 2 | from django.contrib.auth.models import User
 3 | from django.db.models.signals import post_save
 4 | from django.dispatch import receiver
 5 | 
 6 | 
 7 | class Profile(models.Model):
 8 |     user = models.OneToOneField(User, on_delete=models.CASCADE)
 9 |     avatar = models.ImageField(upload_to='profiles')
10 |     dob = models.DateField(null=True, blank=True)
11 | 
12 | 
13 | @receiver(post_save, sender=User)
14 | def update_user_profile(sender, instance, created, **kwargs):
15 |     if created:
16 |         Profile.objects.create(user=instance)
17 |     instance.profile.save()
18 | 
19 | 
20 | @receiver(post_save, sender=User)
21 | def save_user_profile(sender, instance, **kwargs):
22 |     instance.profile.save()
--------------------------------------------------------------------------------
/accounts/pipelines.py:
--------------------------------------------------------------------------------
 1 | from django.contrib.auth.models import User
 2 | 
 3 | 
 4 | # Setting avatar after OAuth Login
 5 | def saveAvatar(backend, strategy, details, response,
 6 |         user=None, *args, **kwargs):
 7 |     url = None
 8 |     if backend.name == 'google-oauth2':
 9 |         url = response['image'].get('url')
10 |         ext = url.split('.')[-1]
11 |     if url:
12 |         try:
13 |             user = User.objects.get(username=details['username'])
14 |             user.profile.avatar = url
15 |             user.save()
16 |         except User.DoesNotExist:
17 |             user = None
18 | 
--------------------------------------------------------------------------------
/accounts/templates/profile.html:
--------------------------------------------------------------------------------
 1 | 
 2 | {% include 'tracker/base.html' %}
 3 | 
 4 | {% block body %}
 5 | 
 6 | 
 7 | 
 8 |     
 9 | 		
10 | 		  
11 | 			
12 | 			  {% if profile.avatar != ''%}
13 | 				
14 | 			{% else %}
15 | 			
16 | 				
17 | 			{% endif%}
18 | 			  
{{profile.user}} 
19 | 			
20 | 			
26 | 		  
27 | 		
28 | 		
29 | 		
34 | 	
35 | 
 
36 | 
37 | 
38 | 
39 | 
40 | 
41 | 
42 | {%endblock%}
--------------------------------------------------------------------------------
/accounts/templates/register.html:
--------------------------------------------------------------------------------
 1 | {% include 'tracker/base.html' %}
 2 | 
 3 | {% block body %}
 4 |   Register 
 5 |   
10 | {% endblock %}
--------------------------------------------------------------------------------
/accounts/templates/registration/logged_out.html:
--------------------------------------------------------------------------------
1 | 
2 | {% block content %}
3 | Logged out!
  
4 | 
5 | Click here to login again. 
6 | {% endblock %}
--------------------------------------------------------------------------------
/accounts/templates/registration/login.html:
--------------------------------------------------------------------------------
 1 | 
 2 | {% include 'tracker/base.html' %}
 3 | 
 4 | 
 5 | {% block body %}
 6 |     
 7 |         {% if form.errors %}
 8 |             
Your username and password didn't match. Please try again.
 9 |             {% endif %}
10 | 
11 |             {% if next %}
12 |                 {% if user.is_authenticated %}
13 |                 
Your account doesn't have access to this page. To proceed,
14 |                 please login with an account that has access.
15 |                 {% else %}
16 |                 
Please login to see this page.
17 |                 {% endif %}
18 |             {% endif %}
19 |             
35 | 
36 |             
37 |             
-- OR -- 
38 |             
39 |                 
40 |                     
41 |                       Login with Google
42 |                      
43 |                  
44 |             
45 | 
46 |             
Forget Password? 
47 |             
Don't have an account? Register here. 
48 |     
 
49 | {% endblock %}
--------------------------------------------------------------------------------
/accounts/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 | 
3 | # Create your tests here.
4 | 
--------------------------------------------------------------------------------
/accounts/urls.py:
--------------------------------------------------------------------------------
 1 | from django.urls import path
 2 | from django.contrib.auth.decorators import login_required
 3 | from accounts import views
 4 | 
 5 | 
 6 | urlpatterns = [
 7 |     path('register/', views.register, name='register'),
 8 |     path('profile/', login_required(views.ProfileView.profile), name='profile')
 9 | ]
10 | 
--------------------------------------------------------------------------------
/accounts/views.py:
--------------------------------------------------------------------------------
 1 | from django.contrib.auth import login, authenticate
 2 | from django.shortcuts import render, redirect
 3 | from django.contrib.auth.models import User
 4 | from .forms import RegisterForm
 5 | 
 6 | 
 7 | def register(request):
 8 |     if request.method == 'POST':
 9 |         form = RegisterForm(request.POST)
10 |         if form.is_valid():
11 |             user = form.save()
12 |             user.refresh_from_db()
13 |             user.profile.dob = form.cleaned_data.get('dob')
14 |             user.save()
15 |             raw_password = form.cleaned_data.get('password1')
16 |             user = authenticate(username=user.username, password=raw_password)
17 |             login(request, user)
18 |             return redirect('tracker:expense')
19 |     else:
20 |         form = RegisterForm()
21 |     return render(request, 'register.html', {'form': form})
22 | 
23 | 
24 | class ProfileView():
25 |     def profile(request):
26 |         # Getting current user
27 |         user = User.objects.get(username=request.user)
28 |         # Passing User Profile to Profile Page
29 |         context = {
30 |             'profile': user.profile
31 |         }
32 |         return render(request, 'profile.html', context)
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
 1 | #!/usr/bin/env python
 2 | import os
 3 | import sys
 4 | 
 5 | if __name__ == "__main__":
 6 |     os.environ.setdefault("DJANGO_SETTINGS_MODULE", "website.settings")
 7 |     try:
 8 |         from django.core.management import execute_from_command_line
 9 |     except ImportError as exc:
10 |         raise ImportError(
11 |             "Couldn't import Django. Are you sure it's installed and "
12 |             "available on your PYTHONPATH environment variable? Did you "
13 |             "forget to activate a virtual environment?"
14 |         ) from exc
15 |     execute_from_command_line(sys.argv)
16 | 
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | django_tables2==1.19.0
2 | django_filter==1.1.0
3 | Django==2.0.8
4 | django_crispy_forms==1.7.2
5 | django_filters==0.2.1
6 | social-auth-app-django
7 | Pillow==5.2.0
8 | 
--------------------------------------------------------------------------------
/tracker/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from .models import Expense
3 | 
4 | admin.site.register(Expense)
5 | 
--------------------------------------------------------------------------------
/tracker/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 | 
3 | 
4 | class TrackerConfig(AppConfig):
5 |     name = 'tracker'
6 | 
--------------------------------------------------------------------------------
/tracker/filters.py:
--------------------------------------------------------------------------------
 1 | import django_filters
 2 | from .models import Expense
 3 | 
 4 | 
 5 | class ExpenseFilter(django_filters.FilterSet):
 6 |     description = django_filters.CharFilter(lookup_expr='icontains')
 7 |     type = django_filters.CharFilter(lookup_expr='icontains')
 8 |     payment = django_filters.CharFilter(lookup_expr='icontains')
 9 |     amount = django_filters.CharFilter(lookup_expr='icontains')
10 | 
11 |     class Meta:
12 |         model = Expense
13 |         fields = (
14 |             'date',
15 |             'description',
16 |             'type',
17 |             'payment',
18 |             'amount'
19 |         )
--------------------------------------------------------------------------------
/tracker/forms.py:
--------------------------------------------------------------------------------
 1 | from django import forms
 2 | from crispy_forms.helper import FormHelper
 3 | from crispy_forms.layout import Layout, Field, ButtonHolder, Submit
 4 | from django.forms import ModelForm
 5 | from .models import Expense
 6 | 
 7 | class ExpenseTableHelper(FormHelper):
 8 |     date = forms.DateField(
 9 |         widget=forms.TextInput(
10 |             attrs={'type': 'date'}
11 |         )
12 |     )
13 |     form_method = 'GET'
14 |     layout = Layout(
15 |         Field('date', placeholder='YYYY-MM-DD'),
16 |         Field('description'),
17 |         Field('type'),
18 |         Field('payment'),
19 |         Field('amount'),
20 |         Submit('submit', 'Filter'),
21 |     )
22 | 
23 | 
24 | class DateInput(forms.DateInput):
25 |     input_type = 'date'
26 | 
27 | 
28 | class AddExpenseForm(ModelForm):
29 |     class Meta:
30 |         model = Expense
31 |         fields = [
32 |             'date',
33 |             'description',
34 |             'type',
35 |             'payment',
36 |             'amount'
37 |         ]
38 |         widgets = {
39 |             'date': DateInput(),
40 |         }
--------------------------------------------------------------------------------
/tracker/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wingkwong/squirrel/0841d68f744b22ed2ba85917257429359a498f9a/tracker/migrations/__init__.py
--------------------------------------------------------------------------------
/tracker/models.py:
--------------------------------------------------------------------------------
 1 | from django.db import models
 2 | from django.urls import reverse
 3 | 
 4 | 
 5 | class Expense(models.Model):
 6 |     date = models.DateField()
 7 |     description = models.CharField(max_length=1000, null=True)
 8 |     type = models.CharField(max_length=30)
 9 |     payment = models.CharField(max_length=30)
10 |     amount = models.FloatField()
11 |     created_by = models.CharField(max_length=100)
12 |     created_at = models.DateField()
13 | 
14 |     class Meta:
15 |         verbose_name = 'Expense'
16 |         verbose_name_plural = 'Expenses'
17 |         ordering = ['-id']
18 | 
19 |     def get_absolute_url(self):
20 |         return reverse('tracker:expense')
21 | 
--------------------------------------------------------------------------------
/tracker/static/imgs/icon_google.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wingkwong/squirrel/0841d68f744b22ed2ba85917257429359a498f9a/tracker/static/imgs/icon_google.png
--------------------------------------------------------------------------------
/tracker/static/imgs/landing-bg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wingkwong/squirrel/0841d68f744b22ed2ba85917257429359a498f9a/tracker/static/imgs/landing-bg.jpg
--------------------------------------------------------------------------------
/tracker/static/main.css:
--------------------------------------------------------------------------------
  1 | /*
  2 |     Override
  3 | */
  4 | 
  5 | .card-panel,
  6 | .collection a.collection-item,
  7 | .navbar {
  8 |     background-color: #1a1a1f;
  9 |     color: #ffffff;
 10 | }
 11 | 
 12 | nav a,
 13 | nav ul a,
 14 | tr a,
 15 | .login a,
 16 | .pagination li a {
 17 |   color: #ffffff;
 18 | }
 19 | 
 20 | nav ul a,
 21 | .input-field>label,
 22 | label{
 23 |     font-size: 18px;
 24 | }
 25 | 
 26 | td, th {
 27 |   padding: 0px;
 28 | }
 29 | 
 30 | .container {
 31 |     margin-top: 20px;
 32 | }
 33 | 
 34 | .btn, .btn-large, .btn-small,
 35 |  .btn:hover, .btn-large:hover, .btn-small:hover,
 36 |   .btn:focus, .btn-large:focus, .btn-small:focus{
 37 |     background-color: #ffffff;
 38 |     color: #1a1a1f;
 39 | }
 40 | 
 41 | input,
 42 |  input:focus{
 43 |     color: #ffffff;
 44 |     border-bottom: 1px solid #ffffff !important;
 45 | }
 46 | .card {
 47 |     background: #1a1a1f;
 48 |     color: #ffffff;
 49 | }
 50 | 
 51 | .collection .collection-item,
 52 | .collection .collection-item.active {
 53 |     background-color: #1a1a1f;
 54 |     color: #ffffff;
 55 | }
 56 | 
 57 | .collection a.collection-item:not(.active):hover,
 58 | .collection a.collection-item {
 59 |     background-color: #323237;
 60 |     color: #ffffff;
 61 | }
 62 | 
 63 | span.badge {
 64 |     color: #ffffff;
 65 | }
 66 | 
 67 | /* Navbar */
 68 |  .navbar-brand{
 69 |      padding: 30px 30px 30px 15px;
 70 |      font-size: 25px;
 71 | }
 72 | /* Landing */
 73 | /*-------------------------------------------------------------- #General --------------------------------------------------------------*/
 74 |  body{
 75 |     background-color: #323237;
 76 |     color: #ffffff;
 77 |      font-size: 18px;
 78 |      font-family: 'Josefin Sans', Helvetica, Arial, sans-serif;
 79 |      overflow-x:hidden;
 80 | }
 81 |  a{
 82 |      color:#ffffff;
 83 | }
 84 | /* a:hover,a:active,a:focus{
 85 |      color:#1a1a1f;
 86 |      outline:none;
 87 |      text-decoration:none;
 88 | }
 89 |  */
 90 |  p{
 91 |      padding:0;
 92 |      margin:30px 0px;
 93 | }
 94 |  h1,h2,h3,h4,h5,h6{
 95 |      font-family:"Montserrat",sans-serif;
 96 |      font-weight:400;
 97 |      padding:0;
 98 | }
 99 | 
100 | .text-center{
101 |     text-align: center;
102 | }
103 | 
104 | .features-row{
105 |     padding: 20px 0px;
106 | }
107 | 
108 | /*-------------------------------------------------------------- #IntroSection --------------------------------------------------------------*/
109 |  #intro{
110 |      width:100%;
111 |      height:100%;
112 |      background-color:#1a1a1f;
113 |      position:relative;
114 |      margin-top:-20px;
115 | }
116 |  @media(min-width:1024px){
117 |      #intro{
118 |          background-attachment:fixed;
119 |     }
120 | }
121 |  #intro .intro-text{
122 |      position:absolute;
123 |      left:0;
124 |      top:100px;
125 |      right:0;
126 |      height:calc(80% -20px);
127 |      display:flex;
128 |      align-items:center;
129 |      justify-content:center;
130 |      text-align:center;
131 |      flex-direction:column;
132 | }
133 | 
134 |  #intro p{
135 |      margin-bottom:20px;
136 |      padding:15px;
137 |      font-size:24px;
138 | }
139 |  @media(max-width:768px){
140 |      #intro p{
141 |          font-size:18px;
142 |          line-height:24px;
143 |          margin-bottom:20px;
144 |     }
145 | }
146 | 
147 | /*
148 |     Login
149 | */
150 | 
151 | .login{
152 |     margin-top: -20px;
153 |     padding: 40px;
154 |     text-align: center;
155 | }
156 | 
157 | .loginBtn {
158 |   box-sizing: border-box;
159 |   position: relative;
160 |   margin: 0.2em;
161 |   padding: 0 15px 0 46px;
162 |   border: none;
163 |   text-align: left;
164 |   line-height: 34px;
165 |   white-space: nowrap;
166 |   border-radius: 0.2em;
167 |   font-size: 16px;
168 |   color: #1a1a1f;
169 | }
170 | .loginBtn:before {
171 |   content: "";
172 |   box-sizing: border-box;
173 |   position: absolute;
174 |   top: 0;
175 |   left: 0;
176 |   width: 34px;
177 |   height: 100%;
178 | }
179 | .loginBtn:focus {
180 |   outline: none;
181 | }
182 | .loginBtn:active {
183 |   box-shadow: inset 0 0 0 32px rgba(0,0,0,0.1);
184 | }
185 | 
186 | 
187 | /* Google */
188 | .loginBtn--google {
189 |   background: #DD4B39;
190 | }
191 | .loginBtn--google:before {
192 |   border-right: #BB3F30 1px solid;
193 |   background: url('imgs/icon_google.png') 6px 6px no-repeat;
194 | }
195 | .loginBtn--google:hover,
196 | .loginBtn--google:focus {
197 |   background: #E74B37;
198 | }
199 | 
200 | /* Filter Panel --------------------------------*/
201 | .filterPanel {
202 |     text-align: center;
203 |     height: 100%;
204 |     width: 0;
205 |     position: fixed;
206 |     z-index: 1;
207 |     top: 0;
208 |     left: 0;
209 |     background-color: #1a1a1f;
210 |     color: #000;
211 |     overflow-x: hidden;
212 |     padding-top: 100px;
213 |     transition: 0.5s;
214 | }
215 | 
216 | .filterPanel .control-group{
217 |     margin: 20px;
218 | }
219 | 
220 | .filterPanel a {
221 |     padding: 8px 8px 8px 32px;
222 |     text-decoration: none;
223 |     font-size: 25px;
224 |     display: block;
225 |     transition: 0.3s;
226 | }
227 | 
228 | .filterPanel .controls input{
229 |     color: #000000;
230 |     text-align: center;
231 | }
232 | 
233 | .filterPanel a:hover {
234 |     color: #f1f1f1;
235 | }
236 | 
237 | .filterPanel .closebtn {
238 |     position: absolute;
239 |     top: 0;
240 |     right: 25px;
241 |     font-size: 36px;
242 |     margin-left: 50px;
243 |     color: #1a1a1ffff;
244 | }
245 | 
246 | .expense-overview tr {
247 |   background-color: #1a1a1f;
248 |   border-color: #ffffff;
249 | }
250 | 
251 | .expense-overview td,
252 | .expense-overview th{
253 |   padding: 5px;
254 |   border-radius: 10px;
255 |   text-align: center;
256 | }
257 | 
258 | .add-expense-form {
259 |   border-radius: 15px;
260 |   padding: 48px 24px 24px 24px;
261 | }
262 | 
263 | .add-expense-form .row {
264 |   margin-bottom: 12px;
265 | }
266 | 
267 | .add-expense-form .row .col{
268 |   margin: 0px;
269 |   padding: 0px 10px;
270 | }
271 | 
--------------------------------------------------------------------------------
/tracker/static/main.js:
--------------------------------------------------------------------------------
 1 | var ExpenseTracker = {};
 2 | 
 3 | $(function() {
 4 |     if ($('.expense-overview').length) ExpenseTracker.Filter.init();
 5 | });
 6 | 
 7 | ExpenseTracker.Filter = {
 8 |     init: function() {
 9 |         var self = this;
10 |         $('.open-filter-btn').click(function(e) {
11 |             e.preventDefault();
12 |             self.openFilterPanel();
13 |         });
14 | 
15 |         $('.closebtn').click(function(e) {
16 |             e.preventDefault();
17 |             self.closeFilterPanel();
18 |         });
19 |     },
20 |     openFilterPanel: function() {
21 |         document.getElementById("filterPanel").style.width = "100%";
22 |     },
23 |     closeFilterPanel: function() {
24 |         document.getElementById("filterPanel").style.width = "0";
25 |     }
26 | }
--------------------------------------------------------------------------------
/tracker/tables.py:
--------------------------------------------------------------------------------
 1 | import django_tables2 as tables
 2 | from django.utils.safestring import mark_safe
 3 | from django.utils.html import escape
 4 | from .models import Expense
 5 | 
 6 | 
 7 | class ActionButtons(tables.Column):
 8 |     empty_values = list()
 9 |     def render(self, value, record):
10 |         html = "  " % escape(record.id)
11 |         return mark_safe(html)
12 | 
13 | 
14 | class ExpenseTable(tables.Table):
15 |     actions = ActionButtons()
16 |     class Meta:
17 |         model = Expense
18 |         fields = (
19 |             'date',
20 |             'description',
21 |             'type',
22 |             'payment',
23 |             'amount'
24 |             )
25 |         attrs = {"class": "table table-striped dt-responsive nowrap"}
26 |         empty_text = "No records found"
27 |         # template_name = 'django_tables2/bootstrap-responsive.html'
28 | 
--------------------------------------------------------------------------------
/tracker/templates/analytics/index.html:
--------------------------------------------------------------------------------
  1 | {% extends 'tracker/base.html'%}
  2 | 
  3 | {% load month_labels %}
  4 | {% block body %}
  5 | 
  6 |     
  7 |         {% if menu_labels %}
  8 |         
 22 |         {% endif %}
 23 | 
 24 |         {% if context_type == 'annually' or context_type == 'monthly' or context_type == 'daily' %}
 25 |             
 26 | 
 27 |                 {% if submenu %}
 28 |                 
 38 |                 {% endif %}
 39 |                 
 40 |                     {% if datasets %}
 41 |                          
 42 |                         {% load django_tables2 %}
 43 |                         {%if expense_table_in_month_view%}
 44 |                             {% render_table expense_table_in_month_view%}
 45 |                         {%endif%}
 46 |                         {%if expense_table_in_daily_view %}
 47 |                             {% render_table expense_table_in_daily_view%}
 48 |                         {%endif%}
 49 |                         
 94 |                     {% else %}
 95 |                         No data
 96 |                     {% endif %}
 97 |                 
 98 |             
 99 |             {% endif %}
100 |     
 
101 | 
102 | 
103 | 
104 |     {% endblock %}
105 | 
106 | 
107 | 
--------------------------------------------------------------------------------
/tracker/templates/landing/index.html:
--------------------------------------------------------------------------------
 1 | {% extends 'tracker/base.html'%}
 2 | 
 3 | {% block body %}
 4 |   
 5 |     
 6 |       
Squirrel 
 7 |       
An Open Source Expense Tracker and Analytics Platform
 8 |     
 
 9 |    
10 | {% endblock %}
--------------------------------------------------------------------------------
/tracker/templates/tracker/base.html:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 | 
 4 |      
 5 |     Squirrel 
 6 |     {% load staticfiles %}
 7 | 
 8 |     
 9 |     
10 |     
11 |     
12 | 
13 |      
14 |      
16 | 
17 |     
18 |      
19 | 
20 |     
21 |     
22 | 
23 | 
24 | 
25 |      
26 |      
27 |      
28 | 
29 | 
30 | 
31 | 
64 | 
65 |     {% block body %}
66 |     {% endblock %}
67 | 
68 | 
69 | 
70 | 
--------------------------------------------------------------------------------
/tracker/templates/tracker/dashboard.html:
--------------------------------------------------------------------------------
  1 | {% extends 'tracker/base.html'%}
  2 | 
  3 | {% block body %}
  4 | 
  5 |     {% if context_type == 'dashboard'%}
  6 |     
  7 |         
  8 |             
  9 |                 
 10 |                     
 11 |                         
 12 |                             Total Records
 13 |                         
{{total_records}} 
 14 |                         
 15 |                             {% if total_expenses_diff > 0 %}
 16 |                             keyboard_arrow_up  {{total_records_diff | stringformat:"+d" | slice:"1:"}}%
 17 |                             {% endif %}
 18 |                             {% if total_expenses_diff < 0 %}
 19 |                             keyboard_arrow_down  {{total_records_diff | stringformat:"+d" | slice:"1:"}}%
 20 |                             {% endif %}
 21 |                             from last month 
 22 |                         
 23 |                     
 24 |                     
 25 |                         
This month:
 26 |                         
{{totalRecordsInThisMonth}}
 27 |                         
Last month:
 28 |                         
{{totalRecordsInLastMonth}}
 29 |                     
 30 |                 
 31 |             
 32 | 
 33 |             
 34 |                 
 35 |                     
 36 |                         
 37 |                             Total Expenses
 38 |                         
${{total_expenses}} 
 39 |                         
 40 |                             {% if total_expenses_diff > 0 %}
 41 |                             keyboard_arrow_up  {{total_expenses_diff | stringformat:"+d" | slice:"1:"}}%
 42 |                             {% endif %}
 43 |                             {% if total_expenses_diff < 0 %}
 44 |                             keyboard_arrow_down  {{total_expenses_diff | stringformat:"+d" | slice:"1:"}}%
 45 |                             {% endif %}
 46 |                             from last month 
 47 |                         
 48 |                     
 49 |                     
 50 |                         
This month:
 51 |                         
${{totalExpensesInThisMonth}}
 52 |                         
Last month:
 53 |                         
${{totalExpensesInLastMonth}}
 54 |                     
 55 |                 
 56 |             
 57 | 
 58 |             
 59 |                 
 60 |                     
 61 |                         
 62 |                             Total Categories
 63 |                         
{{categories_cnt}} 
 64 |                         
 65 |                             {% if total_expenses_diff > 0 %}
 66 |                             keyboard_arrow_up  {{total_categories_diff | stringformat:"+d" | slice:"1:"}}%
 67 |                             {% endif %}
 68 |                             {% if total_expenses_diff < 0 %}
 69 |                             keyboard_arrow_down  {{total_categories_diff | stringformat:"+d" | slice:"1:"}}%
 70 |                             {% endif %}
 71 |                              from last month 
 72 |                         
 73 |                     
 74 |                     
 75 |                         
This month:
 76 |                         
{{categories_in_this_month}}
 77 |                         
Last month:
 78 |                         
{{categories_in_last_month}}
 79 |                     
 80 |                 
 81 |             
 82 | 
 83 |             
 84 |                 
 85 |                     
 86 |                         
 87 |                             Avg. year
 88 |                         
${{avg_year}} 
 89 |                         
 90 |                             dashboard 
 91 |                               
 92 |                         
 93 |                     
 94 |                     
 95 |                         
Avg. month:
 96 |                         
${{avg_month}}
 97 |                         
Avg. day:
 98 |                         
${{avg_day}}
 99 |                     
100 |                 
101 |             
102 |         
 
103 |      
104 | 
105 |     {% if categories %}
106 |     
107 |         
108 |             
109 |                 
110 |                     
111 |                         
112 |                             
113 |                                  
114 |                                 
144 |                             
145 | 
146 |                         
147 |                     
148 |                 
149 |             
150 | 
151 | 
152 |             
153 |                 
154 |                     
155 |                         
156 |                             
Expenses per Categories
157 |                             
158 |                             
217 |                         
218 |                     
219 |                 
220 |             
221 |         
 
222 |      
223 |     {% endif %}
224 | 
225 | 
226 |     {% if latest30DaysData %}
227 |     
228 |         
229 |             
230 |                 
231 |                     
232 |                                          
233 |                 
279 |                     
280 |                 
281 |             
282 |         
 
283 |      
284 |     {% endif %}
285 | 
286 |     
323 |     {% endif %}
324 | 
 
325 | 
326 | {% endblock %}
327 | 
--------------------------------------------------------------------------------
/tracker/templates/tracker/expense_form.html:
--------------------------------------------------------------------------------
 1 | {% extends 'tracker/base.html'%}
 2 | 
 3 | {% block body %}
 4 | 
 5 |  
22 | {% endblock %}
23 | 
--------------------------------------------------------------------------------
/tracker/templates/tracker/form_template.html:
--------------------------------------------------------------------------------
 1 | {% for field in form %}
 2 | 
 3 |     
 4 |          {{ field.errors }}
 5 |       
 6 |     
 7 |      {{ field }}
 8 |       {{ field.label_tag }} 
 9 |     
10 |   
11 | 
12 | {% endfor %}
--------------------------------------------------------------------------------
/tracker/templates/tracker/index.html:
--------------------------------------------------------------------------------
 1 | {% extends 'tracker/base.html'%}
 2 | {% load django_tables2 crispy_forms_tags %}
 3 | 
 4 | {% block body %}
 5 | 
 6 |     
 7 |          
 8 |             
 9 |               Filter
10 |              
11 |          
12 |         
13 |             
16 |         
17 | 
18 |     
19 | 
20 |     
21 |        
22 |            {% render_table table %}
23 |        
24 |     
25 | 
26 | 
27 |     
33 | 
34 | {% endblock %}
35 | 
--------------------------------------------------------------------------------
/tracker/templatetags/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wingkwong/squirrel/0841d68f744b22ed2ba85917257429359a498f9a/tracker/templatetags/__init__.py
--------------------------------------------------------------------------------
/tracker/templatetags/month_labels.py:
--------------------------------------------------------------------------------
1 | from django import template
2 | 
3 | register = template.Library()
4 | 
5 | @register.filter
6 | def get_by_index(lst, idx):
7 |     return lst[idx-1]
--------------------------------------------------------------------------------
/tracker/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 | 
3 | # Create your tests here.
4 | 
--------------------------------------------------------------------------------
/tracker/urls.py:
--------------------------------------------------------------------------------
 1 | from django.urls import path
 2 | from django.contrib.auth.decorators import login_required
 3 | from . import views
 4 | 
 5 | app_name = 'tracker'
 6 | 
 7 | urlpatterns = [
 8 | 	# /
 9 |     path('', views.LandingView.landing, name ="landing"),
10 | 
11 |     # /dashboard
12 |     path('dashboard', login_required(views.Dashboard.index), name="dashboard"),
13 | 
14 |     # /tracker
15 |     path('tracker', login_required(views.IndexView.as_view()), name ="expense"),
16 | 
17 |     # /add
18 |     path('tracker/add', login_required(views.ExpenseCreate.as_view()), name="create_expense"),
19 | 
20 |     # /update/{id}
21 |     path('tracker/update/
', login_required(views.ExpenseUpdate.as_view()), name="update_expense"),
22 | 
23 |     # /delete/{id}
24 |     path('tracker/delete/', login_required(views.ExpenseDelete.as_view()), name="delete_expense"),
25 | 
26 |     # /export
27 |     path('tracker/export', login_required(views.ReportView.export), name="export"),
28 | 
29 |     # /analytics
30 |     # FIXME: redirect to dashboard
31 |     path('analytics', login_required(views.Dashboard.index), name="analytics"),
32 | 
33 |     # /analytics/2018
34 |     path('analytics/', login_required(views.AnalyticsView.annually), name="annually"),
35 | 
36 |     # /analytics/2018/02
37 |     path('analytics//', login_required(views.AnalyticsView.monthly), name="monthly"),
38 | 
39 |     # /analytics/2018/02/20
40 |     path('analytics///', login_required(views.AnalyticsView.daily), name="daily"),
41 | ]
42 | 
--------------------------------------------------------------------------------
/tracker/utils.py:
--------------------------------------------------------------------------------
 1 | from django_tables2 import SingleTableView
 2 | from django_tables2.config import RequestConfig
 3 | import math
 4 | 
 5 | class PagedFilteredTableView(SingleTableView):
 6 |     filter_class = None
 7 |     formhelper_class = None
 8 |     context_filter_name = 'filter'
 9 | 
10 |     def get_queryset(self, **kwargs):
11 |         qs = super(PagedFilteredTableView, self).get_queryset()
12 |         self.filter = self.filter_class(self.request.GET, queryset=qs)
13 |         self.filter.form.helper = self.formhelper_class()
14 |         return self.filter.qs
15 | 
16 |     def get_table(self, **kwargs):
17 |         table = super(PagedFilteredTableView, self).get_table()
18 |         RequestConfig(self.request, paginate={'page': self.kwargs['page'],
19 |                             "per_page": self.paginate_by}).configure(table)
20 |         return table
21 | 
22 |     def get_context_data(self, **kwargs):
23 |         context = super(PagedFilteredTableView, self).get_context_data()
24 |         context[self.context_filter_name] = self.filter
25 |         return context
26 | 
27 | class AmountUnitUtil():
28 |     millnames = ['','K','M','B','T']
29 |     
30 |     def convertToMills(n):
31 |         n = float(n)
32 |         index = max(0, min(len(AmountUnitUtil.millnames) - 1, int(math.floor(0 if n == 0 else math.log10(abs(n))/3))))
33 |         return '{:.2f}{}'.format(n / 10**(3 * index), AmountUnitUtil.millnames[index])
--------------------------------------------------------------------------------
/tracker/views.py:
--------------------------------------------------------------------------------
  1 | from django.views import generic
  2 | from django.views.generic.edit import CreateView, UpdateView, DeleteView
  3 | from django.urls import reverse_lazy
  4 | from django_tables2 import RequestConfig
  5 | from django.http import HttpResponseRedirect
  6 | from django.shortcuts import render
  7 | from django.db.models import Sum, Count
  8 | from .tables import ExpenseTable
  9 | from .models import Expense
 10 | from .forms import ExpenseTableHelper
 11 | from .filters import ExpenseFilter
 12 | from .utils import AmountUnitUtil
 13 | from django.http import HttpResponse
 14 | from calendar import monthrange
 15 | from .forms import AddExpenseForm
 16 | import csv
 17 | import random
 18 | import datetime
 19 | 
 20 | 
 21 | class Dashboard():
 22 |     template_name = "tracker/dashboard.html"
 23 | 
 24 |     def index(request):
 25 |         # init
 26 |         avg_year = 0
 27 |         avg_month = 0
 28 |         avg_day = 0
 29 | 
 30 |         now = datetime.datetime.now()
 31 | 
 32 |         # get user expense objects
 33 |         exp = Expense.objects.filter(created_by=request.user)
 34 | 
 35 |         # count total record
 36 |         total_records = exp.count()
 37 | 
 38 |         # count total record in this month
 39 |         current_month = now.month
 40 |         total_records_in_this_month = exp.filter(created_at__month=current_month).count()
 41 |         if total_records_in_this_month is None:
 42 |             total_records_in_this_month = 0
 43 | 
 44 |         # count total record in last month
 45 |         last_month = now.month - 1
 46 |         if last_month<0:
 47 |             last_month = 11
 48 |         total_records_in_last_month = exp.filter(created_at__month=last_month).count()
 49 |         if total_records_in_last_month is None:
 50 |             total_records_in_last_month = 0
 51 | 
 52 |         print(total_records_in_this_month)
 53 |         print(total_records_in_last_month)
 54 |         # diff between this month and last month
 55 |         if total_records == 0:
 56 |             total_records_diff = 0
 57 |         else:
 58 |             total_records_diff = ( (total_records_in_this_month - total_records_in_last_month) / total_records ) * 100
 59 | 
 60 |         # sum up all the expenses
 61 |         total_expenses = exp.aggregate(amount=Sum('amount'))['amount']
 62 |         if total_expenses is None:
 63 |             total_expenses = 0
 64 | 
 65 |         # sum up all the expenses in this month
 66 |         total_expenses_in_this_month = exp.filter(created_at__month=current_month).aggregate(amount=Sum('amount'))['amount']
 67 |         if total_expenses_in_this_month is None:
 68 |             total_expenses_in_this_month = 0
 69 | 
 70 |         # sum up all the expenses in last month
 71 |         total_expenses_in_last_month = exp.filter(created_at__month=last_month).aggregate(amount=Sum('amount'))['amount']
 72 |         if total_expenses_in_last_month is None:
 73 |             total_expenses_in_last_month = 0
 74 | 
 75 |         # diff between this month and last month
 76 |         if total_expenses == 0:
 77 |             total_expenses_diff = 0
 78 |         else:
 79 |             total_expenses_diff = ( (total_expenses_in_this_month - total_expenses_in_last_month) / total_expenses ) * 100
 80 | 
 81 |         # get all categories
 82 |         categories = list(exp.values('type').distinct().order_by('type').values_list('type', flat=True))
 83 | 
 84 |         # get categories record count
 85 |         categories_record_cnt = list(exp.values('type').annotate(the_count=Count('type')).order_by('type').values_list('the_count', flat=True))
 86 | 
 87 |         # categories count
 88 |         categories_cnt = exp.values('type').annotate(the_count=Count('type')).count()
 89 |         if categories_cnt is None:
 90 |             categories_cnt = 0
 91 | 
 92 |         # total expense per category
 93 |         categories_expense = list(exp.values('type').order_by('type').annotate(the_count=Sum('amount')).values_list('the_count', flat=True))
 94 | 
 95 |         categories_color_arr = []
 96 |         for cat in categories:
 97 |             # generate random color
 98 |             r = lambda: random.randint(0, 255)
 99 |             color = '#%02X%02X%02X' % (r(), r(), r())
100 |             categories_color_arr.append(color)
101 | 
102 |         # categories count in this month
103 |         categories_in_this_month = exp.filter(created_at__month=current_month).values('type').annotate(the_count=Count('type')).count()
104 |         if categories_in_this_month is None:
105 |             categories_in_this_month = 0
106 | 
107 |         # categories count in last month
108 |         categories_in_last_month = exp.filter(created_at__month=last_month).values('type').annotate(the_count=Count('type')).count()
109 |         if categories_in_last_month is None:
110 |             categories_in_last_month = 0
111 | 
112 |         # diff between this month and last month
113 |         if categories_cnt == 0:
114 |             total_categories_diff = 0
115 |         else:
116 |             total_categories_diff = ((categories_in_this_month - categories_in_last_month) / categories_cnt) * 100
117 | 
118 |         # list out dates for the following processing.
119 |         dates = list(exp.values('date')
120 |                      .values_list('date', flat=True))
121 | 
122 |         # avg amount per year, per month and per day
123 |         year_arr = []
124 |         month_arr = []
125 |         day_arr = []
126 | 
127 |         for date in dates:
128 |             if date.year not in year_arr:
129 |                 year_arr.append(date.year)
130 |             if date.year not in month_arr:
131 |                 month_arr.append(date.month)
132 |             if date.year not in day_arr:
133 |                 day_arr.append(date.day)
134 | 
135 |         if total_expenses > 0:
136 |             avg_year = AmountUnitUtil.convertToMills(total_expenses / year_arr.__len__())
137 |             avg_month = AmountUnitUtil.convertToMills(total_expenses / month_arr.__len__())
138 |             avg_day = AmountUnitUtil.convertToMills(total_expenses / day_arr.__len__())
139 |             total_expenses = AmountUnitUtil.convertToMills(total_expenses)
140 | 
141 |         # Get Latest 30 days records
142 | 
143 |         latest30DaysArr = list(exp.filter(created_at__lte=datetime.datetime.today(),
144 |                                        created_at__gt=datetime.datetime.today() - datetime.timedelta(days=30))
145 |                             .values('date').distinct().order_by('date').values_list('date', flat=True))
146 | 
147 |         latest30DaysLabel = []
148 |         for day in latest30DaysArr:
149 |             latest30DaysLabel.append(day.strftime('%d/%m'))
150 | 
151 | 
152 |         latest30DaysData = list(exp.filter(created_at__lte=datetime.datetime.today(), created_at__gt=datetime.datetime.today()-datetime.timedelta(days=30))
153 |                              .values('date').distinct().order_by('date')
154 |                              .annotate(amount=Sum('amount')).values_list('amount', flat=True))
155 | 
156 |         context = {
157 |             'context_type': 'dashboard',
158 |             'total_records': total_records,
159 |             'total_expenses': total_expenses,
160 |             'categories': categories,
161 |             'categories_record_cnt': categories_record_cnt,
162 |             'categories_cnt': categories_cnt,
163 |             'categories_expense': categories_expense,
164 |             'categories_color_arr': categories_color_arr,
165 |             'avg_year': avg_year,
166 |             'avg_month': avg_month,
167 |             'avg_day':avg_day,
168 |             'current_year': now.year,
169 |             'current_month': now.month,
170 |             'current_day': now.day,
171 |             'latest30DaysLabel': latest30DaysLabel,
172 |             'latest30DaysData': latest30DaysData,
173 |             'totalRecordsInThisMonth': total_records_in_this_month,
174 |             'totalRecordsInLastMonth': total_records_in_last_month,
175 |             'totalExpensesInThisMonth': float("{0:.2f}".format(total_expenses_in_this_month)),
176 |             'totalExpensesInLastMonth': float("{0:.2f}".format(total_expenses_in_last_month)),
177 |             'categories_in_this_month': categories_in_this_month,
178 |             'categories_in_last_month': categories_in_last_month,
179 |             'total_expenses_diff': float("{0:.2f}".format(total_expenses_diff)),
180 |             'total_records_diff': float("{0:.2f}".format(total_records_diff)),
181 |             'total_categories_diff': float("{0:.2f}".format(total_categories_diff))
182 | 
183 |         }
184 | 
185 |         return render(request, "tracker/dashboard.html", context)
186 | 
187 | 
188 | class IndexView(generic.ListView):
189 |     template_name = "tracker/index.html"
190 |     context_object_name = "records"
191 |     model = Expense
192 |     table_class = ExpenseTable
193 |     filter_class = ExpenseFilter
194 |     formhelper_class = ExpenseTableHelper
195 | 
196 |     def get_queryset(self):
197 |         return Expense.objects.filter(created_by=self.request.user)
198 | 
199 |     def get_context_data(self, **kwargs):
200 |         context = super(IndexView, self).get_context_data(**kwargs)
201 |         filter = ExpenseFilter(self.request.GET, queryset=self.get_queryset(**kwargs))
202 |         filter.form.helper = ExpenseTableHelper()
203 |         table = ExpenseTable(filter.qs)
204 |         table.order_by = '-date'
205 |         RequestConfig(self.request, paginate={'per_page': 10}).configure(table)
206 |         context['filter'] = filter
207 |         context['table'] = table
208 |         return context
209 | 
210 | 
211 | class ExpenseCreate(CreateView):
212 |     model = Expense
213 |     form_class = AddExpenseForm
214 | 
215 |     def form_valid(self, form):
216 |         obj = form.save(commit=False)
217 |         obj.created_by = self.request.user
218 |         obj.created_at = datetime.datetime.now()
219 |         obj.save()
220 |         return HttpResponseRedirect(reverse_lazy('tracker:expense'))
221 | 
222 | 
223 | class ExpenseUpdate(UpdateView):
224 |     model = Expense
225 |     fields = [
226 |         'date',
227 |         'description',
228 |         'type',
229 |         'payment',
230 |         'amount'
231 |     ]
232 | 
233 | 
234 | # obsolete
235 | class ExpenseDelete(DeleteView):
236 |     model = Expense
237 |     success_url = reverse_lazy('tracker:expense')
238 | 
239 | 
240 | class AnalyticsView(generic.ListView):
241 |     template_name = "analytics/index.html"
242 |     context_object_name = "records"
243 |     model = Expense
244 | 
245 |     def annually(request, year):
246 |         # get user expense objects
247 |         exp = Expense.objects.filter(created_by=request.user)
248 | 
249 |         # retrieve distinct types
250 |         expense_type = list(exp.filter(date__year=year).values('type').distinct().order_by()
251 |                                  .values_list('type', flat=True))
252 | 
253 |         datasets = []
254 |         months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October",
255 |                   "November", "December"]
256 | 
257 |         for type in expense_type:
258 |             # expense
259 |             arr = list(exp.filter(date__year=year).filter(type=type).distinct().order_by('date'))
260 | 
261 |             # expense month by type
262 |             month_tmp_arr = list(exp.filter(date__year=year).filter(type=type).values('date').distinct()
263 |                             .order_by('date')
264 |                             .values_list('date', flat=True))
265 | 
266 |             # expense amount by type
267 |             expense_tmp_arr = list(exp.filter(date__year=year).filter(type=type)
268 |                 .values('date').distinct().order_by('date')
269 |                 .annotate(amount=Sum('amount')).values_list('amount', flat=True))
270 | 
271 |             # init array with size 12 with value 0
272 |             monthly_expense_cnt = [0] * 12
273 | 
274 |             for i, m in enumerate(arr):
275 |                 monthly_expense_cnt[m.date.month-1] += m.amount
276 | 
277 |             total_amount = []
278 | 
279 |             for j, k in enumerate(monthly_expense_cnt):
280 |                 o = {}
281 |                 o['x'] = months[j];
282 |                 o['y'] = monthly_expense_cnt[j];
283 |                 total_amount.append(o)
284 | 
285 |             # generate random color
286 |             r = lambda: random.randint(0, 255)
287 |             color = '#%02X%02X%02X' % (r(), r(), r())
288 | 
289 |             # construct dataset
290 |             dataset = {
291 |                 'label': type,
292 |                 'backgroundColor': color,
293 |                 'borderColor': color,
294 |                 'data': total_amount,
295 |                 'fill': 'false'
296 |             }
297 | 
298 |             datasets.append(dataset)
299 | 
300 |         # fetch available years for menu labels
301 |         dates = list(exp.values('date')
302 |              .values_list('date', flat=True))
303 |         year_arr = []
304 |         for date in dates:
305 |             if date.year not in year_arr:
306 |                 year_arr.append(date.year)
307 |         year_arr.sort()
308 | 
309 |         # construct submenu
310 |         expense_per_month = []
311 |         month_not_none = []
312 |         for i, month in enumerate(months):
313 |             amount = exp.filter(date__year=year).filter(date__month=(i+1)).values('date').distinct().order_by('date').aggregate(amount=Sum('amount'))['amount']
314 |             if amount is not None:
315 |                 expense_per_month.append(float("{0:.2f}".format(amount)))
316 |                 month_not_none.append((months[i]))
317 | 
318 | 
319 |         if expense_per_month:
320 |             submenu = zip(month_not_none, expense_per_month)
321 |         else:
322 |             submenu = False
323 | 
324 |         context = {
325 |             'context_type': 'annually',
326 |             'datasets': datasets,
327 |             'labels': months,
328 |             'title': 'Annual Report in ' + str(year),
329 |             'report_type': 'line',
330 |             'menu_labels': year_arr,
331 |             'x_axis_label': 'Month',
332 |             'submenu': submenu
333 |         }
334 | 
335 |         return render(request, "analytics/index.html", context)
336 | 
337 |     def monthly(request, year, month):
338 |         # get user expense objects
339 |         exp = Expense.objects.filter(created_by=request.user)
340 | 
341 |         expense_table_in_month_view = ExpenseTable(exp.filter(date__year=year).filter(date__month=month))
342 |         expense_table_in_month_view.paginate(page=request.GET.get('page', 1), per_page=10)
343 | 
344 |         months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October",
345 |                   "November", "December"]
346 |         months_reverse = dict(January=1, February=2, March=3, April=4)
347 |         datasets = []
348 | 
349 |         # retrieve distinct types
350 |         expense_type = list(exp.filter(date__year=year).filter(date__month=month).values('type').distinct().order_by()
351 |                             .values_list('type', flat=True))
352 | 
353 |         lastday = monthrange(year, month)[1]
354 | 
355 |         days = list(range(1,lastday+1))
356 | 
357 |         for type in expense_type:
358 |             # expense
359 |             expense_queryset = exp.filter(date__year=year).filter(date__month=month).filter(type=type).distinct().order_by('date')
360 |             arr = list(expense_queryset)
361 | 
362 |             # expense month by type
363 |             month_tmp_arr = list(exp.filter(date__year=year).filter(date__month=month).filter(type=type).values('date').distinct()
364 |                                  .order_by('date')
365 |                                  .values_list('date', flat=True))
366 | 
367 |             # expense amount by type
368 |             expense_tmp_arr = list(exp.filter(date__year=year).filter(date__month=month).filter(type=type)
369 |                                    .values('date').distinct().order_by('date')
370 |                                    .annotate(amount=Sum('amount')).values_list('amount', flat=True))
371 | 
372 |             # init array
373 |             daily_expense_cnt = [0] * lastday
374 |             for i, m in enumerate(arr):
375 |                 daily_expense_cnt[m.date.day - 1] += m.amount
376 | 
377 |             total_amount = []
378 |             for j, k in enumerate(daily_expense_cnt[1:]):
379 |                 o = {}
380 |                 o['x'] = days[j];
381 |                 o['y'] = daily_expense_cnt[j];
382 |                 total_amount.append(o)
383 | 
384 |             # generate random color
385 |             r = lambda: random.randint(0, 255)
386 |             color = '#%02X%02X%02X' % (r(), r(), r())
387 | 
388 |             # construct dataset
389 |             dataset = {
390 |                 'label': type,
391 |                 'backgroundColor': color,
392 |                 'borderColor': color,
393 |                 'data': total_amount,
394 |                 'fill': 'false'
395 |             }
396 | 
397 |             datasets.append(dataset)
398 | 
399 |         # fetch available months for menu labels
400 |         dates = list(exp.values('date')
401 |                      .values_list('date', flat=True))
402 |         month_arr = []
403 |         month_labels = []
404 |         for date in dates:
405 |             if date.month not in month_arr:
406 |                 month_arr.append(date.month)
407 |                 month_arr.sort()
408 |         
409 |         for single_month in month_arr:
410 |             month_labels.append(months[single_month - 1])
411 |         # construct submenu
412 |         expense_per_day = []
413 |         day_not_none  = []
414 |         for i, d in enumerate(days):
415 |             amount = exp.filter(date__year=year).filter(date__month=month).filter(date__day=(i+1)).values('date').distinct().order_by(
416 |                 'date').aggregate(amount=Sum('amount'))['amount']
417 |             if amount is not None:
418 |                 expense_per_day.append(float("{0:.2f}".format(amount)))
419 |                 day_not_none.append((i+1))
420 | 
421 |         if expense_per_day:
422 |             submenu = zip(day_not_none, expense_per_day)
423 |         else:
424 |             submenu = False
425 | 
426 |         context = {
427 |             'context_type': 'monthly',
428 |             'datasets': datasets,
429 |             'monthly_expense': expense_type,
430 |             'labels': days,
431 |             'title': 'Monthly Report on ' + str(months[month-1]) + ' ' + str(year),
432 |             'report_type': 'bar',
433 |             'selected_year':year ,
434 |             'menu_labels': month_arr,
435 |             'month_labels': month_labels,
436 |             'x_axis_label': 'Day',
437 |             'submenu': submenu,
438 |             'months': months,
439 |             'expense_table_in_month_view': expense_table_in_month_view
440 |         }
441 |         return render(request, "analytics/index.html", context)
442 | 
443 |     def daily(request, year, month, day):
444 |         # get user expense objects
445 |         exp = Expense.objects.filter(created_by=request.user)
446 | 
447 |         # expense table
448 |         expense_table_in_daily_view = ExpenseTable(exp.filter(date__year=year).filter(date__month=month).filter(date__day=day))
449 |         expense_table_in_daily_view.paginate(page=request.GET.get('page', 1), per_page=10)
450 | 
451 |         type =[]
452 |         # retrieve distinct types
453 |         expense_type = list(
454 |             exp.filter(date__year=year).filter(date__month=month)
455 |             .filter(date__day=day).values('type').distinct().order_by()
456 |             .values_list('type', flat=True))
457 | 
458 |         datasets = []
459 |         expense_arr = []
460 |         color_arr = []
461 |         for type in expense_type:
462 |             # expense amount by type
463 |             expense_tmp_arr = list(exp.filter(date__year=year).filter(date__month=month).filter(date__day=day)
464 |                                    .filter(type=type)
465 |                                    .values('date').distinct().order_by('date')
466 |                                    .annotate(amount=Sum('amount')).values_list('amount', flat=True))
467 |             # get the sum
468 |             expense_arr.append(expense_tmp_arr[0])
469 | 
470 |             # generate random color
471 |             r = lambda: random.randint(0, 255)
472 |             color = '#%02X%02X%02X' % (r(), r(), r())
473 |             color_arr.append(color)
474 | 
475 |         # construct dataset
476 |         if color_arr and expense_arr:
477 |             dataset = {
478 |                 'label': type,
479 |                 'backgroundColor': color_arr,
480 |                 'borderColor': color_arr,
481 |                 'data': expense_arr
482 |             }
483 |             datasets.append(dataset)
484 | 
485 |         # construct submenu
486 |         expense_per_type = []
487 |         for i, type in enumerate(expense_type):
488 |             amount = exp.filter(date__year=year).filter(date__month=month).filter(date__day=day).filter(type=type).values('date').distinct().order_by('date').aggregate(amount=Sum('amount'))['amount']
489 |             if amount is not None:
490 |                 expense_per_type.append(float("{0:.2f}".format(amount)))
491 | 
492 |         if expense_per_type:
493 |             submenu = zip(expense_type, expense_per_type)
494 |         else:
495 |             submenu = False
496 | 
497 |         context = {
498 |             'context_type': 'daily',
499 |             'datasets': datasets,
500 |             'daily_expense': expense_type,
501 |             'labels': expense_type,
502 |             'title': 'Daily Report on ' + str(day) + '/' + str(month) + '/' + str(year) ,
503 |             'x_axis_label': 'Type',
504 |             'report_type': 'pie',
505 |             'submenu': submenu,
506 |             'expense_table_in_daily_view': expense_table_in_daily_view
507 |         }
508 |         return render(request, "analytics/index.html", context)
509 | 
510 | 
511 | class LandingView():
512 |     def landing(request):
513 |         context = {}
514 |         return render(request, "landing/index.html", context)
515 | 
516 | 
517 | class ReportView():
518 |     def export(request):
519 |         exp = Expense.objects.filter(created_by=request.user).order_by('id')
520 |         opts = exp.model._meta
521 |         response = HttpResponse(content_type='text/csv')
522 |         response['Content-Disposition'] = 'attachment;filename=export.csv'
523 |         writer = csv.writer(response)
524 |         field_names = [field.name for field in opts.fields]
525 |         # writer.writerow(field_names)
526 |         for obj in exp:
527 |             writer.writerow([getattr(obj, field) for field in field_names])
528 |         return response
529 | 
530 | 
--------------------------------------------------------------------------------
/website/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wingkwong/squirrel/0841d68f744b22ed2ba85917257429359a498f9a/website/__init__.py
--------------------------------------------------------------------------------
/website/settings.py:
--------------------------------------------------------------------------------
  1 | """
  2 | Django settings for website project.
  3 | 
  4 | Generated by 'django-admin startproject' using Django 2.0.2.
  5 | 
  6 | For more information on this file, see
  7 | https://docs.djangoproject.com/en/2.0/topics/settings/
  8 | 
  9 | For the full list of settings and their values, see
 10 | https://docs.djangoproject.com/en/2.0/ref/settings/
 11 | """
 12 | 
 13 | import os
 14 | 
 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 17 | 
 18 | 
 19 | # Quick-start development settings - unsuitable for production
 20 | # See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/
 21 | 
 22 | # SECURITY WARNING: keep the secret key used in production secret!
 23 | SECRET_KEY = '(pwnf+znz^!914bc%wg8)wj+c!_7y8-71ya65k(79gpiw8ye+2'
 24 | 
 25 | # SECURITY WARNING: don't run with debug turned on in production!
 26 | DEBUG = True
 27 | 
 28 | ALLOWED_HOSTS = []
 29 | 
 30 | 
 31 | # Application definition
 32 | 
 33 | INSTALLED_APPS = [
 34 |     'django.contrib.admin',
 35 |     'django.contrib.auth',
 36 |     'django.contrib.contenttypes',
 37 |     'django.contrib.sessions',
 38 |     'django.contrib.messages',
 39 |     'django.contrib.staticfiles',
 40 |     'tracker.apps.TrackerConfig',
 41 |     'accounts.apps.AccountsConfig',
 42 |     'django_tables2',
 43 |     'django_filters',
 44 |     'crispy_forms',
 45 |     'tracker.templatetags.month_labels',
 46 |     'social_django'
 47 | ]
 48 | 
 49 | MIDDLEWARE = [
 50 |     'django.middleware.security.SecurityMiddleware',
 51 |     'django.contrib.sessions.middleware.SessionMiddleware',
 52 |     'django.middleware.common.CommonMiddleware',
 53 |     'django.middleware.csrf.CsrfViewMiddleware',
 54 |     'django.contrib.auth.middleware.AuthenticationMiddleware',
 55 |     'django.contrib.messages.middleware.MessageMiddleware',
 56 |     'django.middleware.clickjacking.XFrameOptionsMiddleware',
 57 | ]
 58 | 
 59 | ROOT_URLCONF = 'website.urls'
 60 | 
 61 | TEMPLATES = [
 62 |     {
 63 |         'BACKEND': 'django.template.backends.django.DjangoTemplates',
 64 |         'DIRS': ['./templates',],
 65 |         'APP_DIRS': True,
 66 |         'OPTIONS': {
 67 |             'context_processors': [
 68 |                 'django.template.context_processors.debug',
 69 |                 'django.template.context_processors.request',
 70 |                 'django.contrib.auth.context_processors.auth',
 71 |                 'django.contrib.messages.context_processors.messages',
 72 |                 'social_django.context_processors.backends',
 73 |                 'social_django.context_processors.login_redirect',
 74 |             ],
 75 |         },
 76 |     },
 77 | ]
 78 | 
 79 | WSGI_APPLICATION = 'website.wsgi.application'
 80 | 
 81 | 
 82 | # Database
 83 | # https://docs.djangoproject.com/en/2.0/ref/settings/#databases
 84 | 
 85 | DATABASES = {
 86 |     'default': {
 87 |         'ENGINE': 'django.db.backends.sqlite3',
 88 |         'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
 89 |     }
 90 | }
 91 | 
 92 | 
 93 | # Password validation
 94 | # https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators
 95 | 
 96 | AUTH_PASSWORD_VALIDATORS = [
 97 |     {
 98 |         'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
 99 |     },
100 |     {
101 |         'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
102 |     },
103 |     {
104 |         'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
105 |     },
106 |     {
107 |         'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
108 |     },
109 | ]
110 | 
111 | 
112 | # Internationalization
113 | # https://docs.djangoproject.com/en/2.0/topics/i18n/
114 | 
115 | LANGUAGE_CODE = 'en-us'
116 | 
117 | TIME_ZONE = 'UTC'
118 | 
119 | USE_I18N = True
120 | 
121 | USE_L10N = True
122 | 
123 | USE_TZ = True
124 | 
125 | 
126 | # Static files (CSS, JavaScript, Images)
127 | # https://docs.djangoproject.com/en/2.0/howto/static-files/
128 | 
129 | STATIC_URL = '/static/'
130 | 
131 | LOGIN_URL = '/accounts/login'
132 | LOGIN_REDIRECT_URL = '/tracker'
133 | 
134 | AUTHENTICATION_BACKENDS = (
135 |  'social_core.backends.open_id.OpenIdAuth',  # for Google authentication
136 |  'social_core.backends.google.GoogleOpenId',  # for Google authentication
137 |  'social_core.backends.google.GoogleOAuth2',  # for Google authentication
138 |  'django.contrib.auth.backends.ModelBackend',
139 | )
140 | 
141 | SOCIAL_AUTH_PIPELINE = (
142 |     'social_core.pipeline.social_auth.social_details',
143 |     'social_core.pipeline.social_auth.social_uid',
144 |     'social_core.pipeline.social_auth.auth_allowed',
145 |     'social_core.pipeline.social_auth.social_user',
146 |     'social_core.pipeline.social_auth.associate_user',
147 |     'social_core.pipeline.social_auth.load_extra_data',
148 |     'social_core.pipeline.user.user_details',
149 |     'accounts.pipelines.saveAvatar',
150 | )
151 | 
152 | SOCIAL_AUTH_GOOGLE_OAUTH2_KEY=''#ClientKey
153 | SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET=''#SecretKey
154 | 
--------------------------------------------------------------------------------
/website/urls.py:
--------------------------------------------------------------------------------
 1 | from django.contrib import admin
 2 | from django.urls import path, include
 3 | 
 4 | urlpatterns = [
 5 |     path('admin/', admin.site.urls),
 6 |     path('', include('tracker.urls')),
 7 |     path('', include('accounts.urls'))
 8 | ]
 9 | 
10 | urlpatterns += [
11 |     path('accounts/', include('django.contrib.auth.urls')),
12 |     path('auth/', include('social_django.urls', namespace='social')),
13 | ]
14 | 
15 | 
--------------------------------------------------------------------------------
/website/wsgi.py:
--------------------------------------------------------------------------------
 1 | """
 2 | WSGI config for website 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/2.0/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", "website.settings")
15 | 
16 | application = get_wsgi_application()
17 | 
--------------------------------------------------------------------------------