From 4c0eaf3cca99e70cbb6fece3bd2339c81f9b3d26 Mon Sep 17 00:00:00 2001 From: Kevin Hellemun Date: Sun, 2 Jul 2017 21:54:45 +0200 Subject: [PATCH] Release v0.7.1 --- .gitignore | 8 + .travis.yml | 16 +- BunqAPI/callbacks.py | 3 +- BunqWebApp/config.yaml.gpg | Bin 0 -> 932 bytes BunqWebApp/settings.py | 50 +++- BunqWebApp/views.py | 26 +- Caddyfile.gpg | Bin 0 -> 656 bytes History.md | 14 + README.md | 8 +- Wiki | 2 +- bower.json | 25 ++ bunq_bot/responses/commands/start.md | 11 +- filecreator/creator.py | 18 +- filecreator/views.py | 12 +- keyrings/live/blackbox-admins.txt | 1 + keyrings/live/blackbox-files.txt | 4 + keyrings/live/pubring.kbx | Bin 0 -> 2505 bytes keyrings/live/trustdb.gpg | Bin 0 -> 1200 bytes package.json | 3 +- requirements.txt | 8 +- static/BunqAPI/CSS/my_bunq.css | 3 + static/BunqAPI/JS/my_bunq.js | 242 ++++++++---------- .../BunqAPI/templates/mustache/accounts.html | 2 + .../BunqAPI/templates/mustache/payments.html | 58 ++--- .../mustache/single_transaction.html | 21 ++ static/images/under-construction-gif-6.gif | Bin 0 -> 5389 bytes templates/BunqAPI/my_bunq.html | 36 ++- templates/Manager/index.html | 21 +- uwsgi-develop.ini.gpg | Bin 0 -> 762 bytes uwsgi.ini.gpg | Bin 0 -> 756 bytes 30 files changed, 352 insertions(+), 240 deletions(-) create mode 100644 BunqWebApp/config.yaml.gpg create mode 100644 Caddyfile.gpg create mode 100644 History.md create mode 100644 bower.json create mode 100644 keyrings/live/blackbox-admins.txt create mode 100644 keyrings/live/blackbox-files.txt create mode 100644 keyrings/live/pubring.kbx create mode 100644 keyrings/live/trustdb.gpg create mode 100644 static/BunqAPI/templates/mustache/single_transaction.html create mode 100644 static/images/under-construction-gif-6.gif create mode 100644 uwsgi-develop.ini.gpg create mode 100644 uwsgi.ini.gpg diff --git a/.gitignore b/.gitignore index ce5c5b9..c29d60f 100644 --- a/.gitignore +++ b/.gitignore @@ -185,3 +185,11 @@ tmp/ static/semantic/tasks static/semantic/src static/semantic/dist/components +/keyrings/live/pubring.gpg~ +/keyrings/live/pubring.kbx~ +/keyrings/live/secring.gpg +/Caddyfile +/uwsgi.ini +/BunqWebApp/config.yaml +/uwsgi-develop.ini +Android Emulator diff --git a/.travis.yml b/.travis.yml index 3b75b58..525d667 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,13 +21,15 @@ before_script: - "export DISPLAY=:99.0" - "sh -e /etc/init.d/xvfb start" - sleep 3 # give xvfb some time to start -- sudo /etc/init.d/postgresql stop -- wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key - add - -- sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt/ precise-pgdg main - 9.5" >> /etc/apt/sources.list.d/postgresql.list' -- sudo apt-get update -- sudo apt-get install postgresql-9.5 +- npm install -g bower +- bower install +# - sudo /etc/init.d/postgresql stop +# - wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key +# add - +# - sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt/ precise-pgdg main +# 9.5" >> /etc/apt/sources.list.d/postgresql.list' +# - sudo apt-get update +# - sudo apt-get install postgresql-9.5 - psql -c "CREATE DATABASE travisci;" -U postgres - ./manage.py migrate - ./manage.py collectstatic --noinput -v 0 diff --git a/BunqAPI/callbacks.py b/BunqAPI/callbacks.py index 49ec38a..8271897 100644 --- a/BunqAPI/callbacks.py +++ b/BunqAPI/callbacks.py @@ -329,7 +329,8 @@ def get_payment_pdf(self): try: pdf = Creator(self._user, - 'pdf').payment(payment['Response'][0]) + 'pdf').payment(payment['Response'][0], + transaction_id=self.payment_id) except KeyError: # pragma: no cover error_msg = payment['Error'][0]['error_description_translated'] error = { diff --git a/BunqWebApp/config.yaml.gpg b/BunqWebApp/config.yaml.gpg new file mode 100644 index 0000000000000000000000000000000000000000..0325a973a23ed642ccd093563d435c2e7a522c17 GIT binary patch literal 932 zcmV;V16%xs0t^FCDvNjhhW@hw5C38)7{D1Ci(gx+zKe8nSw9m6%A*LrawcUPw(QML znJ-}%Nl}hadrH+t_!!0ZdrL=_x7?aBwD0C^BWQCJrf{ps2vzv+zi84(p#5{k{d#Yu znD^$zM`puA&^ehLHqxJ*yxI>Wtmwq`2prHkHkCyZQ>-kzzh+9f1UFr^ZB3mz>dWQW zrVL!~5D&X610s&~|M}UnhjsVc4*mo55p#kRnGv6|Xx*ANH})FpVjnSd9y!UZy~&Ze$~9CRB8}FIt~vsr z4O0Zay2n%oA*Q?|9(G`hF8vf>naAY?lc?YPVowA3;L2?Y&UxPrwj)f)Po!mZ}o{#y5bmtlz9}eCv-$;MO@QM{+2zBbPqzQ1InCm4espoHj8)x zuXa3)V$q#`)!zr|TjktoQ(o5iA53N!Ey2Ixdb{0hWM|LVr?- zXMyXsS1TC?z2wt3fM3$U(g8ku2SBIc#c_R;zeyx?4A2(|(UX6HdxE4EvhC)2cd9J0 zKu=o-F`qHwC8XXuRP{y-jp(Jo3uoiiJIUYy2?vbolo z{(f6Cu2L@-Mh(Cncp0Z>J_&7X=?Mc#!T<>hMd5kveOb3Fi1WTVV-rFfxiT!7wV{Ha_+$cH#?dF`eL;RB>PPzu zVkxn8?6c%6^ZhY8h*05XWh#)BC3^><3Fg&O=dpnHeF2Oq-`Bet> z@D=R2CxxN>yj>mvUo(X`Jeuq$AMh(NuA*>)*<5M*f~s$V1XqpX!re@Pm0Js?YgRuE GPt2*^L%UJ{ literal 0 HcmV?d00001 diff --git a/BunqWebApp/settings.py b/BunqWebApp/settings.py index 82775fe..58fba71 100644 --- a/BunqWebApp/settings.py +++ b/BunqWebApp/settings.py @@ -12,13 +12,22 @@ import os import dj_database_url +from ruamel.yaml import YAML +from box import Box +import pathlib # import raven # from django.core.urlresolvers import reverse_lazy - # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__)) +config_file = pathlib.Path(os.path.join(BASE_DIR, 'BunqWebApp/config.yaml')) +if config_file.is_file(): + config = Box(YAML(typ='safe').load(pathlib.Path( + os.path.join(BASE_DIR, + 'BunqWebApp/config.yaml')))) # noqa +else: + config = None # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ @@ -44,21 +53,33 @@ Exception('Please create a %s file with random characters \ to generate your secret key!' % SECRET_FILE) -DEBUG = True -SECURE_SSL_REDIRECT = False -SESSION_COOKIE_SECURE = False -DESABLE_LOGGERS = False -SANDBOX = True -ALLOWED_HOSTS = ['*'] -USE_PROXY = False -PROXY_URI = 'url' -TELEGRAM_TOKEN = None +if config is not None: + DEBUG = True if str(config.DEBUG) == 'True' else False + SECURE_SSL_REDIRECT = False + SESSION_COOKIE_SECURE = False + DESABLE_LOGGERS = False + SANDBOX = True if str(config.SANDBOX) == 'True' else False + ALLOWED_HOSTS = ['*'] + USE_PROXY = True if str(config.USE_PROXY) == 'True' else False + PROXY_URI = config.PROXY_URI + TELEGRAM_TOKEN = None if str(config.TELEGRAM_TOKEN) == "None" else config\ + .TELEGRAM_TOKEN # noqa + +else: + DEBUG = True + SANDBOX = True + DESABLE_LOGGERS = False + ALLOWED_HOSTS = ['*'] + USE_PROXY = False + TELEGRAM_TOKEN = None SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') SESSION_EXPIRE_AT_BROWSER_CLOSE = True -RAVEN_CONFIG = { - 'dsn': None, + +if config is not None and config.DSN is not 'None': + RAVEN_CONFIG = { + 'dsn': config.DSN if str(config.DSN) != "None" else None, } # Application definition @@ -180,7 +201,8 @@ DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', - 'NAME': 'KevinH', + 'NAME': 'khellemun' if config is None else config.DATABASE.NAME, + 'USER': None if config is None else config.DATABASE.USER } } db_from_env = dj_database_url.config() @@ -230,7 +252,7 @@ STATIC_URL = '/static/' STATIC_ROOT = os.path.join(BASE_DIR, 'static-files') STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static'), os.path.join( - BASE_DIR, 'node_modules')] + BASE_DIR, 'node_modules'), os.path.join(BASE_DIR, 'bower_components')] STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' STATICFILES_FINDERS = [ 'django.contrib.staticfiles.finders.FileSystemFinder', diff --git a/BunqWebApp/views.py b/BunqWebApp/views.py index 58ab6c5..3415443 100644 --- a/BunqWebApp/views.py +++ b/BunqWebApp/views.py @@ -112,10 +112,11 @@ def post(self, request): password = form.cleaned_data['password'] file_contents = request.FILES['user_file'].read().decode() - self.authenticate_user(username, password, request) + user = self.authenticate_user(username, password, request) session = self.store_in_session(file_contents, password, username) - if session is not False: + if session: self.check_bunq_session(username) + login(request, user) return redirect('my_bunq') else: messages.error(request=request, @@ -133,7 +134,12 @@ def post(self, request): def authenticate_user(username, password, request): user = authenticate(username=username, password=password) if user is not None: - login(request, user) + # login(request, user) + ''' + This will always be called because users is being authenticated + in the form. + ''' + return user @staticmethod def store_in_session(data, password, username): @@ -143,7 +149,7 @@ def store_in_session(data, password, username): try: dec_data = signing.loads(data['secret'], key=password) except signing.BadSignature: - return False + return None enc_data = signing.dumps(dec_data) @@ -152,6 +158,7 @@ def store_in_session(data, password, username): s.create() user.session.session_token = s.session_key user.save() + return True @staticmethod def check_bunq_session(username): @@ -203,13 +210,13 @@ def post(self, request): password = form.cleaned_data['password'] encryption_password = form.cleaned_data['encryption_password'] user_file = request.FILES['user_file'] - self.authenticate_user(username=username, password=password, - request=request) + user = self.authenticate_user(username=username, password=password, + request=request) api_key = self.decrypt_file(user_file=user_file, encryption_password=encryption_password) # noqa if api_key is not False: - print(api_key) form = self.generate_form(initial={'API': api_key}) + login(request, user) return render(request, 'BunqAPI/index.html', {'form': form}) else: @@ -224,10 +231,7 @@ def post(self, request): def authenticate_user(username, password, request): user = authenticate(username=username, password=password) if user is not None: - login(request, user) - return True - else: - return False + return user @staticmethod def decrypt_file(user_file, encryption_password): diff --git a/Caddyfile.gpg b/Caddyfile.gpg new file mode 100644 index 0000000000000000000000000000000000000000..e870defd7eaac283dbdb3172a5d78c0b38e4d120 GIT binary patch literal 656 zcmV;B0&o3=0t^FCDvNjhhW@hw5C3s_LI_qucM(uS@p)+s0pX-7wM6q;WB+xhd8Kr0 zx@U$L6EwV6+sDW#SvOYdcxclWX<|gSyI?2ZZJo)OGmN-2dS?!DUtL7s}^t zoS&48@w9u4f~veO<{&32ta|5_8-6D?!BDQu_FVY#qJS%v2oV3Wgn2=wv5SQJUHzMV zY}Go)U8Yz5ReQUn(lmLhV=u)@+p1n@MWOvBI3AaoMDgv>uw^A2nY67!uE=Q5VIW!T zq%~gTKFfm!Hx9T~vx%p)qN8&?1@>LjLw>-{Ru0*=8CWUj{&MjmwDWK+asi za%}=o@~%4e7(vS#&>y|wndW06fcwurs$LCih3>n_X9vZvlWk&vzQw3#%ShO~jCfsqQfGjjmK6%#t>Z!? zy&`eW#)N8sx1P?D-r;d*-l@YB41Rubg-xDiLJ=U|7yH1mJJ!4GB(-v8IsUK$uiL$$ zs&kb%LdwNA$%Oxy1!tNh&{*rc?--ljr+yn8>Kh5n1j3XGp>@Gq?E__KlwR-VN4l$L zOQ}?KH}1U0bTSIZ%l$9$J%b4$L+9RrMxl0*UkRW88{zoPjPiLZ~gK$ z?)D0&*afe*44a#M-&)tLVMdzcX$S(OR2o1^yp*=h&szW!VLF?>>a1zaW20 +View the app live based on the latest [release]: View the [wiki] for more information. @@ -47,10 +47,10 @@ View the [wiki] for more information. - Use Bunq CSV file and see a Pie Charts of Income, Expanses, Transcation Names and Percentages - - User the bunq API to view your transactions + - Use the bunq API to view your transactions - Export your latest invoice - Export the shown transactions in CSV format - + - Export a bunq official transactions overview # Why Community ? @@ -96,8 +96,6 @@ MIT [django]: - [heroku]: - [npm]: [virtualenv]: diff --git a/Wiki b/Wiki index eaaf1b2..8a77981 160000 --- a/Wiki +++ b/Wiki @@ -1 +1 @@ -Subproject commit eaaf1b2e58fbfd9899e43e2b402049236c579ddb +Subproject commit 8a7798120fbbad119e8679c3c0275c7dec9e0db2 diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..ec3422a --- /dev/null +++ b/bower.json @@ -0,0 +1,25 @@ +{ + "name": "communitybunqweb", + "description": "Bunq web interface made by bunqers", + "main": "./manage.py", + "authors": [ + "OGKevin " + ], + "license": "MIT", + "keywords": [ + "bunq", + "pyhton", + "django" + ], + "homepage": "https://github.com/OGKevin/ComBunqWebApp", + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ], + "dependencies": { + "bPopup": "bpopup#^0.11.0" + } +} diff --git a/bunq_bot/responses/commands/start.md b/bunq_bot/responses/commands/start.md index f0871d5..cc7a95d 100644 --- a/bunq_bot/responses/commands/start.md +++ b/bunq_bot/responses/commands/start.md @@ -1,8 +1,9 @@ Hi I'm a bot, the bunq-bot here to help you the best way I can. -I can help you with: -Nothing at the moment :( +My commands are: +/start `To start receiving automatic messages.` +/stop `To stop receiving automatic messages. This command should also be used before you want to remove me.` +/news `Shows the recent 5 blog posts from bunq.` +/help `Displays a help message.` -If you need help you can use the /help command. - -My source code can be found [here](https://github.com/OGKevin/bunq-bot). +My source code can be found [here](https://github.com/OGKevin/ComBunqWebApp/tree/master/bunq_bot). diff --git a/filecreator/creator.py b/filecreator/creator.py index 2998941..4f1191b 100644 --- a/filecreator/creator.py +++ b/filecreator/creator.py @@ -24,7 +24,7 @@ def __init__(self, user, extension=None): self.user = user self.extension = extension - def payment(self, data): + def payment(self, data, transaction_id=None): data['Payment']['amount']['value'] = float( data['Payment']['amount']['value']) @@ -38,7 +38,8 @@ def payment(self, data): )).replace('.', ',') if self.extension == 'pdf': - file_path = self.pdf(data['Payment'], 'payment.html') + file_path = self.pdf(data['Payment'], 'payment.html', + prefix=transaction_id) self.store_in_session(file_path) @@ -158,7 +159,7 @@ def csv(headers, rows): return temp_file.name @staticmethod - def pdf(data, template): + def pdf(data, template, prefix=None): html_string = render_to_string( 'filecreator/pdf/%s' % template, {'data': data} ) @@ -177,24 +178,29 @@ def pdf(data, template): pdf = pdfkit.from_string(html_string, False, options=options) - temp_file = Creator.temp_file('.pdf') + temp_file = Creator.temp_file('.pdf', append_prefix=prefix) temp_file.write(pdf) temp_file.close() return temp_file.name @staticmethod - def temp_file(extension, bytes_=True): + def temp_file(extension, bytes_=True, append_prefix=None): if bytes_ is True: mode = 'wb' else: mode = 'w' + if append_prefix is not None: + prefix = "ComBunqWebApp-pr-%s-pr-" % append_prefix + else: + prefix = "ComBunqWebApp" + temp_file = tempfile.NamedTemporaryFile( mode=mode, dir=None, suffix='%s' % extension, - prefix='ComBunqWebApp', + prefix=prefix, delete=False ) return temp_file diff --git a/filecreator/views.py b/filecreator/views.py index 63b11de..618bd6b 100644 --- a/filecreator/views.py +++ b/filecreator/views.py @@ -34,12 +34,20 @@ def get(self, request): session_key=user.tokens.file_token ).get_decoded()["file_path"] + file_name = os.path.basename(file_path).split('-pr-') + if len(file_name) >= 3: + transaction_id = "_%s" % file_name[1] + else: + transaction_id = '' + file_extension = os.path.splitext(file_path)[1] with open(file_path, 'rb') as f: - response = HttpResponse(f.read(), content_type="application/force-download") # noqa + response = HttpResponse(f.read(), + content_type="application/force-download") response['Content-Disposition'] = 'attachment; filename=%s' % smart_str( # noqa - 'ComBunqWebApp_%s%s' % (user, file_extension)) + 'ComBunqWebApp_%s%s%s' % (user, transaction_id, + file_extension)) try: return response # except Exception as e: diff --git a/keyrings/live/blackbox-admins.txt b/keyrings/live/blackbox-admins.txt new file mode 100644 index 0000000..c48076a --- /dev/null +++ b/keyrings/live/blackbox-admins.txt @@ -0,0 +1 @@ +OGKevin diff --git a/keyrings/live/blackbox-files.txt b/keyrings/live/blackbox-files.txt new file mode 100644 index 0000000..e6b5e04 --- /dev/null +++ b/keyrings/live/blackbox-files.txt @@ -0,0 +1,4 @@ +BunqWebApp/config.yaml +Caddyfile +uwsgi-develop.ini +uwsgi.ini diff --git a/keyrings/live/pubring.kbx b/keyrings/live/pubring.kbx new file mode 100644 index 0000000000000000000000000000000000000000..6e945fe9e3bb9dc8cdd739b26d1ababf603ea296 GIT binary patch literal 2505 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 literal 0 HcmV?d00001 diff --git a/keyrings/live/trustdb.gpg b/keyrings/live/trustdb.gpg new file mode 100644 index 0000000000000000000000000000000000000000..f0f2046af46d7cfcef13de40b4f421ec631c2ad1 GIT binary patch literal 1200 zcmZQfFGy!*W@Ke#Vql02lnY|O4j8$xi(`n6s>28pu)t`zjD`y+1V+;VW$6F_=a&PF literal 0 HcmV?d00001 diff --git a/package.json b/package.json index 5d4fa5f..aed7512 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,7 @@ "keywords": [ "bunq", "pyhton", - "django", - "heroku" + "django" ], "author": "OGKevin ", "license": "MIT", diff --git a/requirements.txt b/requirements.txt index fe5005a..0957d92 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ appdirs==1.4.3 +APScheduler==3.3.1 arrow==0.10.0 asn1crypto==0.22.0 Babel==2.4.0 @@ -13,7 +14,7 @@ coverage==4.4.1 cryptography==1.9 cssselect==1.0.1 dj-database-url==0.4.2 -Django==1.11.2 +Django==1.11.3 django-formtools==2.0 django-simple-captcha==0.5.5 django-simple-history==1.9.0 @@ -31,8 +32,8 @@ olefile==0.44 packaging==16.8 pbkdf2==1.3 pdfkit==0.6.1 -Pillow==4.1.1 -pip-upgrader==1.4.1 +Pillow==4.2.0 +pip-upgrader==1.4.2 psycopg2==2.7.1 pycparser==2.17 pycrypto==2.6.1 @@ -47,6 +48,7 @@ qrcode==5.3 raven==6.1.0 requests==2.18.1 requests-mock==1.3.0 +ruamel.yaml==0.15.16 shellescape==3.4.1 six==1.10.0 terminaltables==3.1.0 diff --git a/static/BunqAPI/CSS/my_bunq.css b/static/BunqAPI/CSS/my_bunq.css index 1c81858..d95d3d4 100644 --- a/static/BunqAPI/CSS/my_bunq.css +++ b/static/BunqAPI/CSS/my_bunq.css @@ -24,3 +24,6 @@ th, td { #user_payments{ visibility: hidden; } +#single_transaction{ + display: none; +} diff --git a/static/BunqAPI/JS/my_bunq.js b/static/BunqAPI/JS/my_bunq.js index 2440c47..d5ab3dc 100644 --- a/static/BunqAPI/JS/my_bunq.js +++ b/static/BunqAPI/JS/my_bunq.js @@ -1,118 +1,91 @@ -var dataTable; - $(function() { var jsonObj; - sendPost(jsonObj, "load_file", false) - - function get_file() { - data = $("#id_encrypted_file")[0].files[0] - var reader = new FileReader() - reader.readAsText(data) - reader.onload = function(event) { - jsonObj = JSON.parse(event.target.result); - } - } - $("#encryption_form").submit(function(event) { - /* Act on the event */ - event.preventDefault() - get_file() - deactivateItems() - $(this).addClass('active') - - - setTimeout(function() { - sendPost(jsonObj, "load_file", false) - - }, 500) - }); - - $("#load_file").click(function(event) { - // firstCall() - get_file() - deactivateItems() - $(this).addClass('active') - - - setTimeout(function() { - sendPost(jsonObj, "load_file", false) - - }, 500) - - }); + sendPost( "load_file", false) + + // $("#encryption_form").submit(function(event) { + // /* Act on the event */ + // event.preventDefault() + // get_file() + // deactivateItems() + // $(this).addClass('active') + // + // + // setTimeout(function() { + // sendPost( "load_file", false) + // + // }, 500) + // }); + + // $("#load_file").click(function(event) { + // get_file() + // deactivateItems() + // $(this).addClass('active') + // + // + // setTimeout(function() { + // sendPost( "load_file", false) + // + // }, 500) + // + // }); $('#register').click(function(event) { deactivateItems() $(this).addClass('active') - sendPost(jsonObj, $(this)[0].id, register_template) + sendPost( $(this)[0].id, register_template) }); $('#start_session').click(function(event) { deactivateItems() $(this).addClass('active') - sendPost(jsonObj, $(this)[0].id, start_session_template) + sendPost( $(this)[0].id, start_session_template) }); $("#users").click(function(event) { deactivateItems() $(this).addClass('active') - sendPost(jsonObj, $(this)[0].id + '/' + get_user_id() + '/', ussers_template) + // sendPost( $(this)[0].id + '/' + get_user_id() + '/', ussers_template) + sendPost( $(this)[0].id, ussers_template) }); $("#accounts").click(function(event) { deactivateItems() $(this).addClass('active') - sendPost(jsonObj, $(this)[0].id + '/' + get_user_id() + '/' + get_account_id(), accounts_template) + // sendPost( $(this)[0].id + '/' + get_user_id() + '/' + get_account_id(), accounts_template) + sendPost( $(this)[0].id + '/' + get_user_id(), accounts_template) }); - $("#payment").click(function(event) { - deactivateItems() - $(this).addClass('active') - sendPost(jsonObj, $(this)[0].id + '/' + get_user_id() + '/' + get_account_id(), payments_template) - - }); + // $("#payment").click(function(event) { + // deactivateItems() + // $(this).addClass('active') + // sendPost( $(this)[0].id + '/' + get_user_id() + '/' + get_account_id(), payments_template) + // + // }); $("#card").click(function(event) { deactivateItems() $(this).addClass('active') - sendPost(jsonObj, $(this)[0].id + '/' + get_user_id() + '/' + get_account_id(), card_template) + // sendPost( $(this)[0].id + '/' + get_user_id() + '/' + get_account_id(), card_template) + sendPost( $(this)[0].id + '/' + get_user_id(), card_template) }); $("#mastercard_action").click(function(event) {}); - $("#export_transactions").click(function(event) { - deactivateItems() - $(this).addClass('active') - pages = $("#pages").val() - if (dataTable) { - // getTableData() - json = $("#transaction_table").tableToJSON() - sendPost(json, 'filecreator/transactions/csv') - // curentPage = dataTable.currentPage - // if (pages) { - // - // dataTable.export('csv', 'Bunq-transactions', ';', '\r\n', [pages]) - // } else { - // dataTable.export('csv', 'Bunq-transactions', ';', '\r\n', curentPage) - // - // } - } else { - $("#loading").html('Table is not created yet?') - } - }); + // $("#export_transactions").click(function(event) { + // deactivateItems() + // $(this).addClass('active') + // pages = $("#pages").val() + // json = $("#transaction_table").tableToJSON() + // sendPost('filecreator/transactions/csv', null, json) + // }); $("#export_invoice").click(function(event) { deactivateItems() $(this).addClass('active') - sendPost(jsonObj, 'invoice/' + get_user_id()) + sendPost( 'invoice/' + get_user_id()) }); - $("#export_payment").click(function(event) { + $("#export_customer_statement").click(function(event) { /* Act on the event */ deactivateItems() $(this).addClass('active') - sendPost(jsonObj, 'get_payment_pdf/' + get_user_id() + '/' + get_account_id() + '/' + get_payment_id()) - }); - $("#export_payment2").click(function(event) { - /* Act on the event */ - deactivateItems() - $(this).addClass('active') - sendPost(jsonObj, 'customer_statement/' + get_user_id() + '/' + get_account_id() + '/' + get_format_type() + '/' + get_begin_date() + '/' + get_end_date() + '/' + 'european') + sendPost( 'customer_statement/' + get_user_id() + '/' + get_account_id() + '/' + get_format_type() + '/' + get_begin_date() + '/' + get_end_date() + '/' + 'european') }); }); @@ -139,7 +112,7 @@ function get_end_date() { function get_format_type() { return $("#format_type").val() } -function sendPost(json, action, template) { +function sendPost(action, template, data) { $('#loading').html('
') csrftoken = Cookies.get('csrftoken') @@ -152,9 +125,8 @@ function sendPost(json, action, template) { url: '/API/' + action, type: 'POST', dataType: '', - data: { - 'json': JSON.stringify(json), - 'pass': $('#id_encryption_password').val() + data : { + 'json': JSON.stringify(data) }, beforeSend: function(xhr, settings) { if (!csrfSafeMethod(settings.type) && !this.crossDomain) { @@ -172,7 +144,7 @@ function sendPost(json, action, template) { } else if (action.match(/load_file/)) { show(r.start_session, false, start_session_template, 'start_session') show(r.accounts, false, accounts_template, 'accounts') - createTable(r.payments) + createTable(r.payments, payments_template) } else if (action.match(/accounts/)) { @@ -186,7 +158,7 @@ function sendPost(json, action, template) { alert('File download failed!'); }); } else if (action.match(/payment/)) { - createTable(r.Response) + createTable(r.Response, template) } else if (action.match(/invoice/)) { $.fileDownload('/filecreator/download') .done(function() { @@ -239,6 +211,38 @@ function sendPost(json, action, template) { }); } +$(document).delegate('.table-click', 'click', function(event) { + /* Act on the event */ + payment_id = $(this).data("id") + ma_id = $(this).data("ma") + sendPost('payment' + '/' + get_user_id() + '/' + ma_id + '/' + payment_id, single_transaction_template) + setTimeout(function(){ + + $("#single_transaction").bPopup() + }, 1000) +}); + +$(document).delegate('#export_payment', 'click', function(event) { + payment_id = $(this).data("id") + ma_id = $(this).data("ma") + sendPost('get_payment_pdf/' + get_user_id() + '/' + ma_id + '/' + payment_id) +}); + +$(document).delegate('.search', 'keyup', function(event) { + var $rows = $('#table-trans tbody tr'); + var val = $.trim($(this).val()).replace(/ +/g, ' ').toLowerCase(); + + $rows.show().filter(function() { + var text = $(this).text().replace(/\s+/g, ' ').toLowerCase(); + return !~text.indexOf(val); + }).hide(); +}); + +$(document).delegate('.ma-table-click', 'click', function(event) { + ma_id = $(this).data('id') + sendPost('payment' + '/' + get_user_id() + '/' + ma_id, payments_template) +}) + function show(j, error, template, location) { if (error) { $("#user_accounts").html(j.error_description_translated) @@ -267,7 +271,7 @@ function show(j, error, template, location) { case "accounts": $("#user_accounts").html(rendered) $("#user_accounts").css('visibility', 'visible'); - $("#accountID").val(j[0].MonetaryAccountBank.id) + // $("#accountID").val(j[0].MonetaryAccountBank.id) break; case "users": $("#user_accounts").html(rendered) @@ -277,63 +281,33 @@ function show(j, error, template, location) { default: } - // $("#" + location).html(rendered) - // locatoin(rendered) }) } } -function createTable(input) { - - var rows = [], - headers = [ - 'Payment ID', - 'Account ID', - 'Date', - 'Amount', - // 'Account IBAN', - 'Payee IBAN', - 'Name', - 'Description', - // 'Type' - ] - numeral.locale("nl-nl") +function createTable(input, template) { + numeral.locale("nl-nl") for (var i = 0; i < input.length; i++) { - rows.push([ - input[i].Payment.id, - input[i].Payment.monetary_account_id, - moment(input[i].Payment.updated.slice(0, 10)).format("MMM Do YYYY"), - numeral(input[i].Payment.amount.value.replace(".", ",")).format('$0,0[.]00'), - // input[i].Payment.alias.iban, - input[i].Payment.counterparty_alias.iban, - input[i].Payment.counterparty_alias.label_user.public_nick_name, - input[i].Payment.description, - // input[i].Payment.type - ]) + input[i].Payment.updated = moment(input[i].Payment.updated.slice(0, 10)).format("MMM Do YYYY"), + input[i].Payment.amount.value = numeral(input[i].Payment.amount.value.replace(".", ",")).format('$0,0[.]00') } - options = { - data: { - 'headings': headers, - "rows": rows - } - } - if (dataTable) { - dataTable.destroy(); + data = { + 'rows': input } - dataTable = new DataTable("#transaction_table", options); - $("#transaction_table").css('visibility', 'visible'); - $("#user_payments").css('visibility', 'visible'); + arr = template.split("/") + $.get(template, function(template) { + rendered = Mustache.render(template, data) + if (arr[arr.length-1] == "payments.html"){ + $("#transaction_table").html(rendered) + $("#user_payments").css('visibility', 'visible'); + + }else{ + $("#single_transaction").html(rendered) + } + }) } function deactivateItems() { - $("#load_file, #register, #start_session, #users, #accounts, #lock_ids, #payment, #card, #mastercard_action, #export_transactions, #export_invoice").removeClass('active') -} - -function getTableData() { - table = dataTable - json = $("#transaction_table").tableToJSON() - // for (var i = 0; i < table.rows.length; i++) { - // data.push(table.rows[i].innerHTML) - // } + $("#load_file, #register, #start_session, #users, #accounts, #lock_ids, #payment, #card, #mastercard_action, #export_transactions, #export_customer_statement, #export_invoice").removeClass('active') } diff --git a/static/BunqAPI/templates/mustache/accounts.html b/static/BunqAPI/templates/mustache/accounts.html index 2939878..497eab3 100644 --- a/static/BunqAPI/templates/mustache/accounts.html +++ b/static/BunqAPI/templates/mustache/accounts.html @@ -11,6 +11,7 @@

Accounts

ID First alias Balance + Actions @@ -20,6 +21,7 @@

Accounts

{{id}} {{alias.0.value}} {{balance.currency}} {{balance.value}} + {{/MonetaryAccountBank}} {{/.}} diff --git a/static/BunqAPI/templates/mustache/payments.html b/static/BunqAPI/templates/mustache/payments.html index d3602be..3a9b080 100644 --- a/static/BunqAPI/templates/mustache/payments.html +++ b/static/BunqAPI/templates/mustache/payments.html @@ -1,32 +1,28 @@ - - -
-

Something else

-
- - - - - - - - - - - {{#.}} - - - - - - - - - {{/.}} +
+ + +
+
DateAmountAccount IBANPayee IBANNameDescription
{{Payment.updated}}{{Payment.amount.value}}{{Payment.alias.iban}}{{Payment.counterparty_alias.iban}}{{Payment.counterparty_alias.label_user.public_nick_name}}{{Payment.description}}
+ + + + + + + + + + + + {{#rows}} + + + + + + + + + {{/rows}} +
DateAmountPayee IBANNameDescriptionActions
{{Payment.updated}}{{Payment.amount.value}}{{Payment.counterparty_alias.iban}}{{Payment.counterparty_alias.display_name}}{{Payment.description}} |
diff --git a/static/BunqAPI/templates/mustache/single_transaction.html b/static/BunqAPI/templates/mustache/single_transaction.html new file mode 100644 index 0000000..68a1fb5 --- /dev/null +++ b/static/BunqAPI/templates/mustache/single_transaction.html @@ -0,0 +1,21 @@ + +
+ {{#rows}} + Payment ID : {{Payment.id}} +
+ Date : {{Payment.updated}} +
+ Amount : {{Payment.amount.value}} +
+ From : {{Payment.alias.display_name}} -- {{Payment.alias.iban}} +
+ To : {{Payment.counterparty_alias.display_name}} -- {{Payment.counterparty_alias.iban}} +
+ Description : {{Payment.description}} + {{/rows}} +
+
diff --git a/static/images/under-construction-gif-6.gif b/static/images/under-construction-gif-6.gif new file mode 100644 index 0000000000000000000000000000000000000000..27e9ba141880f84beffcd8f5ac15a6fb09ec020b GIT binary patch literal 5389 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 ro) @@ -99,14 +100,15 @@

Control center

-
- -
-
- -
+ +

The input boxes below are needed for 'Export transactions'

+
+ +
@@ -120,12 +122,12 @@

Control center

Restart session Users Accounts - Payment + Cards - Export transactions table + Export invoice - Export payment - Export transactions + + Export transactions
@@ -144,8 +146,10 @@

Control center

Transactions

- -
+ +
+
+ +
+ +
+ diff --git a/templates/Manager/index.html b/templates/Manager/index.html index 74cdd26..99e2cd2 100644 --- a/templates/Manager/index.html +++ b/templates/Manager/index.html @@ -1,4 +1,14 @@ + +{%load static%} +back + + + NOTE: this table needs to be hidden and unhidden when index.js + sends data to create it
- +

- + made with by the bunq API contributors team

+--> diff --git a/uwsgi-develop.ini.gpg b/uwsgi-develop.ini.gpg new file mode 100644 index 0000000000000000000000000000000000000000..eaa5d70e518492b5bffc53cade091bd294fdd808 GIT binary patch literal 762 zcmVdy8f;cds$;aXwrV)z zZ-FZm0s||3=k??Cw-ff_I=n2<$=21sixEXUwg=kD;eyLvIGkcWxg8Z2D;fyHiPxKT5Ga z{Do2`VyZ5{vA4bwZ+AP}!54DnPxJ@K;cx?DE=^O=7d7tG&1i1JsdV7mSfrcP;=Bfd zN-p-}iJAiA;SIPmz6S~N+J7S5kLn|7s)D{CU5HzlGlVY@i{lZ4~J(qxIreC6^t>kJ|4{fS?IzwDk#@lbuMa}?}HLZ z-fiqK2w$vP2#jJz-y!i*YOP}@>CEENCrnjBJg~gk>#vcuJg<3K&&wm{$M{-g2uN&D zM}89Zl|cY0&SzR{{A>el^j_|BXn|fD#c|B$^;H{1X^*v><_FN2Xazf zBTLYq2OsC_;ZlwRy6v2YDy)i$$X=RuAIB_707-Jahc$Tx&g4ejWn)b%mR3kL{Fpjb zoC-WxCs8a~7$R@yO-r0|SWTp=1d3B}69+8bvsfr=^T=_vxx3b-B<^Rdg3svi<;Sw{TvZM8?qZ+Yx-wx9z2(wN>H zjPHt0pp@l$K>QH-WS0RZ93$1rw1U=%>ZKUVYOqC40X&=bm}k{n2|2nndtMug9>lF0 zT#?2hPr1per)a7-AdV9F5kpCleQBT0^rIvMs;}RuA)Q+u z;Lav)&b-&)6WM30oENd&b_ccTvL-C0M#FB?=GyYe!hWy-*0nINH7Q2Wv1|B==~`l} z1)r)uNtq6B^Xgn(L*%bgd60}B5=I){ks;*|2Sfu$gP0BD2e^W9gIazafkH*=P z@MY#2ru`OLPBco7b-U8QA_3g1)2{qk+Nh=JX!HCxc_-1#gd28OO%>072JBc}E7lxW z`u})T&C#!D4#`Y@YdM95zh!&LX@R+zn}s$ohTB#@ m=C*QYkV)L?;DL2EtMRL