initial commit
This commit is contained in:
commit
8e00cbb079
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
# created by virtualenv automatically
|
||||
.venv
|
||||
.idea
|
||||
__pycache__
|
||||
app-2.2.0.js
|
48
app.py
Normal file
48
app.py
Normal file
@ -0,0 +1,48 @@
|
||||
import locale
|
||||
|
||||
from flask import Flask, render_template, request
|
||||
|
||||
from forms.SearchForm import SearchForm
|
||||
from arztapi.APIHandler import APIHandler
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = "nosecretkey"
|
||||
locale.setlocale(locale.LC_ALL, 'de_DE.UTF-8')
|
||||
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def main_page():
|
||||
search_form = SearchForm()
|
||||
return render_template("index.html", form=search_form)
|
||||
|
||||
|
||||
@app.route("/search", methods=["POST"])
|
||||
def search_for_doctors_with_params():
|
||||
search_form = SearchForm(request.form)
|
||||
if search_form.validate():
|
||||
location = search_form.location.data
|
||||
therapy_types = search_form.therapy_type.data
|
||||
therapy_age_range = search_form.therapy_age_range.data
|
||||
therapy_setting = search_form.therapy_setting.data
|
||||
|
||||
print(f"Location: {location}")
|
||||
print(f"Therapy Types: {therapy_types}")
|
||||
print(f"Age Range: {therapy_age_range}")
|
||||
print(f"Therapy Setting: {therapy_setting}")
|
||||
|
||||
else:
|
||||
print(search_form.errors)
|
||||
|
||||
api_handler = APIHandler()
|
||||
locations = api_handler.get_lat_lon_location_list(location)[0]
|
||||
base64_value = api_handler.calculate_req_value_base64(float(locations["lat"]), float(locations["lon"]))
|
||||
api_handler.get_list_of_doctors(float(locations['lat']), float(locations['lon']), base64_value, therapy_types, therapy_age_range, therapy_setting)
|
||||
api_handler.get_general_doctor_information()
|
||||
sorted_doctor_phone_times = api_handler.get_doctor_phone_times_sorted()
|
||||
|
||||
return render_template("result.html", doctors=sorted_doctor_phone_times)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run()
|
199
arztapi/APIHandler.py
Normal file
199
arztapi/APIHandler.py
Normal file
@ -0,0 +1,199 @@
|
||||
from typing import List
|
||||
|
||||
import requests
|
||||
from requests import JSONDecodeError
|
||||
import base64
|
||||
from datetime import datetime
|
||||
from arztapi.ArztPraxisDatas import ArztPraxisDatas
|
||||
from arztapi.DoctorInformation import DoctorInformation, PhoneTime
|
||||
from arztapi.DoctorPhoneTime import DoctorPhoneTime
|
||||
|
||||
|
||||
class APIHandler:
|
||||
def __init__(self):
|
||||
self.base_api_url = "https://arztsuche.116117.de/api/"
|
||||
self.phone_times = []
|
||||
self.general_information = []
|
||||
|
||||
def get_lat_lon_location_list(self, location):
|
||||
api_path = self.base_api_url + "location"
|
||||
headers = {
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"Accept-Language": "de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7",
|
||||
"Authorization": "Basic YmRwczpma3I0OTNtdmdfZg==",
|
||||
"Connection": "keep-alive",
|
||||
}
|
||||
|
||||
params = {
|
||||
"loc": location,
|
||||
}
|
||||
|
||||
response = requests.get(api_path, params=params, headers=headers)
|
||||
|
||||
try:
|
||||
return response.json()
|
||||
|
||||
except JSONDecodeError:
|
||||
print("foo")
|
||||
print(response.text)
|
||||
|
||||
def get_list_of_doctors(self, lat, lon, req_val_base64, therapy_types, therapy_age, therapy_setting) -> ArztPraxisDatas:
|
||||
api_path = self.base_api_url + "data"
|
||||
headers = {
|
||||
"Accept": "application/json",
|
||||
"Accept-Language": "de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7",
|
||||
"Authorization": "Basic YmRwczpma3I0OTNtdmdfZg==",
|
||||
"Connection": "keep-alive",
|
||||
"req-val": req_val_base64,
|
||||
}
|
||||
|
||||
json_data = {
|
||||
# TODO: Find out what r means
|
||||
"r": 900,
|
||||
"lat": lat,
|
||||
"lon": lon,
|
||||
# TODO: mapping for filters
|
||||
"filterSelections": [
|
||||
{
|
||||
"title": "Fachgebiet Kategorie",
|
||||
"fieldName": "fgg",
|
||||
"selectedCodes": [
|
||||
"12",
|
||||
],
|
||||
},
|
||||
{
|
||||
"title": "Psychotherapie: Verfahren",
|
||||
"fieldName": "ptv",
|
||||
# TODO: filter for therapy types
|
||||
"selectedCodes": therapy_types,
|
||||
},
|
||||
{
|
||||
"title": "Psychotherapie: Altersgruppe",
|
||||
"fieldName": "pta",
|
||||
# TODO: filter for age
|
||||
"selectedCodes": [
|
||||
therapy_age
|
||||
],
|
||||
},
|
||||
{
|
||||
"title": "Psychotherapie: Setting",
|
||||
"fieldName": "pts",
|
||||
"selectedCodes": [
|
||||
therapy_setting
|
||||
],
|
||||
},
|
||||
],
|
||||
"locOrigin": "USER_INPUT",
|
||||
"initialSearch": False,
|
||||
"viaDeeplink": False,
|
||||
}
|
||||
|
||||
response = requests.post(api_path, headers=headers, json=json_data)
|
||||
response.raise_for_status()
|
||||
self.phone_times = ArztPraxisDatas(**response.json())
|
||||
return self.phone_times
|
||||
|
||||
def get_general_doctor_information(self) -> List[DoctorInformation]:
|
||||
general_doctor_information = []
|
||||
for data in self.phone_times.arztPraxisDatas:
|
||||
doctor_day_times = data.tsz
|
||||
phone_times = []
|
||||
for day in doctor_day_times:
|
||||
if day.tszDesTyps:
|
||||
for contact_times in day.tszDesTyps:
|
||||
if contact_times.typ == "Telefonische Erreichbarkeit":
|
||||
phone_times_day = contact_times.sprechzeiten
|
||||
for phone_time_day in phone_times_day:
|
||||
start_time_str, end_time_str = phone_time_day.zeit.split("-")
|
||||
start_date_time = self.parse_date_string(f"{day.d} {start_time_str}")
|
||||
end_date_time = self.parse_date_string(f"{day.d} {end_time_str}")
|
||||
current_phone_time_dict = {
|
||||
"start": start_date_time,
|
||||
"end": end_date_time
|
||||
}
|
||||
current_phone_time = PhoneTime(**current_phone_time_dict)
|
||||
phone_times.append(current_phone_time)
|
||||
|
||||
doctor_information_dict = {
|
||||
"name": data.name,
|
||||
"tel": data.tel,
|
||||
"fax": data.fax,
|
||||
"anrede": data.anrede,
|
||||
"email": data.email,
|
||||
"distance": data.distance,
|
||||
"strasse": data.strasse,
|
||||
"hausnummer": data.hausnummer,
|
||||
"plz": data.plz,
|
||||
"ort": data.ort,
|
||||
"telefonzeiten": phone_times
|
||||
}
|
||||
doctor_information = DoctorInformation(**doctor_information_dict)
|
||||
general_doctor_information.append(doctor_information)
|
||||
|
||||
self.general_information = general_doctor_information
|
||||
return self.general_information
|
||||
|
||||
def get_doctor_phone_times_sorted(self):
|
||||
doctor_phone_times = []
|
||||
for doctor_information in self.general_information:
|
||||
for phone_time in doctor_information.telefonzeiten:
|
||||
doctor_phone_time_dict = {
|
||||
"phone_time": phone_time,
|
||||
"doctor_name": doctor_information.name,
|
||||
"doctor_address": f"{doctor_information.plz} {doctor_information.ort} "
|
||||
f"{doctor_information.strasse} {doctor_information.hausnummer}",
|
||||
"doctor_phone_number": doctor_information.tel
|
||||
}
|
||||
doctor_phone_time = DoctorPhoneTime(**doctor_phone_time_dict)
|
||||
doctor_phone_times.append(doctor_phone_time)
|
||||
return sorted(doctor_phone_times, key=lambda dpt: dpt.phone_time.start)
|
||||
|
||||
@staticmethod
|
||||
def calculate_req_value_base64(lat, lon):
|
||||
"""
|
||||
This function is based on the initial Javascript code found in app.js.
|
||||
It is rewritten in Python to calculate the HTTP header req_val for proper requests with the correct location.
|
||||
:param lat:
|
||||
:param lon:
|
||||
:return:
|
||||
"""
|
||||
# Adjust lat and lon values slightly
|
||||
adjusted_lat = lat + 1.1
|
||||
adjusted_lon = lon + 2.3
|
||||
|
||||
# Get the current time in milliseconds since epoch
|
||||
current_time = datetime.now()
|
||||
timestamp_str = str(int(current_time.timestamp() * 1000)) # Convert to milliseconds
|
||||
|
||||
# Extract digits from latitude
|
||||
lat_integer_part = str(adjusted_lat).split(".")[0]
|
||||
lat_last_digit = lat_integer_part[-1]
|
||||
lat_first_fraction_digit = str(adjusted_lat).split(".")[1][0] if len(str(adjusted_lat).split(".")) > 1 else "0"
|
||||
|
||||
# Extract digits from longitude
|
||||
lon_integer_part = str(adjusted_lon).split(".")[0]
|
||||
lon_last_digit = lon_integer_part[-1]
|
||||
lon_first_fraction_digit = str(adjusted_lon).split(".")[1][0] if len(str(adjusted_lon).split(".")) > 1 else "0"
|
||||
|
||||
# Create the final string by combining digits
|
||||
combined_string = (
|
||||
lat_last_digit +
|
||||
timestamp_str[-1] +
|
||||
lon_last_digit +
|
||||
timestamp_str[-2] +
|
||||
lat_first_fraction_digit +
|
||||
timestamp_str[-3] +
|
||||
lon_first_fraction_digit
|
||||
)
|
||||
|
||||
# Encode the combined string in Base64
|
||||
encoded_value = base64.b64encode(combined_string.encode()).decode()
|
||||
|
||||
return encoded_value
|
||||
|
||||
@staticmethod
|
||||
def parse_date_string(date_string):
|
||||
format_string = "%d.%m. %H:%M"
|
||||
# TODO: handle turn of year
|
||||
parsed_date = datetime.strptime(date_string, format_string).replace(year=datetime.now().year)
|
||||
return parsed_date
|
51
arztapi/ArztPraxisDatas.py
Normal file
51
arztapi/ArztPraxisDatas.py
Normal file
@ -0,0 +1,51 @@
|
||||
from typing import List, Optional
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Sprechzeit(BaseModel):
|
||||
zeit: str
|
||||
freitext: Optional[str] = None
|
||||
|
||||
|
||||
class TszDesTyps(BaseModel):
|
||||
typ: str
|
||||
sprechzeiten: List[Sprechzeit]
|
||||
|
||||
|
||||
class Tsz(BaseModel):
|
||||
d: str
|
||||
t: str
|
||||
tszDesTyps: Optional[List[TszDesTyps]] = None
|
||||
|
||||
|
||||
class Ag(BaseModel):
|
||||
key: str
|
||||
value: str
|
||||
|
||||
|
||||
class ArztPraxisData(BaseModel):
|
||||
arzt: bool
|
||||
id: str
|
||||
web: Optional[str]
|
||||
kv: str
|
||||
name: str
|
||||
tel: str
|
||||
fax: Optional[str]
|
||||
anrede: str
|
||||
geschlecht: str
|
||||
handy: Optional[str]
|
||||
email: Optional[str] = None
|
||||
|
||||
distance: int
|
||||
strasse: str
|
||||
hausnummer: str
|
||||
plz: str
|
||||
ort: str
|
||||
geoeffnet: str
|
||||
keineSprechzeiten: bool
|
||||
ag: List[Ag]
|
||||
tsz: List[Tsz]
|
||||
|
||||
|
||||
class ArztPraxisDatas(BaseModel):
|
||||
arztPraxisDatas: List[ArztPraxisData]
|
23
arztapi/DoctorInformation.py
Normal file
23
arztapi/DoctorInformation.py
Normal file
@ -0,0 +1,23 @@
|
||||
import datetime
|
||||
from typing import Optional, List
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class PhoneTime(BaseModel):
|
||||
start: datetime.datetime
|
||||
end: datetime.datetime
|
||||
|
||||
|
||||
class DoctorInformation(BaseModel):
|
||||
name: str
|
||||
tel: str
|
||||
fax: Optional[str] = None
|
||||
anrede: str
|
||||
email: Optional[str] = None
|
||||
distance: int
|
||||
strasse: str
|
||||
hausnummer: str
|
||||
plz: str
|
||||
ort: str
|
||||
telefonzeiten: List[PhoneTime]
|
9
arztapi/DoctorPhoneTime.py
Normal file
9
arztapi/DoctorPhoneTime.py
Normal file
@ -0,0 +1,9 @@
|
||||
from pydantic import BaseModel
|
||||
from arztapi.DoctorInformation import PhoneTime
|
||||
|
||||
|
||||
class DoctorPhoneTime(BaseModel):
|
||||
phone_time: PhoneTime
|
||||
doctor_name: str
|
||||
doctor_address: str
|
||||
doctor_phone_number: str
|
28
forms/SearchForm.py
Normal file
28
forms/SearchForm.py
Normal file
@ -0,0 +1,28 @@
|
||||
from wtforms import Form, StringField, validators
|
||||
from wtforms.fields.choices import SelectMultipleField, RadioField
|
||||
from wtforms.widgets.core import CheckboxInput, ListWidget
|
||||
|
||||
|
||||
def validate_therapy_types(form, field):
|
||||
valid_choices = {"A", "S", "T", "V"}
|
||||
if not all(choice in valid_choices for choice in field.data):
|
||||
raise validators.ValidationError("Invalid value in therapy types selection.")
|
||||
|
||||
|
||||
class SearchForm(Form):
|
||||
location = StringField("Standort", validators=[validators.InputRequired(), validators.Length(min=2, max=50),
|
||||
validators.Regexp("^[a-zA-ZäöüßÄÖÜẞ]+$")])
|
||||
therapy_type = SelectMultipleField("Therapieverfahren", choices=[
|
||||
("A", "Analytische Psychotherapie"),
|
||||
("S", "Systemische Therapie"),
|
||||
("T", "Tiefenpsychologisch fundierte Psychotherapie"),
|
||||
("V", "Verhaltenstherapie")], option_widget=CheckboxInput(), widget=ListWidget(prefix_label=False),
|
||||
validators=[validate_therapy_types])
|
||||
therapy_age_range = RadioField("Altersgruppe", choices=[
|
||||
("E", "Erwachsene"),
|
||||
("K", "Kinder und Jugendliche")], validators=[validators.InputRequired(), validators.any_of(["E", "K"])])
|
||||
therapy_setting = RadioField("Therapiesetting", choices=[
|
||||
("E", "Einzeltherapie"),
|
||||
("G", "Gruppentherapie")], validators=[validators.InputRequired(), validators.any_of(["E", "G"])])
|
||||
|
||||
|
12
static/css/bootstrap.min.css
vendored
Normal file
12
static/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
38
templates/base.html
Normal file
38
templates/base.html
Normal file
@ -0,0 +1,38 @@
|
||||
<!doctype html>
|
||||
<title>{% block title %}{% endblock %} - TheraPy jetzt</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
|
||||
|
||||
<body>
|
||||
|
||||
<nav class="navbar navbar-expand-lg bg-primary" data-bs-theme="dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#">TheraPy</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarColor01" aria-controls="navbarColor01" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarColor01">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="#">Home
|
||||
<span class="visually-hidden">(current)</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">About</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<section class="content">
|
||||
<header>
|
||||
{% block header %}{% endblock %}
|
||||
</header>
|
||||
{% for message in get_flashed_messages() %}
|
||||
<div class="flash">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% block content %}{% endblock %}
|
||||
</section>
|
||||
|
||||
</body>
|
85
templates/index.html
Normal file
85
templates/index.html
Normal file
@ -0,0 +1,85 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg-8 col-md-7 col-sm-6">
|
||||
<form method="POST" action="/search">
|
||||
{{ form.csrf_token }}
|
||||
<fieldset>
|
||||
<legend>Suche</legend>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="locationInput" class="form-label mt-4">{{ form.location.label }}</label>
|
||||
{{ form.location(class="form-control", id="locationInput", placeholder="Standort eingeben") }}
|
||||
{% if form.location.errors %}
|
||||
<div class="text-danger">
|
||||
{{ form.location.errors[0] }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<fieldset>
|
||||
<legend class="mt-4">{{ form.therapy_type.label }}</legend>
|
||||
{% for subfield in form.therapy_type %}
|
||||
<div class="form-check">
|
||||
{{ subfield(class="form-check-input") }}
|
||||
<label class="form-check-label" for="{{ subfield.id }}">
|
||||
{{ subfield.label.text }}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if form.therapy_type.errors %}
|
||||
<div class="text-danger">
|
||||
{{ form.therapy_type.errors[0] }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<fieldset>
|
||||
<legend class="mt-4">{{ form.therapy_age_range.label }}</legend>
|
||||
{% for subfield in form.therapy_age_range %}
|
||||
<div class="form-check">
|
||||
{{ subfield(class="form-check-input") }}
|
||||
<label class="form-check-label" for="{{ subfield.id }}">
|
||||
{{ subfield.label.text }}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if form.therapy_age_range.errors %}
|
||||
<div class="text-danger">
|
||||
{{ form.therapy_age_range.errors[0] }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<fieldset>
|
||||
<legend class="mt-4">{{ form.therapy_setting.label }}</legend>
|
||||
{% for subfield in form.therapy_setting %}
|
||||
<div class="form-check">
|
||||
{{ subfield(class="form-check-input") }}
|
||||
<label class="form-check-label" for="{{ subfield.id }}">
|
||||
{{ subfield.label.text }}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if form.therapy_setting.errors %}
|
||||
<div class="text-danger">
|
||||
{{ form.therapy_setting.errors[0] }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Suche</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
34
templates/result.html
Normal file
34
templates/result.html
Normal file
@ -0,0 +1,34 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg-8 col-md-7 col-sm-6">
|
||||
<h2>Suchergebnisse</h2>
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Nächster Telefontag</th>
|
||||
<th scope="col">Nächste Telefonzeit</th>
|
||||
<th scope="col">Telefonnummer</th>
|
||||
<th scope="col">Adresse</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for doctor in doctors %}
|
||||
<tr class="table-secondary">
|
||||
<th scope="row">{{ doctor.doctor_name }}</th>
|
||||
<td>{{ doctor.phone_time.start.strftime("%A %d.%m") }}</td>
|
||||
<td>{{ doctor.phone_time.start.strftime("%H:%M") }} bis {{ doctor.phone_time.end.strftime("%H:%M") }}</td>
|
||||
<td>{{ doctor.doctor_phone_number }}</td>
|
||||
<td>{{ doctor.doctor_address }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
Loading…
Reference in New Issue
Block a user