initial commit

This commit is contained in:
Lea 2024-08-26 21:05:34 +02:00
commit 8e00cbb079
11 changed files with 532 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
# created by virtualenv automatically
.venv
.idea
__pycache__
app-2.2.0.js

48
app.py Normal file
View 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
View 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

View 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]

View 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]

View 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
View 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

File diff suppressed because one or more lines are too long

38
templates/base.html Normal file
View 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
View 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
View 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 %}