...
 
Commits (3)
......@@ -29,17 +29,20 @@ public class AlarmReceiver extends BroadcastReceiver {
}
// Setup new exact alarm in 1 minute
Intent alarm = new Intent(context, AlarmReceiver.class);
log.debug("{WORK_ID: " + work_id + "} - Setting new alarm.");
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(System.currentTimeMillis());
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 99, alarm, 0);
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
alarmManager.setExact(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis()+60000, pendingIntent);
//alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis()+2000, 60000, pendingIntent);
log.debug("Set alarm from AlarmReceiver");
log.debug("{WORK_ID: " + work_id + "} - DONE.");
try {
Intent alarm = new Intent(context, AlarmReceiver.class);
log.debug("{WORK_ID: " + work_id + "} - Setting new alarm.");
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(System.currentTimeMillis());
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 99, alarm, 0);
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
alarmManager.setExact(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis()+60000, pendingIntent);
log.debug("Set alarm from AlarmReceiver");
log.debug("{WORK_ID: " + work_id + "} - DONE.");
} catch (Exception ex) {
log.error("Error while setting new alarm within Alarm Receiver.", ex);
}
}
}
......@@ -66,7 +66,8 @@ public class GPSService extends Service {
int max_distance = sharedPreferences.getInt("radius", 0);
if (distance <= (float) max_distance) {
// throws java.lang.OutOfMemoryError
String date = String.valueOf(toLocal(Calendar.getInstance().getTime().getTime()));
// divide by 1000 since this are milliseconds, but we need seconds
String date = String.valueOf(toLocal(Calendar.getInstance().getTime().getTime())/1000);
String user_id = sharedPreferences.getString("user_id", "");
sendData(user_id, date, work_id);
showNotification();
......
......@@ -110,17 +110,26 @@ public class MainActivity extends Activity
log = GPSLogger.getLogger("MainActivity" );
sharedPreferences = getSharedPreferences("GPSService", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreferences.edit();
// when not activated setup default radius and show options_activity
if (!sharedPreferences.getBoolean("activated", false)) {
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putInt("radius", 50);
editor.apply();
Intent intent = new Intent(this, OptionsActivity.class);
startActivity(intent);
}
setupAlarm();
// TODO mark alarm set in shared preferences
// TODO add alarm each hour for checking that everything is running
try {
setupAlarm();
editor.putBoolean("alarm_set", true);
editor.apply();
} catch (Exception ex) {
log.error("Exception while setting alarm in MainActivity: ", ex);
}
initGMaps();
......
from flask_login import LoginManager
from flask_principal import Principal, Permission, RoleNeed
from flask_bcrypt import Bcrypt
from flask_debugtoolbar import DebugToolbarExtension
from flask import session, redirect, current_app, request, url_for
bcrypt = Bcrypt()
principals = Principal()
admin_permission = Permission(RoleNeed('admin'))
employees_permission = Permission(RoleNeed('mitarbeiter'))
hiwi_permission = Permission(RoleNeed('hiwi'))
default_permission = Permission(RoleNeed('default'))
login_manager = LoginManager()
login_manager.login_view = "login"
login_manager.session_protection = "strong"
login_manager.login_message = "Please login to access this page"
login_manager.login_message_category = "info"
@login_manager.user_loader
def load_user(user_id):
from server.models import User
return User.query.get(user_id)
# Debug Toolbar
toolbar = DebugToolbarExtension()
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, PasswordField, BooleanField, HiddenField, RadioField
from wtforms.validators import DataRequired, Length, ValidationError
from wtforms.fields.html5 import EmailField, DateField, TimeField, IntegerField
from wtforms_alchemy import fields
from datetime import date
from server.models import User
def old_pw_check(form, field):
user = User.query.filter_by(username=form.username.data).first()
if not user.check_password(field.data):
raise ValidationError('Altes Password stimmt nicht!')
def check_new_pw(form, field):
if form.password_old.data == field.data:
raise ValidationError('Passwörter dürfen nicht gleich sein!')
class DateForm(FlaskForm):
begin = StringField("Begin", [DataRequired()])
end = StringField("Ende", [DataRequired()])
class PWForm(FlaskForm):
username = HiddenField('Username', [DataRequired(), Length(max=255)])
password_old = PasswordField('altes Passwort', validators=[old_pw_check, DataRequired(message="Es muss ein Passwort angegeben werden.")])
password = PasswordField('Passwort', validators=[check_new_pw, DataRequired(message="Es muss ein Passwort angegeben werden.")])
class LoginForm(FlaskForm):
username = StringField('Username', [DataRequired(), Length(max=255)])
password = PasswordField('Password', [DataRequired()])
remember = BooleanField("Remember Me")
next = HiddenField('next')
def validate(self):
check_validate = super(LoginForm, self).validate()
# if our validators do not pass
if not check_validate:
return False
# Does our the exist
user = User.query.filter_by(username=self.username.data).first()
if not user:
self.username.errors.append('Invalid username or password')
return False
# Do the passwords match
if not user.check_password(self.password.data):
self.username.errors.append('Invalid username or password')
return False
return True
class RegisterForm(FlaskForm):
username = StringField('Username', validators=[DataRequired(), Length(max=256)])
password = PasswordField('Passwort', validators=[DataRequired(), Length(max=32)])
from flask_sqlalchemy import SQLAlchemy
from flask_login import AnonymousUserMixin, UserMixin
from flask import redirect, url_for, request, flash
from server.extensions import bcrypt
db = SQLAlchemy()
roles = db.Table(
'role_users',
db.Column('user_id', db.Integer, db.ForeignKey('user.id')),
db.Column('role_id', db.Integer, db.ForeignKey('role.id'))
)
class GPSWork(db.Model):
__tablename__ = "gps_work"
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.String(80), nullable=False)
datetime = db.Column(db.DateTime, unique=True, nullable=False)
def __repr__(self):
return '<GPSWork %r>' % self.id
########################################################################
class User(db.Model, UserMixin):
__tablename__ = "user"
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(256), nullable=False, unique=True)
password = db.Column(db.String(256), nullable=True)
removed = db.Column(db.Boolean, default=False)
roles = db.relationship(
'Role',
secondary=roles,
backref=db.backref('user', lazy='dynamic')
)
def has_roles(self, *args):
return set(args).issubset({role.name for role in self.roles})
def __init__(self, **kwargs):
super(User, self).__init__(**kwargs)
default = Role.query.filter_by(name="default").one()
self.roles.append(default)
def __repr__(self):
return f'{self.firstname} {self.lastname}'
def set_password(self, password):
self.password = bcrypt.generate_password_hash(password)
def check_password(self, password):
return bcrypt.check_password_hash(self.password, password)
def is_authenticated(self):
if isinstance(self, AnonymousUserMixin):
return False
else:
return True
def is_active(self):
return True
def is_anonymous(self):
if isinstance(self, AnonymousUserMixin):
return True
else:
return False
class Role(db.Model):
id = db.Column(db.Integer(), primary_key=True)
name = db.Column(db.String(80), unique=True)
description = db.Column(db.String(255))
def __init__(self, name):
self.name = name
def __repr__(self):
return '<Role {}>'.format(self.name)
from test_server import app
import sys
import os
sys.path.append("../")
from server.test_server import app
from server.models import db, Role
from server.extensions import bcrypt, login_manager, principals
from flask_login import current_user
from flask_principal import identity_loaded, UserNeed, RoleNeed
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///test.db'
app.config['SECRET_KEY'] = os.urandom(16)
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db.init_app(app)
with app.app_context():
#db.drop_all()
db.create_all()
if not Role.query.all():
r = Role('default')
db.session.add(r)
db.session.commit()
# init login extensions
bcrypt.init_app(app)
login_manager.init_app(app)
principals.init_app(app)
@identity_loaded.connect_via(app)
def on_identity_loaded(sender, identity):
# Set the identity user object
identity.user = current_user
# Add the UserNeed to the identity
if hasattr(current_user, 'id'):
identity.provides.add(UserNeed(current_user.id))
# Add each role to the identity
if hasattr(current_user, 'roles'):
for role in current_user.roles:
identity.provides.add(RoleNeed(role.name))
app.run()
{% macro render_field(field) -%}
<div class="form-group">
{{ field.label }}
{% if field.errors %}
{% for e in field.errors %}
<div class="alert alert-warning">
<strong>Fehler!</strong> {{ e }}
</div>
{% endfor %}
{% endif %}
{{ field(class_='form-control') }}
</div>
{%- endmacro %}
\ No newline at end of file
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}Home{% endblock %}</title>
<meta name="description" content="{% block description %}{% endblock %}">
<script
src="https://code.jquery.com/jquery-3.3.1.min.js"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
crossorigin="anonymous"></script>
<script
src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"
integrity="sha256-VazP97ZCwtekAsvgPBSUwPFKdrwD3unUfSGVYrahUqU="
crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css" type="text/css"/>
{% block header %}{% endblock %}
</head>
<body>
<div class="container">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}" id="alert" role="alert">
<button type="button" class="close" data-dismiss="alert"><span aria-hidden="true">&times;</span></button>
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block body %}
{% endblock %}
<!-- Footer -->
<footer class="page-footer font-small blue">
<!-- Copyright -->
<div class="footer-copyright text-center py-3">© 2019 Daniel Niecke
</div>
<!-- Copyright -->
</footer>
</div>
<script>
$(document).ready (function() {
$(".alert").fadeTo(2000, 500).slideUp(500, function(){
$(".alert").slideUp(500);
});
});
</script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/js/bootstrap.bundle.min.js"></script>
{% block js %}
{% endblock %}
</body>
</html>
\ No newline at end of file
{% extends "base.html" %}
{% block title %}Startseite{% endblock %}
{% block body %}
<h1>Arbeitszeiten</h1>
<div class="row">
<div class="col">
<form action="{{ url_for('home') }}" method="POST">
{{ form.hidden_tag() }}
<div class="form-group">
{{ form.begin.label }}
{% if form.begin.errors %}
{% for e in form.begin.errors %}
<div class="alert alert-warning">
<strong>Fehler!</strong> {{ e }}
</div>
{% endfor %}
{% endif %}
<div class="input-group date" id="beginDate" data-target-input="nearest">
<input id="begin" name="begin" type="text" class="form-control datetimepicker-input" data-target="#beginDate"/>
<div class="input-group-append" data-target="#beginDate" data-toggle="datetimepicker">
<div class="input-group-text"><i class="fa fa-calendar"></i></div>
</div>
</div>
</div>
</div>
<div class="col">
<div class="form-group">
{{ form.end.label }}
{% if form.end.errors %}
{% for e in form.end.errors %}
<div class="alert alert-warning">
<strong>Fehler!</strong> {{ e }}
</div>
{% endfor %}
{% endif %}
<div class="input-group date" id="endDate" data-target-input="nearest">
<input id="end" name="end" type="text" class="form-control datetimepicker-input" data-target="#endDate"/>
<div class="input-group-append" data-target="#endDate" data-toggle="datetimepicker">
<div class="input-group-text"><i class="fa fa-calendar"></i></div>
</div>
</div>
</div>
</div>
<div class="col">
<input class="btn btn-primary" id="submit" name="submit" type="submit" value="Daten abfragen">
</div>
</div>
{% if content %}
<div class="row">
<div class="col">
<table class="table">
<thead>
<tr>
<th scope="col">Anfang</th>
<th scope="col">Ende</th>
</tr>
</thead>
<tbody>
{% for c in content %}
<tr>
<td>{{ c.start }}</td>
<td>{{ c.end }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<div class="row">
<div class="col"></div>
<div class="col"></div>
<div class="col"><a href="{{ url_for('logout') }}" class="btn btn-primary">Logout</a></div>
</div>
{% endblock %}
{% block js %}
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.3/css/all.css" integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/" crossorigin="anonymous">
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.22.2/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.22.2/locale/de.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/tempusdominus-bootstrap-4/5.0.0-alpha14/js/tempusdominus-bootstrap-4.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tempusdominus-bootstrap-4/5.0.0-alpha14/css/tempusdominus-bootstrap-4.min.css" />
<script>
$.extend($.fn.datetimepicker.Constructor.Default, {
icons: {
time: 'far fa-clock',
date: 'far fa-calendar',
up: 'fas fa-arrow-up',
down: 'fas fa-arrow-down',
previous: 'fas fa-chevron-left',
next: 'fas fa-chevron-right',
today: 'far fa-calendar-check-o',
clear: 'far fa-trash',
close: 'far fa-times'
}
});
$(function () {
$('#beginDate').datetimepicker({
format: 'L',
});
});
$(function () {
$('#endDate').datetimepicker({
format: 'L',
});
});
</script>
{% endblock %}
\ No newline at end of file
{% extends "base.html" %}
{% block title %}Login{% endblock %}
{% block body %}
{% import "_formhelpers.jinja" as mc%}
<div class="row">
<div class="col"></div>
<div class="col">
<h1 class="text-center">Login</h1>
<form method="POST" action="{{ url_for('.login') }}">
{{ form.hidden_tag() }}
{{ mc.render_field(form.username) }}
{{ mc.render_field(form.password) }}
<div class="form-group text-center">
<input class="btn btn-primary" id="login" name="login" type="submit" value="Login">
</div>
</form>
</div>
<div class="col"></div>
</div>
{% endblock %}
\ No newline at end of file
{% extends "base.html" %}
{% block title %}Login{% endblock %}
{% block body %}
{% import "_formhelpers.jinja" as mc%}
<div class="row">
<div class="col"></div>
<div class="col">
<h1 class="text-center">Login</h1>
<form method="POST" action="{{ url_for('.register') }}">
{{ form.hidden_tag() }}
{{ mc.render_field(form.username) }}
{{ mc.render_field(form.password) }}
<div class="form-group text-center">
<input class="btn btn-primary" id="register" name="register" type="submit" value="Registrieren">
</div>
</form>
</div>
<div class="col"></div>
</div>
{% endblock %}
\ No newline at end of file
from flask import Flask, Response
from flask_sqlalchemy import SQLAlchemy
from flask import Flask, Response, request, make_response
from datetime import datetime, timedelta
from server.models import db, GPSWork
from urllib.parse import urlparse, urljoin
from flask import render_template, current_app, Blueprint, redirect, url_for, flash, abort, request
from flask_login import login_user, login_required, logout_user, current_user
from flask_principal import Identity, AnonymousIdentity, identity_changed
from server.models import User, db
from server.forms import LoginForm, PWForm, RegisterForm, DateForm
from datetime import date
from datetime import datetime
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///test.db'
db = SQLAlchemy(app)
def is_safe_url(target):
ref_url = urlparse(request.host_url)
test_url = urlparse(urljoin(request.host_url, target))
return test_url.scheme in ('http', 'https') and \
ref_url.netloc == test_url.netloc
class GPSWork(db.Model):
__tablename__ = "gps_work"
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.String(80), nullable=False)
timestamp = db.Column(db.Integer, unique=True, nullable=False)
def __repr__(self):
return '<GPSWork %r>' % self.id
@app.route('/register', methods= ['GET', 'POST'])
def register():
form = RegisterForm(request.form)
if form.validate_on_submit():
# check if username already exists
user = User.query.filter_by(username=form.username.data).all()
if user:
flash("Der Username ist bereits vergeben.", category="error")
return redirect(url_for('register'))
user = User()
user.username = form.username.data
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
flash("User wurde erfolgreich angelegt.", category="success")
return redirect(url_for('login'))
return render_template('register.html', form=form)
@app.route('/', methods=['POST', 'GET'])
@login_required
def home():
form = DateForm(request.form)
if form.validate_on_submit():
content = get_data(form.begin.data, form.end.data)
return render_template('index.html', form=form, content=content)
return render_template('index.html', form=form)
def get_data(begin, end, format_type='list'):
if format_type not in ['list', 'plain', 'csv']:
return Response("The requested type is not supported.", 400)
user_id = current_user.id
begin = datetime.combine(datetime.strptime(begin, "%d.%m.%Y"), datetime.min.time())
end = datetime.combine(datetime.strptime(end, "%d.%m.%Y"), datetime.max.time())
gps_works = GPSWork.query\
.filter_by(user_id=user_id)\
.filter(GPSWork.datetime >= begin)\
.filter(GPSWork.datetime <= end)\
.order_by(GPSWork.datetime).all()
aggregated_data = aggregat_data(gps_works)
# return plain
if format_type == 'list':
return aggregated_data
if format_type == 'plain':
content = f'User_ID | Start | End \n'
for data in aggregated_data:
print_string = f'{data.get("user_id", "df"): <8}|{data.get("start", "df").strftime("%Y-%m-%d %H:%M:%S")}|{data.get("end", "df").strftime("%Y-%m-%d %H:%M:%S")}\n'
content += print_string
return Response(content, mimetype='text/plain')
# return csv
if format_type == 'csv':
content = f'User_ID;Start;End;\n'
for data in aggregated_data:
print_string = f'{data.get("user_id", "df")};{data.get("start", "df").strftime("%Y-%m-%d %H:%M:%S")};{data.get("end", "df").strftime("%Y-%m-%d %H:%M:%S")}\n'
content += print_string
output = make_response(content)
output.headers["Content-Disposition"] = "attachment; filename=export.csv"
output.headers["Content-type"] = "text/csv"
return output
@app.route("/logout")
@login_required
def logout():
logout_user()
identity_changed.send(
current_app._get_current_object(),
identity=AnonymousIdentity()
)
flash("Sie wurden erfolgreich ausgeloggt.", category="success")
return redirect(url_for('login'))
@app.route('/login', methods=['POST', 'GET'])
def login():
form = LoginForm(request.form)
if request.args.get('next'):
form.next.data=request.args.get('next')
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).one()
login_user(user)
identity_changed.send(
current_app._get_current_object(),
identity=Identity(user.id)
)
flash("Sie wurden erfolgreich eingeloggt.", category="success")
next_ = form.next.data
if not is_safe_url(next_):
return abort(400)
if next_:
return redirect(next_)
else:
return redirect(url_for('home'))
return render_template('login.html', form=form)
@app.route("/<int:user>/<string:date>")
def index(user, date):
gps_work = GPSWork()
gps_work.user_id = user
gps_work.timestamp = date
gps_work.datetime = datetime.utcfromtimestamp(int(date))
db.session.add(gps_work)
db.session.commit()
print("New data added.")
return ""
@app.route("/show_user_data")
@login_required
def show_user_data():
format_type = request.args.get('format_type', 'plain')
if format_type not in ['plain', 'csv']:
return Response("The requested type is not supported.", 400)
user_id = current_user.id
gps_works = GPSWork.query.filter_by(user_id=user_id).order_by(GPSWork.timestamp).all()
aggregated_data = aggregat_data(gps_works)
# return plain
if format_type == 'plain':
content = f'User_ID | Start | End \n'
for data in aggregated_data:
print_string = f'{data.get("user_id", "df"): <8}|{data.get("start", "df").strftime("%Y-%m-%d %H:%M:%S")}|{data.get("end", "df").strftime("%Y-%m-%d %H:%M:%S")}\n'
content += print_string
return Response(content, mimetype='text/plain')
# return csv
if format_type == 'csv':
content = f'User_ID;Start;End;\n'
for data in aggregated_data:
print_string = f'{data.get("user_id", "df")};{data.get("start", "df").strftime("%Y-%m-%d %H:%M:%S")};{data.get("end", "df").strftime("%Y-%m-%d %H:%M:%S")}\n'
content += print_string
output = make_response(content)
output.headers["Content-Disposition"] = "attachment; filename=export.csv"
output.headers["Content-type"] = "text/csv"
return output
@app.route("/show_data/<int:user_id>")
def show_data(user_id):
# print(user_id)
format_type = request.args.get('format_type', 'plain')
if format_type not in ['plain', 'csv']:
return Response("The requested type is not supported.", 400)
gps_works = GPSWork.query.filter_by(user_id=user_id).order_by(GPSWork.timestamp).all()
aggregated_data = aggregat_data(gps_works)
# return plain
if format_type == 'plain':
content = f'User_ID | Start | End \n'
for data in aggregated_data:
print_string = f'{data.get("user_id", "df"): <8}|{data.get("start", "df").strftime("%Y-%m-%d %H:%M:%S")}|{data.get("end", "df").strftime("%Y-%m-%d %H:%M:%S")}\n'
content += print_string
return Response(content, mimetype='text/plain')
# return csv
if format_type == 'csv':
content = f'User_ID;Start;End;\n'
for data in aggregated_data:
print_string = f'{data.get("user_id", "df")};{data.get("start", "df").strftime("%Y-%m-%d %H:%M:%S")};{data.get("end", "df").strftime("%Y-%m-%d %H:%M:%S")}\n'
content += print_string
output = make_response(content)
output.headers["Content-Disposition"] = "attachment; filename=export.csv"
output.headers["Content-type"] = "text/csv"
return output
def aggregat_data(data_list):
aggregated_data = []
aggregated_obj = None
for gps_work in gps_works:
# timestamp is in miliseconds not seconds, so we dived by 1000
tmp = gps_work.timestamp/1000
seconds = tmp % 60
d = datetime.fromtimestamp(tmp - seconds)
for gps_work in data_list:
if not aggregated_obj:
# first entry
aggregated_obj = {'user_id': gps_work.user_id, 'start': d, 'end': d}
elif d - aggregated_obj['end'] <= timedelta(minutes=5):
aggregated_obj['end'] = d
aggregated_obj = {'user_id': gps_work.user_id, 'start': gps_work.datetime, 'end': gps_work.datetime}
elif gps_work.datetime - aggregated_obj['end'] <= timedelta(minutes=5):
aggregated_obj['end'] = gps_work.datetime
else:
aggregated_data.append(aggregated_obj)
aggregated_obj = None
# add last obj
if aggregated_obj:
aggregated_data.append(aggregated_obj)
content = f'User_ID | Start | End \n'
for data in aggregated_data:
print_string = f'{data.get("user_id", "df"): <8}|{data.get("start", "df").strftime("%Y-%m-%d %H:%M:%S")}|{data.get("end", "df").strftime("%Y-%m-%d %H:%M:%S")}<br>\n'
content += print_string
return Response(content, mimetype='text/plain')
return aggregated_data
@app.route("/show_data_raw/<int:user_id>")
def show_raw(user_id):
......