commit e385650802dde5b79df3fb4df51005bf57ed056f Author: Timotheus Pokorra Date: Thu Jan 7 22:01:30 2021 +0100 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a39fa80 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +db.sqlite3 +.venv +static +local.py +settings_local.py +__pycache__ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4516897 --- /dev/null +++ b/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) Timotheus Pokorra +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. Neither the name of BasxConnect nor the names of its contributors may be used + to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/LICENSE.Skeleton b/LICENSE.Skeleton new file mode 100644 index 0000000..595589a --- /dev/null +++ b/LICENSE.Skeleton @@ -0,0 +1,23 @@ +https://github.com/dhg/Skeleton/blob/master/LICENSE.md + +The MIT License (MIT) + +Copyright (c) 2011-2014 Dave Gamache + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4094caa --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +About +===== + +This tool helps to watch your crypto portfolio. + +You can see the current value of your wallet, and the current value of the crypto currencies you are watching. + +You can manage your past transactions to and from your wallet. + +This application is written in Python using Django. + +Install +======= + + python3 -m venv .venv + source .venv/bin/activate + pip install -r requirements.txt diff --git a/apps/core/templates/base.html b/apps/core/templates/base.html new file mode 100644 index 0000000..f332c33 --- /dev/null +++ b/apps/core/templates/base.html @@ -0,0 +1,23 @@ +{% load static %} + + + Wallet Monitor + + + + + + + + + {% if user.is_authenticated %} + + {% endif %} + {% block content %} + {% endblock %} + + diff --git a/apps/core/templates/django_registration/activation_complete.html b/apps/core/templates/django_registration/activation_complete.html new file mode 100644 index 0000000..bf22323 --- /dev/null +++ b/apps/core/templates/django_registration/activation_complete.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} + +{% block content %} +
+

Success!

+ +

Now you can Login

+
+{% endblock %} + diff --git a/apps/core/templates/django_registration/activation_email_body.txt b/apps/core/templates/django_registration/activation_email_body.txt new file mode 100644 index 0000000..c4cbe72 --- /dev/null +++ b/apps/core/templates/django_registration/activation_email_body.txt @@ -0,0 +1,18 @@ +Hello {{ user.username }}, + +you have registered at {{ site }} + +If this was not you, please ignore this email! + +If you want to activate your account at https://{{ site }}, please click on this link: + +{% autoescape off %} +Please click on the link to confirm your registration, https://{{ site }}{% url 'django_registration_activate' activation_key=activation_key %} +{% endautoescape %} + +All the best! + +The team at {{ site }} + +PS: this is an automated E-Mail, please do not reply. +You can contact us at info@{{ site }} diff --git a/apps/core/templates/django_registration/activation_email_subject.txt b/apps/core/templates/django_registration/activation_email_subject.txt new file mode 100644 index 0000000..9af1da3 --- /dev/null +++ b/apps/core/templates/django_registration/activation_email_subject.txt @@ -0,0 +1 @@ +Confirm your registration for {{ site }} diff --git a/apps/core/templates/django_registration/activation_failed.html b/apps/core/templates/django_registration/activation_failed.html new file mode 100644 index 0000000..458f798 --- /dev/null +++ b/apps/core/templates/django_registration/activation_failed.html @@ -0,0 +1,11 @@ +{% extends "base.html" %} + +{% block content %} +
+

Something went wrong.

+ +

Perhaps you have confirmed already?

+ +

Please try to Login

+
+{% endblock %} diff --git a/apps/core/templates/django_registration/registration_closed.html b/apps/core/templates/django_registration/registration_closed.html new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/apps/core/templates/django_registration/registration_closed.html @@ -0,0 +1 @@ + diff --git a/apps/core/templates/django_registration/registration_complete.html b/apps/core/templates/django_registration/registration_complete.html new file mode 100644 index 0000000..acd9809 --- /dev/null +++ b/apps/core/templates/django_registration/registration_complete.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} + +{% block content %} +
+

Success!

+ +

An E-Mail has been sent to. Please confirm by clicking on the link in that E-Mail!

+
+{% endblock %} diff --git a/apps/core/templates/django_registration/registration_form.html b/apps/core/templates/django_registration/registration_form.html new file mode 100644 index 0000000..6911448 --- /dev/null +++ b/apps/core/templates/django_registration/registration_form.html @@ -0,0 +1,33 @@ +{% extends "base.html" %} + +{% block content %} +
+ {% if form.errors %} +

{{ form.errors }}

+ {% endif %} + +
+ {% csrf_token %} + + + + + + + + + + + + + + + + + +
{{ form.username.label_tag }}{{ form.username }}
{{ form.email.label_tag }}{{ form.email }}
{{ form.password1.label_tag }}{{ form.password1 }}
{{ form.password2.label_tag }}{{ form.password2 }}
+ + +
+
+{% endblock %} diff --git a/apps/core/templates/registration/login.html b/apps/core/templates/registration/login.html new file mode 100644 index 0000000..1ca2c2c --- /dev/null +++ b/apps/core/templates/registration/login.html @@ -0,0 +1,38 @@ +{% extends "base.html" %} + +{% block content %} +
+ {% if form.errors %} +

Your username and password didn't match. Please try again.

+ {% endif %} + + {% if next %} + {% if user.is_authenticated %} +

Your account doesn't have access to this page. To proceed, + please login with an account that has access.

+ {% else %} +

Please login to see this page.

+ {% endif %} + {% endif %} + +
+ {% csrf_token %} + + + + + + + + + +
{{ form.username.label_tag }}{{ form.username }}
{{ form.password.label_tag }}{{ form.password }}
+ + +
+ + {# Assumes you setup the password_reset view in your URLconf #} +

Lost password?

+

Create a free account

+
+{% endblock %} diff --git a/apps/core/views.py b/apps/core/views.py new file mode 100644 index 0000000..7ff3ac0 --- /dev/null +++ b/apps/core/views.py @@ -0,0 +1,9 @@ +from django.contrib.auth import authenticate +from django.shortcuts import render, redirect + +def home(request): + # if not logged in => redirect to login screen + if not request.user.is_authenticated: + return redirect('/accounts/login/') + # if logged in => redirect to monitor view + return redirect('/monitor') diff --git a/apps/monitor/__init__.py b/apps/monitor/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/monitor/admin.py b/apps/monitor/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/apps/monitor/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/monitor/apps.py b/apps/monitor/apps.py new file mode 100644 index 0000000..7a5aa7e --- /dev/null +++ b/apps/monitor/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class MonitorConfig(AppConfig): + name = 'monitor' diff --git a/apps/monitor/calc.py b/apps/monitor/calc.py new file mode 100644 index 0000000..54f1194 --- /dev/null +++ b/apps/monitor/calc.py @@ -0,0 +1,90 @@ +from django.db import connection +from apps.transactions.models import Transaction +from apps.rates.models import ExchangeRate +from decimal import Decimal +import datetime + +class Calc: + def ShowDiffRate(self, DayDiff, CurrentRate, Crypto, Fiat): + with connection.cursor() as cursor: + sql = """SELECT rate, datetime_valid FROM exchangerate WHERE crypto_currency = %s AND fiat_currency = %s + AND datetime_valid BETWEEN %s AND %s + ORDER BY datetime_valid DESC""" + startDate = datetime.datetime.now() - datetime.timedelta(days=int(DayDiff)) + endDate = startDate + datetime.timedelta(days=1) + cursor.execute(sql, [Crypto, Fiat, startDate, endDate]) + rateMinusXDay = cursor.fetchone() + if rateMinusXDay: + return {"dateRelative": "%s days ago" % (DayDiff,), "date" : rateMinusXDay[1], "rateEUR": rateMinusXDay[0], + "diffPercentage": (CurrentRate.rate-Decimal(rateMinusXDay[0]))/Decimal(rateMinusXDay[0])*100, "rateUSD": None} + return None + + def GetCurrentValue(self, userid, crypto, total_investment, current_value, total_tax_free): + amount_kept = 0 + out = {} + out["crypto"] = crypto + + with connection.cursor() as cursor: + sql = """SELECT SUM(amount_after_fee) as after_fee, SUM(amount) as amount FROM `transaction` WHERE amount >= 0 and crypto_currency=%s and owner_id=%s""" + cursor.execute(sql, [crypto, userid]) + bought = cursor.fetchone() + if bought[1]: + out["bought"] = bought[1] + total_investment += Decimal(bought[1]) + amount_kept += Decimal(bought[0]) + else: + out["bought"] = None + + with connection.cursor() as cursor: + sql = """SELECT SUM(amount_before_fee) as before_fee, SUM(amount) as amount FROM `transaction` WHERE amount < 0 and crypto_currency=%s and owner_id=%s""" + cursor.execute(sql, [crypto, userid]) + sold = cursor.fetchone() + if sold[1]: + out["sold"] = sold[1] + total_investment += Decimal(sold[1]) + amount_kept += Decimal(sold[0]) + else: + out["sold"] = None + + rateEUR = ExchangeRate.objects.filter(crypto_currency=crypto, fiat_currency='EUR').order_by('-datetime_valid').first() + rateUSD = ExchangeRate.objects.filter(crypto_currency=crypto, fiat_currency='USD').order_by('-datetime_valid').first() + + if rateEUR: + cv = Decimal(rateEUR.rate) * amount_kept + current_value += cv + out["current_value"] = cv + out["amount_kept"] = amount_kept + out["rateEUR"] = rateEUR.rate + else: + out["current_value"] = None + out["amount_kept"] = None + out["rateEUR"] = None + + out["rates"] = [] + if rateUSD and rateEUR: + out["rates"].append({"dateRelative": "Now", "date" : rateEUR.datetime_valid, "rateEUR": rateEUR.rate, "diffPercentage": None, "rateUSD": rateUSD.rate}) + + if rateEUR: + for daydiff in ["1", "3", "7", "14", "30"]: + diffrate = self.ShowDiffRate(daydiff, rateEUR, crypto, 'EUR') + if diffrate: + out["rates"].append(diffrate) + + amount_within_past_year = 0 + with connection.cursor() as cursor: + sql = """SELECT SUM(amount_after_fee) as amount FROM `transaction` WHERE amount > 0 and crypto_currency=%s and owner_id=%s and date_valid>%s""" + cursor.execute(sql, [crypto, userid, datetime.datetime.now() - datetime.timedelta(days=365)]) + bought = cursor.fetchone() + out["bought_recently"] = None + if bought[0]: + out["bought_recently"] = bought[0] + amount_within_past_year = Decimal(bought[0]) + amount_available_to_sell = amount_kept - amount_within_past_year + out["bought_tax_free"] = None + out["value_tax_free"] = None + if rateEUR: + out["bought_tax_free"] = amount_available_to_sell + out["value_tax_free"] = amount_available_to_sell * rateEUR.rate + total_tax_free += out["value_tax_free"] + + return (total_investment, current_value, total_tax_free, rateEUR.rate, rateUSD.rate, out) diff --git a/apps/monitor/migrations/__init__.py b/apps/monitor/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/monitor/models.py b/apps/monitor/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/apps/monitor/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/apps/monitor/templates/monitor.html b/apps/monitor/templates/monitor.html new file mode 100644 index 0000000..c6858b3 --- /dev/null +++ b/apps/monitor/templates/monitor.html @@ -0,0 +1,64 @@ +{% extends 'base.html' %} + +{% block content %} +
+

Monitor

+

Hint: these numbers are not very accurate.

+ +{% for c in cryptos %} + + + +
+ {% if c.bought %} + We have bought {{ c.crypto }} for {{ c.bought|floatformat:2 }} EUR
+ {% endif %} + {% if c.sold %} + We have sold {{ c.crypto }} for {{ c.sold|floatformat:2 }} EUR
+ {% endif %} + {% if c.amount_kept %} + Current value of our {{ c.amount_kept|floatformat:3 }} {{ c.crypto }} is {{ c.current_value|floatformat:2 }} EUR
+ {% endif %} + {% if c.bought_recently %} + We have bought within the last 12 months: {{ c.bought_recently|floatformat:3 }} {{ c.crypto }}
+ {% endif %} + {% if c.bought_tax_free %} + We can sell without paying taxes: {{ c.bought_tax_free|floatformat:3 }} {{ c.crypto }} for {{ c.value_tax_free|floatformat:2 }} EUR
+ {% endif %} + + {% if c.rates %} + + {% for r in c.rates %} + + + + + + + {% endfor %} +
{{ r.dateRelative }}{{ r.date|date:'Y-m-d' }}{{ r.rateEUR|floatformat:0 }} EUR/{{ c.crypto }} + {% if r.rateUSD %} +
+ {{ r.rateUSD|floatformat:0 }} USD/{{ c.crypto }} + {% endif %} +
{% if r.diffPercentage %} + {{ r.diffPercentage|floatformat:1 }}% + {% endif %} +
+ {% endif %} + +
My transactions
+
+ Google:
+ {{ c.crypto }} in EUR
+ {{ c.crypto }} in USD
+
+ +{% endfor %} + +
+{% endblock %} diff --git a/apps/monitor/tests.py b/apps/monitor/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/apps/monitor/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/monitor/views.py b/apps/monitor/views.py new file mode 100644 index 0000000..bb88b7e --- /dev/null +++ b/apps/monitor/views.py @@ -0,0 +1,22 @@ +from django.shortcuts import render, redirect +from django.contrib.auth.decorators import login_required +from apps.transactions.models import Transaction +from apps.rates.models import ExchangeRate +from apps.monitor.calc import Calc + +@login_required +def monitor(request): + transactions = Transaction.objects.filter(owner=request.user) + crypto_currencies = {tr.crypto_currency for tr in transactions} + cryptos = [] + total_investment = 0 + current_value = 0 + total_tax_free = 0 + calc = Calc() + + for crypto in crypto_currencies: + (total_investment, current_value, total_tax_free, rateEUR, rateUSD, out2) = calc.GetCurrentValue(request.user.id, crypto, total_investment, current_value, total_tax_free) + + cryptos.append({"crypto": crypto, "rateEUR": rateEUR, "rateUSD": rateUSD, **out2}) + + return render(request,"monitor.html",{'cryptos':cryptos}) diff --git a/apps/rates/__init__.py b/apps/rates/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/rates/admin.py b/apps/rates/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/apps/rates/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/rates/apps.py b/apps/rates/apps.py new file mode 100644 index 0000000..50c55c5 --- /dev/null +++ b/apps/rates/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class RatesConfig(AppConfig): + name = 'rates' diff --git a/apps/rates/migrations/0001_initial.py b/apps/rates/migrations/0001_initial.py new file mode 100644 index 0000000..e0ac9ad --- /dev/null +++ b/apps/rates/migrations/0001_initial.py @@ -0,0 +1,27 @@ +# Generated by Django 3.1.4 on 2021-01-03 21:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='ExchangeRate', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('crypto_currency', models.CharField(max_length=10)), + ('fiat_currency', models.CharField(max_length=10)), + ('rate', models.DecimalField(decimal_places=10, max_digits=24)), + ('datetime_valid', models.DateTimeField(verbose_name='datetime valid')), + ], + options={ + 'db_table': 'exchangerate', + }, + ), + ] diff --git a/apps/rates/migrations/__init__.py b/apps/rates/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/rates/models.py b/apps/rates/models.py new file mode 100644 index 0000000..ba9684d --- /dev/null +++ b/apps/rates/models.py @@ -0,0 +1,10 @@ +from django.db import models + +# Create your models here. +class ExchangeRate(models.Model): + crypto_currency = models.CharField(max_length=10) + fiat_currency = models.CharField(max_length=10) + rate = models.DecimalField(max_digits=24, decimal_places=10) + datetime_valid = models.DateTimeField('datetime valid') + class Meta: + db_table = "exchangerate" diff --git a/apps/rates/tests.py b/apps/rates/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/apps/rates/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/rates/views.py b/apps/rates/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/apps/rates/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/apps/transactions/__init__.py b/apps/transactions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/transactions/admin.py b/apps/transactions/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/apps/transactions/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/transactions/apps.py b/apps/transactions/apps.py new file mode 100644 index 0000000..37b2cfa --- /dev/null +++ b/apps/transactions/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class TransactionsConfig(AppConfig): + name = 'transactions' diff --git a/apps/transactions/forms.py b/apps/transactions/forms.py new file mode 100644 index 0000000..f5a506c --- /dev/null +++ b/apps/transactions/forms.py @@ -0,0 +1,14 @@ +from django import forms +from apps.transactions.models import Transaction +#from datetimepicker.widgets import DateTimePicker +#from django.contrib.admin.widgets import AdminDateWidget + +class TransactionForm(forms.ModelForm): + + class Meta: + model = Transaction + fields = "__all__" + + # https://stackoverflow.com/a/52702275/1632368 + #date_valid = forms.DateField(widget=AdminDateWidget()) + diff --git a/apps/transactions/migrations/0001_initial.py b/apps/transactions/migrations/0001_initial.py new file mode 100644 index 0000000..5c4d155 --- /dev/null +++ b/apps/transactions/migrations/0001_initial.py @@ -0,0 +1,34 @@ +# Generated by Django 3.1.4 on 2020-12-26 10:16 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Transaction', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('crypto_currency', models.CharField(max_length=10)), + ('amount_before_fee', models.DecimalField(decimal_places=10, max_digits=24)), + ('amount_after_fee', models.DecimalField(decimal_places=10, max_digits=24)), + ('exchange_rate', models.DecimalField(decimal_places=10, max_digits=24)), + ('fiat_currency', models.CharField(max_length=10)), + ('amount', models.DecimalField(decimal_places=10, max_digits=24)), + ('date_valid', models.DateTimeField(verbose_name='date transfered')), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'db_table': 'transaction', + }, + ), + ] diff --git a/apps/transactions/migrations/__init__.py b/apps/transactions/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/transactions/models.py b/apps/transactions/models.py new file mode 100644 index 0000000..f34f74e --- /dev/null +++ b/apps/transactions/models.py @@ -0,0 +1,15 @@ +from django.db import models +from django.contrib.auth.models import User + +# Create your models here. +class Transaction(models.Model): + crypto_currency = models.CharField(max_length=10) + owner = models.ForeignKey(User, on_delete=models.CASCADE) + amount_before_fee = models.DecimalField(max_digits=24, decimal_places=10) + amount_after_fee = models.DecimalField(max_digits=24, decimal_places=10) + exchange_rate = models.DecimalField(max_digits=24, decimal_places=10) + fiat_currency = models.CharField(max_length=10) + amount = models.DecimalField(max_digits=24, decimal_places=10) + date_valid = models.DateTimeField('date transfered') + class Meta: + db_table = "transaction" diff --git a/apps/transactions/templates/add.html b/apps/transactions/templates/add.html new file mode 100644 index 0000000..44780c4 --- /dev/null +++ b/apps/transactions/templates/add.html @@ -0,0 +1,90 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+ {% csrf_token %} +
+
+{% if form.errors %} + {% for field in form %} + {% for error in field.errors %} +
+ {{field.name}}: {{ error|escape }} +
+ {% endfor %} + {% endfor %} + {% for error in form.non_field_errors %} +
+ {{ error|escape }} +
+ {% endfor %} +{% endif %} + +
+ +
+

Add transaction

+
+
+ +
+ +
+ {{ form.crypto_currency }} +
+
+ +
+ +
+ {{ form.amount_before_fee }} +
+
+ +
+ +
+ {{ form.amount_after_fee }} +
+
+ +
+ +
+ {{ form.exchange_rate }} +
+
+ +
+ +
+ {{ form.fiat_currency }} +
+
+ +
+ +
+ {{ form.amount }} +
+
+ +
+ +
+ {{ form.date_valid }} +
+
+ +
+ +
+ +
+
+ +
+
+
+{% endblock %} diff --git a/apps/transactions/templates/edit.html b/apps/transactions/templates/edit.html new file mode 100644 index 0000000..fabd282 --- /dev/null +++ b/apps/transactions/templates/edit.html @@ -0,0 +1,90 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+ {% csrf_token %} +
+
+{% if form.errors %} + {% for field in form %} + {% for error in field.errors %} +
+ {{field.name}}: {{ error|escape }} +
+ {% endfor %} + {% endfor %} + {% for error in form.non_field_errors %} +
+ {{ error|escape }} +
+ {% endfor %} +{% endif %} + +
+ +
+

Update Transaction

+
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+
+
+{% endblock %} diff --git a/apps/transactions/templates/show.html b/apps/transactions/templates/show.html new file mode 100644 index 0000000..dd0909b --- /dev/null +++ b/apps/transactions/templates/show.html @@ -0,0 +1,41 @@ +{% extends 'base.html' %} +{% load humanize %} + +{% block content %} +
+
Add New Record
+
+ + + + + + + + + + + +{% for transaction in transactions %} + + + + + + + + +{% endfor %} + +
DateAmountValueRateActions
{{ transaction.date_valid }}{{ transaction.amount_before_fee|floatformat:3 }} {{ transaction.crypto_currency }}{{ transaction.amount|floatformat:2 }} {{ transaction.fiat_currency }}{% if transaction.amount < 0 %} + {{ transaction.exchange_rate|floatformat:0 }} {{ transaction.crypto_currency }}/{{ transaction.fiat_currency }} + {% endif %} + {% if transaction.amount > 0 %} + {{ transaction.exchange_rate|floatformat:0 }} {{ transaction.crypto_currency }}/{{ transaction.fiat_currency }} + {% endif %} + + Edit + Delete +
+
+{% endblock %} diff --git a/apps/transactions/tests.py b/apps/transactions/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/apps/transactions/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/transactions/views.py b/apps/transactions/views.py new file mode 100644 index 0000000..57db05d --- /dev/null +++ b/apps/transactions/views.py @@ -0,0 +1,52 @@ +from django.shortcuts import render, redirect +from django.contrib.auth.decorators import login_required +from apps.transactions.forms import TransactionForm +from apps.transactions.models import Transaction + +@login_required +def add(request): + if request.method == "POST": + # request.POST is immutable, so make a copy + values = request.POST.copy() + values['owner'] = request.user.id + form = TransactionForm(values) + if form.is_valid(): + try: + form.save() + return redirect('/transactions/show') + except: + pass + else: + form = TransactionForm() + return render(request,'add.html',{'form':form}) + +@login_required +def show(request): + if 'crypto' in request.GET: + transactions = Transaction.objects.filter(owner=request.user, crypto_currency=request.GET['crypto']).order_by('-date_valid') + else: + transactions = Transaction.objects.filter(owner=request.user).order_by('-date_valid') + return render(request,"show.html",{'transactions':transactions}) + +@login_required +def edit(request, id): + transaction = Transaction.objects.get(id=id, owner=request.user) + return render(request,'edit.html', {'transaction':transaction}) + +@login_required +def update(request, id): + transaction = Transaction.objects.get(id=id, owner=request.user) + # request.POST is immutable, so make a copy + values = request.POST.copy() + values['owner'] = request.user.id + form = TransactionForm(values, instance = transaction) + if form.is_valid(): + form.save() + return redirect("/transactions/show") + return render(request, 'edit.html', {'transaction': transaction, 'form': form}) + +@login_required +def destroy(request, id): + transaction = Transaction.objects.get(id=id, owner=request.user.id) + transaction.delete() + return redirect("/transactions/show") diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..7af578f --- /dev/null +++ b/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'walletmonitor.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7ff85a5 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +django +django-registration + diff --git a/walletmonitor/__init__.py b/walletmonitor/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/walletmonitor/asgi.py b/walletmonitor/asgi.py new file mode 100644 index 0000000..c3d3efb --- /dev/null +++ b/walletmonitor/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for walletmonitor project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'walletmonitor.settings') + +application = get_asgi_application() diff --git a/walletmonitor/settings.py b/walletmonitor/settings.py new file mode 100644 index 0000000..574d90f --- /dev/null +++ b/walletmonitor/settings.py @@ -0,0 +1,126 @@ +""" +Django settings for walletmonitor project. + +Generated by 'django-admin startproject' using Django 3.1.4. + +For more information on this file, see +https://docs.djangoproject.com/en/3.1/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/3.1/ref/settings/ +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/ + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django.contrib.humanize', + 'django.contrib.sites', + 'django_registration', + 'apps.core', + 'apps.rates', + 'apps.transactions', + 'apps.monitor', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'walletmonitor.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'walletmonitor.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/3.1/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/3.1/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.1/howto/static-files/ + +STATIC_URL = '/static/' +STATICFILES_DIRS = [ '/static/' ] +LOGIN_REDIRECT_URL = '/home/' +LOGOUT_REDIRECT_URL = '/home/' + +from .settings_local import * diff --git a/walletmonitor/settings_local.py.example b/walletmonitor/settings_local.py.example new file mode 100644 index 0000000..3baa2ba --- /dev/null +++ b/walletmonitor/settings_local.py.example @@ -0,0 +1,16 @@ +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ACCOUNT_ACTIVATION_DAYS = 1 +REGISTRATION_OPEN = True +DEFAULT_FROM_EMAIL = 'no-reply@example.org' +EMAIL_HOST = 'smtp.example.org' +EMAIL_PORT = 25 +EMAIL_HOST_USER = 'myuser' +EMAIL_HOST_PASSWORD = 'topsecret' +EMAIL_USE_TLS = True + +SITE_ID = 1 diff --git a/walletmonitor/urls.py b/walletmonitor/urls.py new file mode 100644 index 0000000..8c490df --- /dev/null +++ b/walletmonitor/urls.py @@ -0,0 +1,35 @@ +"""walletmonitor URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/3.1/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path +from django.conf.urls import include +from apps.transactions import views as tr_views +from apps.core import views as core_views +from apps.monitor import views as monitor_views + +urlpatterns = [ + path('admin/', admin.site.urls), + path('accounts/', include('django_registration.backends.activation.urls')), + path('accounts/', include('django.contrib.auth.urls')), + path('', core_views.home), + path('home/', core_views.home), + path('monitor/', monitor_views.monitor), + path('transactions/add', tr_views.add), + path('transactions/show', tr_views.show), + path('transactions/edit/', tr_views.edit), + path('transactions/update/', tr_views.update), + path('transactions/delete/', tr_views.destroy), +] diff --git a/walletmonitor/wsgi.py b/walletmonitor/wsgi.py new file mode 100644 index 0000000..672f3f9 --- /dev/null +++ b/walletmonitor/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for walletmonitor project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'walletmonitor.settings') + +application = get_wsgi_application()