From 6b057fb941c681d6b4f0fdc13c33ac1928c9b5a7 Mon Sep 17 00:00:00 2001 From: mkrieger Date: Thu, 14 Nov 2024 10:20:42 +0100 Subject: [PATCH] webcrawler v1.0 --- .gitignore | 162 +++++++++++++++ app/__init__.py | 14 +- app/__pycache__/__init__.cpython-310.pyc | Bin 1635 -> 1747 bytes app/__pycache__/models.cpython-310.pyc | Bin 1092 -> 1103 bytes app/__pycache__/routes.cpython-310.pyc | Bin 4637 -> 6876 bytes app/__pycache__/webcrawler.cpython-310.pyc | Bin 3691 -> 4116 bytes app/models.py | 9 +- app/routes.py | 97 ++++++++- app/static/styles.css | 132 ++++++++++++ app/templates/admin_panel.html | 50 +++++ app/templates/base.html | 38 +++- app/webcrawler.py | 190 +++++++++--------- instance/users.db | Bin 16384 -> 24576 bytes migrations/README | 1 + migrations/alembic.ini | 50 +++++ migrations/env.py | 113 +++++++++++ migrations/script.py.mako | 24 +++ ...1a25d_add_is_admin_column_to_user_model.py | 45 +++++ requirements.txt | 1 + 19 files changed, 814 insertions(+), 112 deletions(-) create mode 100644 .gitignore create mode 100644 app/templates/admin_panel.html create mode 100644 migrations/README create mode 100644 migrations/alembic.ini create mode 100644 migrations/env.py create mode 100644 migrations/script.py.mako create mode 100644 migrations/versions/10331d61a25d_add_is_admin_column_to_user_model.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..01e2db0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,162 @@ +### Flask ### +instance/* +!instance/.gitignore +.webassets-cache +.env + +### Flask.Python Stack ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# Uploads / Results +uploads/ +results/ diff --git a/app/__init__.py b/app/__init__.py index 23efef7..26974b4 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -2,20 +2,25 @@ import os from flask import Flask, redirect, url_for, request from flask_sqlalchemy import SQLAlchemy from flask_login import LoginManager, current_user -from .models import db, User +from flask_migrate import Migrate # Konfiguration für Upload- und Ergebnis-Ordner UPLOAD_FOLDER = '/app/uploads' RESULT_FOLDER = '/app/results' +db = SQLAlchemy() +migrate = Migrate() + def create_app(): app = Flask(__name__) app.config['SECRET_KEY'] = '008e7369b075886d5f494c8813efdfb17155da6af12b3fe8ee' app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db' app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER app.config['RESULT_FOLDER'] = RESULT_FOLDER + app.config['ALLOW_USER_SIGNUP'] = False db.init_app(app) + migrate.init_app(app, db) # Flask-Login Setup login_manager = LoginManager() @@ -24,16 +29,15 @@ def create_app(): @login_manager.user_loader def load_user(user_id): + from .models import User return User.query.get(int(user_id)) # Umleitung nicht authentifizierter Benutzer, statische Dateien und bestimmte Routen ausnehmen @app.before_request def require_login(): allowed_routes = ['auth.login', 'auth.signup'] - - # Prüfen, ob der Benutzer authentifiziert ist oder eine erlaubte Route anfragt - if (not current_user.is_authenticated - and request.endpoint not in allowed_routes + if (not current_user.is_authenticated + and request.endpoint not in allowed_routes and not request.path.startswith('/static/')): return redirect(url_for('auth.login')) diff --git a/app/__pycache__/__init__.cpython-310.pyc b/app/__pycache__/__init__.cpython-310.pyc index 41c64c991cb710c8b058301b4dfd03936b0857d5..229f85265cc2c13ee2cefdde39612ab3fcab4bad 100644 GIT binary patch delta 747 zcmYLGOK;Oa5Z>8cKjN$t=K%sy0YV62A&LN%Q$Zl{6m396RfR8B=(=fwlN8$#LUPnY zdgK-P4VqQt2In^mcv22 z)87KM%A-T^=7=UDUP2~8(vb9Eq{e_Qow9_d_{N!9B5V9s$+$+o5eJd0d3tXkgD{TD zXwJ1a!yr2IE@$l^$yukLxYRR-80dH8JR+m)vVB&yQauHAAX{N0u(L1hqkU1?9b_ie zA#3p4k4vavFFhIzlOT5Mk(xF)eZSx81-_r_TZ8^KerFNQkx)J=UwLMy-%0$|?rv`M z{{MCMgMYoRip4jOnP4r53ecI3I;RNDg1T7;!30QVi=>Qxo})%*;jsWCqAIsJC$;JaQ#m+|T1 z=HB%h>2WX#({N%^gFy8(A}?)HgZvXnW5PZp!Q%aX>lboyBG!VQuhrvXm@od^!yk4R zZ@(&QOI^VV5z6`;W=Qo42VR96w!*AI9yLy2jx$j~=95ps*-e%~7BVBK)&&a5ycVpo z;q*C25jDqV63rAs1~Y zms|FeYTFO;0Gqb1-k2*JJx^$9KdSq2gP8=^I-eew!y>t8f2&T%FquaV-1ns3=x|gH zo*f<8UhVT+$rYgJ$v?@zR`ADd(goUV>TrfhT=bY>}=Bj diff --git a/app/__pycache__/models.cpython-310.pyc b/app/__pycache__/models.cpython-310.pyc index 641a174829f8f8ccdc1790465e02fa48ae45e6a2..af1ee595d9916e31613d3cf69f200e529b1e4ca5 100644 GIT binary patch delta 590 zcmZuuyN=U96rJnm#IeN^Ld5bAyHXk#EZRjBNQi=BDNv%75EO1AlRIlo@FN;K+DAhH zEk)$7*n$tB-<}ptM;VDPpn*G1LBovZ=-zWYXJ(GC`F9>wQ4}y5N8g@i@j80&aY_v` zgq#(S0GQg!t-?wys%>;~yKoW**eixEdY>730)(@0KRJmM z2HksG3pJ7FvZRJ%+K5ILVgp;%!uNBw9^8RMs+=%D|_O8Usjt=Ep= z$)Hbru0Z=JPSaK1OyjUUfw8Mx;@fjLoM``LRplZrHJ9})#X^=>Ox-29gWrS4>2lev zqC%0^2g?iPk!lQT6A5QKFoGdX<+VTI`jJoMrN5#l97sG?A+@SUusI^o{)s42mYIqS z@3lXyy=ON}xntm_0b{KO25uP`66oN#QbK0sYjL4F@0mn(yFIk`c5S13t7cwSXGt(u zg7U&y+URy+KOFBH)iJmE_o!`k&zRpQ=(k_(-ON~JWfbF%k hJ25GA33Pk^XR7&g%4(I17v_(3A%!f25Qf&s`wPrGhIjw~ delta 619 zcmZuuzi-n(6u$G%i)+&~pwJGHK#GL26lfV35E2p!12h7)EM6ivcL@h)J3ZTiFwmiF zOpz!41Xlir?%i)?>c7y5_bvkqmhRJg-}~NAU$IM;lt~g1_#XX!vY4!s!TAX|oN%g0 zPE$(Jc9fI5scUUldAXN*ln8o3QvVg<9{0Z!?h9A=AA<`o4S@@I2wZ3xH)Wh1lTqgy z`;4fGj^Dj`p=KXMz5=iFz7qOKevt(@pv-Df%UsOXJ9~88K5*`@{|uoV6kzRu9)>vW zWE8chbm$p}pSC~fyL@TxlPoA-swKFw&)cx=Xd+cAU$@ z0I08F17qw?VG*S9gk=2D3w%Y_N5I;)$?u$Qp<39PO?6a-B@E6UGlG}8hCe45qi zwiXripW58^r`sD2Q0n3A{|>OR?m|TG0>tf=yY~&P$*5zxbCp$}rq!9sHorhSSYm5K vt8yU=?P4kpVRthA5uVYW&=&k(C&Qk>u^T0xQ^>3EG%<;3OateR)A#=Z03(y) diff --git a/app/__pycache__/routes.cpython-310.pyc b/app/__pycache__/routes.cpython-310.pyc index 38d5d360ba7bbc7faccf35154c5e1ed149ca04da..d3de45cd7b9053108bfcfc1c4af12719c91e1223 100644 GIT binary patch literal 6876 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 diff --git a/app/__pycache__/webcrawler.cpython-310.pyc b/app/__pycache__/webcrawler.cpython-310.pyc index b2349e94e3e41642bcbc215d0b7468c43e21fedd..3f9d11e670f0a0c8bfc7453c5a49375363b9ff85 100644 GIT binary patch literal 4116 zcmb7HO>Er873K`NB==9+)t@Cfu{Vj+U>jR<+y-$HBdroSiEKv}tR!yO1`BFuc14*> zu7V z>n+1ES{WxpaNh9EmgQK`&NwzRoh-AQ9MA7)PJ!7h`vc7>vK-69dyExW5#Hl$jE%#) z#K^iH|I2w{(`dLB+XP-!=BMfe{#lG{dmzbGPk93-z$2#ulNs*yLe7 zWPEWe3Ii~zG5w(royY`em3fkCC$;$cVL`vZ(8W+YwNi4uDCvOr@A zw4P&wz!sOHm)L$-ccm8wiSD}+NYL!e0?TXZij}3cmCCDp)90enxcjN7?9&%2Rl2%* z$4V@it6jeMPGSqb-Qkf0eVSY*`EfBB1q6g`s_N`X27lDYrv# zLoIC7;KjlAvT7)sJEm}%2f#68ugygkm<4_z@<`Cc3QnD;ptY*RSJrT?=u3!Mmu|p9 zAu9sHlqRNuR828~?MWa`PU#rcMi3B>;?zkbl>_n^w30G9Io(Sw{Rmkj^q@fsloGtn zFQ|ns2WS7=FiQ7NjU4?oU^@~!jyU?~Fq_5>M!~uIZ5^V=lLyhmV3~vHVS;m?PNN4n z*Yd!*=yqsBeB7g|mBZFSv?v}p{qoT17$@?|=>_HP!*K$f%;1Yd?jBq1KlgvQn{nxS zexy9zKhX$9%axL|T6;4LcrEC(Hn=zq=O^+cuj+}dY+3VIVm3VPv*@e5otSByc4od> z-oC!Nxa3$vsR|Yf50&LqnJ>X2on2W@VR46wuY(6tGK2_1%gGNn^AJ3$$oyY1l5S}D zuC}LrL~aEHBR}2M zr4b_n^d{sInb|UTt(fc)%p1`uX)}TcIg2?14!)u7=$K`}yEM+!vNE@o-!1HEAgc%= z1Of}CYj_qBG6`8(AcPAhlW>tSo(QzCdj_1@!>3}b<`sIUuLG>pN`bAlevvDrY z$AuX5e@j)pbXIe*#ejk_cEk50D z$fFEo$L+gvdUqzC+yh(06R@ipb_}#X#%A{j zn*+`%H98NznU1H$Jvkds?~s77eW`}4w9?t?==D^=`>?C-+0v>97Xual}A83-sgzYz3u*(9UF*Bz2ro7`nwL}>z1xg&d_$pijgTfB4?r5Zr! z2Qym#WE*w>rBtoq`F!C5RzrUD(!v2Z#7BMYit`!PPU}**cYnc?-0N;9f-u_RqH_+$ zUs-?q%(5%FSAptw^X{*FSqZ%QrmVQW&nrPFyF6eXtY2AgyS@Z-Ip(Vnp*a)J!RJW^ zQY4d(`7-Z&;)Xa6W8!(7Fr7~1%Kkp7^y%qJVgzoBJN8Yl?ggIgrNQzE@z^VidC;b3`&)q1UO zEYzc$3F-E;9oaZ@{!HXG!J|3l0@O2dvM9^vka?<=BoB2PRD@7uyeAVHLS4~zeU)t1;BPc7v-@{+9Th_D+a>3Y9t(cBOxqHY9C2Mocn8ZJHQZAPbThy*1~sWo K?Qy$YDE|jd&ofW} literal 3691 zcmb7HOK%*<5uVr1&OW((h*%c|pIb;W{sqU_> zuCA*2s@N`-@)~|0eEvrBFUK_Pb2T{nGcfo8{-T>8L?e2j)zY_K)044LGl)UVz-(DH ztCgu`bhT~;b}L)Ug3r_(V%KscTgwxN^#`SGOn8G3!$$Vn=Y+-Mk#NIL3&xPH$_O)J?XGm5U(Bp_K^jHscT@%^9 zL2^xP-&oK-yxDVvgE8*CZ}f5rU)F-D5t^>Wzv~@Z*WbV!}@(d`aX;VaH@2i{jv&>DwYF3Zf`V zSk>vJj9~=(Xs9d3|~8upU6?jv^>r!se6PV-J+_&DiBXd z{MP?rJE7e3gD5dPo;uXq*=mQh9(G$BlwI7V8t zy)rx8Y)R`n?jp1EsmU_4qRx=n^;CO&_A1>9D04Tc-*V?SSa*|#F2TgD)#W)i9&xAL zHSY0l)Myhr=XTYXG9PX-uSqx4otvNifw|Qjj#yc$Ut7E_9Yzs}h)2?FQZ5TovcIkr zNK3^^W;)CdxwP>G(~!tKz1N@}?jszv%4)iKaX3{Smv+P%g(j!r^SyfP4Mpaq_VXbA7=&=zcpuEV&KrpL(zyNFtb<$YWp{Q0K4$`!#RfglCT7c6) zT&b5j{jJ8TP;)Se%W*n)&A^JG+s`VQJS2*EDwtPu>T$j_?i+uzUODllWsN*1bweUPJ6|Uo3HbthM$l1o%64QX}KQ z(LaNqIOO}sl5>+Y5-!nyXxX7v33p7LP9`5`6zU4AKPg5M{QdQvTSspfI+wKQue*r{ zbnQ0$07OQ;dSdh{gBTtc6@~iA=mbB$uRqeG95?zW6`#Z%vqOK!H-z;_XV(z3QF0Pd zJB6rO4^JggJ0TOK8DS}1FIbklgv^RwKWZC%cvg%K&mvPQ zPecXu>|wTqMM#}fF(vMcd9<#D=TTdx9_f2AD$6)3%Q48lBF3L<{I4BJjT!%FJS?HY z;H@gmQ7cuxzC2Fset6VwB~?9ty?X{94-k-X>aMVM4A^%Q#6vSOy|e&`oCW9Zb{V0r zx6z~dEPTI9@5>B6Su!smT9222RQ+HBuGtD4rgk0Hmfq;;`8c;Bp-0 z!g(FVP{!RZLc-?UTJD{8s}oRE_jv6ZMF3nz1A5oPa`@a>bPI8}<*p5Ohb|(#<-zaa zjQgI)c6`bxif8C5=X*iqUbu=nPsxRunw@fS^=Q3aYiz-yjX}kI!QeT!dQKKlR8Tiz z!#&Q<;quaGN7Cwed<&Jd?T4~-Yi0Sy{6hWejpc>KRc0v%aN+au|;yd6q>7Nxq^nMz$**j4YX^q^caVC~&Vj-(k<-&EPF z`jy;a{hyG|^J#m#y`g$BQ}zDIwDLt-`%SFXvbc?sRvl7$n?!$}x49S86F%c3VE3en z0XvTyuvbB9BZH2jKFC$33TLfw#4b4;B<{CU%Z%vy?Jzx+pI@oT?lAHt++ ziM@>%Q#P{GAkuE3OAFGBNwP_1HUlr(VXC2K-&15skyDDOW+Tya!~uyb5-&L4g|2Qu Zzpws', methods=['POST']) +@login_required +def reset_password(user_id): + if not current_user.is_admin: + flash("Keine Berechtigung.") + return redirect(url_for('auth.admin_panel')) + + user = User.query.get_or_404(user_id) + new_password = request.form['new_password'] + user.password = generate_password_hash(new_password, method='sha256') + db.session.commit() + + flash(f"Passwort für Benutzer {user.username} wurde zurückgesetzt.") + return redirect(url_for('auth.admin_panel')) + +@bp.route('/admin/delete_user/', methods=['POST']) +@login_required +def delete_user(user_id): + if not current_user.is_admin: + flash("Keine Berechtigung.") + return redirect(url_for('auth.admin_panel')) + + user = User.query.get_or_404(user_id) + if user.is_admin: + flash("Administratoren können nicht gelöscht werden.") + return redirect(url_for('auth.admin_panel')) + + db.session.delete(user) + db.session.commit() + flash(f"Benutzer {user.username} wurde gelöscht.") + return redirect(url_for('auth.admin_panel')) diff --git a/app/static/styles.css b/app/static/styles.css index 20a6174..718040b 100644 --- a/app/static/styles.css +++ b/app/static/styles.css @@ -164,3 +164,135 @@ tr:nth-child(even) td { .delete-btn:hover { background-color: #e60000; } + +/* Flash-Badge Styling */ +.flash-badge { + position: fixed; + top: 20px; + right: 20px; + background-color: #f44336; /* Material Design Rot */ + color: white; + padding: 12px 24px; + border-radius: 8px; + font-family: 'Roboto', sans-serif; + font-weight: 500; + box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.2); + z-index: 1000; + opacity: 0; + transform: translateY(-20px); + transition: opacity 0.4s ease, transform 0.4s ease; +} + +/* Einblend-Animation */ +.flash-badge.show { + opacity: 1; + transform: translateY(0); +} + +/* Ausblend-Animation */ +.flash-badge.hide { + opacity: 0; + transform: translateY(-20px); +} + +.admin-panel { + max-width: 800px; + margin: 2em auto; + padding: 2em; + background: white; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +.admin-panel h2 { + font-weight: 500; + color: #1d1d1f; + margin-bottom: 1em; +} + +.user-table { + width: 100%; + border-collapse: collapse; + margin-bottom: 2em; +} + +.user-table th, .user-table td { + padding: 0.75em; + text-align: left; + border: 1px solid #d1d1d6; +} + +.user-table th { + background-color: #f1f1f1; + color: #333; +} + +.user-table td { + background-color: white; +} + +.user-table tr:nth-child(even) td { + background-color: #f9f9f9; +} + +.reset-btn, .delete-btn, .create-btn { + padding: 0.5em 1em; + font-size: 0.9em; + font-weight: 500; + border: none; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.2s ease-in-out; +} + +.reset-btn { + background-color: #4caf50; + color: white; +} + +.reset-btn:hover { + background-color: #388e3c; +} + +.delete-btn { + background-color: #f44336; + color: white; +} + +.delete-btn:hover { + background-color: #d32f2f; +} + +.create-btn { + background-color: #007aff; + color: white; + padding: 0.75em; + margin-top: 1em; + display: block; + width: 100%; + font-size: 1em; +} + +.create-btn:hover { + background-color: #005bb5; +} + +.create-user-form { + margin-top: 1.5em; +} + +.create-user-form input[type="text"], +.create-user-form input[type="password"] { + width: 100%; + padding: 0.75em; + margin-bottom: 1em; + border: 1px solid #d1d1d6; + border-radius: 8px; +} + +.create-user-form label { + font-size: 0.9em; + color: #6e6e73; + display: block; + margin-bottom: 1em; +} diff --git a/app/templates/admin_panel.html b/app/templates/admin_panel.html new file mode 100644 index 0000000..1199858 --- /dev/null +++ b/app/templates/admin_panel.html @@ -0,0 +1,50 @@ +{% extends "base.html" %} + +{% block content %} +
+

Benutzerverwaltung

+ + + + + + + + + + + + + {% for user in users %} + + + + + + + {% endfor %} + +
IDBenutzernameAdminAktionen
{{ user.id }}{{ user.username }}{{ 'Ja' if user.is_admin else 'Nein' }} +
+ + +
+ {% if not user.is_admin %} +
+ +
+ {% endif %} +
+ + +

Neuen Benutzer erstellen

+
+ + + + +
+
+{% endblock %} diff --git a/app/templates/base.html b/app/templates/base.html index 1f06c06..f1f1aa7 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -14,14 +14,50 @@ {% endif %} - + + + {% with messages = get_flashed_messages() %} + {% if messages %} +
+ {% for message in messages %} +
{{ message }}
+ {% endfor %} +
+ {% endif %} + {% endwith %} +
{% block content %}{% endblock %}
+ + + diff --git a/app/webcrawler.py b/app/webcrawler.py index c4b4aff..f73f061 100644 --- a/app/webcrawler.py +++ b/app/webcrawler.py @@ -6,123 +6,133 @@ from flask import current_app UPLOAD_FOLDER = 'uploads' RESULT_FOLDER = 'results' + API_KEY = 'AIzaSyAIf0yXJTwo87VMWLBtq2m2LqE-OaPGbzw' -def get_place_details(street, city_zip): - address = f"{street}, {city_zip}" - url = f"https://maps.googleapis.com/maps/api/place/textsearch/json" - params = {'query': address, 'key': API_KEY} +processed_companies = set() + +def get_geocode(address): + url = f"https://maps.googleapis.com/maps/api/geocode/json" + params = {'address': address, 'key': API_KEY} - results = [] try: response = requests.get(url, params=params, timeout=5) if response.status_code == 200: data = response.json() - print(f"API Response Data for {address}: {data}") + if data['status'] == 'OK': + location = data['results'][0]['geometry']['location'] + return location['lat'], location['lng'] + except requests.RequestException as e: + print(f"Geocode API Fehler für {address}: {e}") + return None, None - for place in data.get('results', []): - name = place.get('name', 'N/A') - place_id = place.get('place_id') - formatted_address = place.get('formatted_address', 'N/A') +def get_nearby_places(lat, lng): + places_url = f"https://maps.googleapis.com/maps/api/place/nearbysearch/json" + params = { + 'location': f"{lat},{lng}", + 'radius': 10, + 'type': 'point_of_interest', + 'key': API_KEY + } - # Zweite Anfrage für detailliertere Informationen - phone, website = 'N/A', 'N/A' - if place_id: - details_url = f"https://maps.googleapis.com/maps/api/place/details/json" - details_params = { - 'place_id': place_id, - 'fields': 'formatted_phone_number,website', - 'key': API_KEY - } - details_response = requests.get(details_url, params=details_params, timeout=5) - if details_response.status_code == 200: - details_data = details_response.json().get('result', {}) - phone = details_data.get('formatted_phone_number', 'N/A') - website = details_data.get('website', 'N/A') + try: + response = requests.get(places_url, params=params, timeout=5) + if response.status_code == 200: + return response.json().get('results', []) + except requests.RequestException as e: + print(f"Nearby Places API Fehler für Standort {lat},{lng}: {e}") + return [] - # Speichern nur, wenn Name und Telefonnummer vorhanden sind - if name != 'N/A' and phone != 'N/A': - results.append({ - 'Name': name, - 'Address': formatted_address, - 'Phone': phone, - 'Website': website - }) - else: - print(f"Fehler beim Abrufen der URL: {url} - Statuscode: {response.status_code}") - except requests.exceptions.RequestException as e: - print(f"Anfragefehler für {url}: {e}") +def get_place_details(place_id): + details_url = f"https://maps.googleapis.com/maps/api/place/details/json" + params = { + 'place_id': place_id, + 'fields': 'formatted_phone_number,website', + 'key': API_KEY + } - return results + try: + response = requests.get(details_url, params=params, timeout=5) + if response.status_code == 200: + result = response.json().get('result', {}) + return result.get('formatted_phone_number', 'N/A'), result.get('website', 'N/A') + except requests.RequestException as e: + print(f"Place Details API Fehler für Place ID {place_id}: {e}") + return 'N/A', 'N/A' def process_file(filename, job_id, app): with app.app_context(): - print(f"Starte Prozess für Job-ID: {job_id}") filepath = os.path.join(UPLOAD_FOLDER, filename) results = [] job = Job.query.get(job_id) if not job: - print("Job wurde abgebrochen, bevor er starten konnte.") + print("Job wurde abgebrochen.") return job.status = "In Progress" db.session.commit() with open(filepath, newline='', encoding='ISO-8859-1') as csvfile: reader = csv.DictReader(csvfile, delimiter=';') - rows = list(reader) - total_rows = len(rows) - print(f"Insgesamt zu verarbeitende Zeilen: {total_rows}") + headers = reader.fieldnames - for index, row in enumerate(rows): - # Job-Verfügbarkeit erneut prüfen - job = Job.query.get(job_id) - if not job: - print("Job wurde abgebrochen.") - return - - # Vollständige Adresse erstellen - street = f"{row.get('Straße', '')} {row.get('Hausnummer', '')}".strip() - city_zip = f"{row.get('PLZ', '')} {row.get('Stadt', '')}".strip() - print(f"Verarbeite Adresse: {street}, {city_zip}") - address_results = get_place_details(street, city_zip) - - for result in address_results: - # Ergebnisse nur speichern, wenn Name und Telefonnummer vorhanden sind - if result['Name'] != 'N/A' and result['Phone'] != 'N/A': - result.update({ - 'PLZ': row.get('PLZ', ''), - 'Stadt': row.get('Stadt', ''), - 'Straße': row.get('Straße', ''), - 'Hausnummer': row.get('Hausnummer', ''), - 'Zusatz': row.get('Zusatz', '') - }) - results.append(result) - - # Results-Dateiname basierend auf dem Upload-Dateinamen - result_file = f"results_{filename}" - result_path = os.path.join(RESULT_FOLDER, result_file) - - # Prüfen und erstellen des Ergebnisverzeichnisses - if not os.path.exists(RESULT_FOLDER): - os.makedirs(RESULT_FOLDER) - print(f"Erstelle Ergebnisverzeichnis: {RESULT_FOLDER}") - - try: - if results: # Nur speichern, wenn Ergebnisse vorhanden sind - with open(result_path, 'w', newline='', encoding='utf-8-sig') as csvfile: - writer = csv.DictWriter(csvfile, fieldnames=['Name', 'Address', 'Phone', 'Website', 'PLZ', 'Stadt', 'Straße', 'Hausnummer', 'Zusatz']) - writer.writeheader() - writer.writerows(results) - print(f"Ergebnisdatei erfolgreich gespeichert unter: {result_path}") - job.status = "Completed" - job.result_filename = result_file - db.session.commit() - else: - print("Keine relevanten Ergebnisse zum Speichern vorhanden. Markiere den Job als 'Failed'.") + if not all(field in headers for field in ['PLZ', 'Straße', 'Hausnummer']): + print("CSV-Datei enthält nicht alle notwendigen Spalten.") job.status = "Failed" db.session.commit() - except Exception as e: - print(f"Fehler beim Schreiben der Ergebnisdatei: {e}") + return + + for row in reader: + plz = row.get('PLZ', '').strip() + city = row.get('Stadt', row.get('Bezirk', '')).strip() + street = row.get('Straße', '').strip() + house_number = row.get('Hausnummer', '').strip() + additional = row.get('Zusatz', '').strip() + + if not all([plz, city, street, house_number]): + continue + + full_address = f"{street} {house_number} {additional}, {plz} {city}" + lat, lng = get_geocode(full_address) + if lat is None or lng is None: + continue + + nearby_places = get_nearby_places(lat, lng) + for place in nearby_places: + company_name = place['name'] + if company_name in processed_companies: + continue + + processed_companies.add(company_name) + company_address = place.get('vicinity', 'N/A').split(',')[0] + place_id = place.get('place_id') + company_phone, company_website = get_place_details(place_id) if place_id else ('N/A', 'N/A') + + results.append({ + 'PLZ': plz, + 'Stadt': city, + 'Straße': street, + 'Hausnummer': house_number, + 'Zusatz': additional, + 'Company Name': company_name, + 'Company Address': company_address, + 'Company Phone': company_phone, + 'Company Website': company_website + }) + + if results: + result_file = f"results_{os.path.splitext(filename)[0]}.csv" + result_path = os.path.join(RESULT_FOLDER, result_file) + with open(result_path, 'w', newline='', encoding='utf-8-sig') as csvfile: + writer = csv.DictWriter(csvfile, fieldnames=[ + 'PLZ', 'Stadt', 'Straße', 'Hausnummer', 'Zusatz', + 'Company Name', 'Company Address', 'Company Phone', 'Company Website' + ]) + writer.writeheader() + writer.writerows(results) + job.status = "Completed" + job.result_filename = result_file + db.session.commit() + else: job.status = "Failed" db.session.commit() diff --git a/instance/users.db b/instance/users.db index 5f9b0347167514d476389843ca9299275c96a68d..b6df26fa69329eb864e1b27643e2926fd9151da8 100644 GIT binary patch delta 853 zcmZ`$OHUI~6u!5GzMxzP)|wE~S%7INka^sBk&rl)28gAVQdAaXa_5C3(-u1|8W%$G z4@kPezu?lab>TlSE_7!?;*t%CMqPRfNJtY;?n&->ednC-92~%dkMPad$SD9IMD?G9 zkEVs>DVQ~3d?*3>2U8$E75|C`(Wk-G`}Vtm+lP@zI5|2BUq5dW!?T~(4f=we?9i|M zOkX&e&BDXzMTB^E)o@LH&u;i`z1Cy)=gN7ll4mN~LLtxe1etU!dR3;^T27jy= zu}Bn0u@x1|ri>lT6WL-dqNt+DswSO*7_!O z!L6gVw7(6*0BR|1EudKq+xI=!r>rlwT;FIlwsn5i^!IZ0>aJ%uZHs4lJi~D_9A;Q` zj^pNdb(UvYLB_PS0X@FDMlWCSaH3z(8>Pnu*JS&jC6= k8DYMoaau711Cx|jfwLrzp2O;5LUf%nN=+1;jNVN73+n5+@*I;Cte34&Gm>Fm? z2gr0@AZFyB$-qBzv!Fpge|;(=D}!vKZEki^W@>tBQE^71k*S%AiJNyGa>5(|awCHP0|WmQ d{yd=3 + return current_app.extensions['migrate'].db.engine + + +def get_engine_url(): + try: + return get_engine().url.render_as_string(hide_password=False).replace( + '%', '%%') + except AttributeError: + return str(get_engine().url).replace('%', '%%') + + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option('sqlalchemy.url', get_engine_url()) +target_db = current_app.extensions['migrate'].db + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def get_metadata(): + if hasattr(target_db, 'metadatas'): + return target_db.metadatas[None] + return target_db.metadata + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=get_metadata(), literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + conf_args = current_app.extensions['migrate'].configure_args + if conf_args.get("process_revision_directives") is None: + conf_args["process_revision_directives"] = process_revision_directives + + connectable = get_engine() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=get_metadata(), + **conf_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/migrations/versions/10331d61a25d_add_is_admin_column_to_user_model.py b/migrations/versions/10331d61a25d_add_is_admin_column_to_user_model.py new file mode 100644 index 0000000..0245f0b --- /dev/null +++ b/migrations/versions/10331d61a25d_add_is_admin_column_to_user_model.py @@ -0,0 +1,45 @@ +"""Add is_admin column to User model + +Revision ID: 10331d61a25d +Revises: +Create Date: 2024-11-14 08:36:27.125841 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '10331d61a25d' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('user') + op.drop_table('job') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('job', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('user_id', sa.INTEGER(), nullable=False), + sa.Column('filename', sa.VARCHAR(length=150), nullable=False), + sa.Column('status', sa.VARCHAR(length=50), nullable=True), + sa.Column('created_at', sa.DATETIME(), nullable=True), + sa.Column('result_filename', sa.VARCHAR(length=150), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('user', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('username', sa.VARCHAR(length=150), nullable=False), + sa.Column('password', sa.VARCHAR(length=150), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('username') + ) + # ### end Alembic commands ### diff --git a/requirements.txt b/requirements.txt index 979acdc..a1aef01 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ Werkzeug==2.2.2 pandas requests beautifulsoup4 +Flask-Migrate