<commit_before>#!/usr/bin/env python
# vim: set fileencoding=utf-8 :

from flask import Flask, render_template, flash, redirect, request, url_for, session, Response, json
from flask_bootstrap import Bootstrap
from flask_login import login_user, current_user, login_required, logout_user
from model import db, Move, Sample, MoveEdit
from datetime import timedelta, datetime
from sqlalchemy.sql import func
from sqlalchemy import distinct, literal
import os
from flask_bcrypt import Bcrypt
import imports
import gpx_export
import csv_export
import dateutil.parser
from flask.helpers import make_response
from flask_script import Manager, Server
from flask_migrate import Migrate, MigrateCommand
from commands import AddUser, ImportMove, DeleteMove, ListMoves
from filters import register_filters, register_globals, radian_to_degree, get_city
from login import login_manager, load_user, LoginForm
import itertools
from collections import OrderedDict
from flask_util_js import FlaskUtilJs
from _import import postprocess_move
from geopy.distance import vincenty
import math
import operator
import pytz
from monthdelta import monthdelta
from jinja2.exceptions import TemplateNotFound
try:
    from urllib.parse import quote_plus
except ImportError:
    from urllib import quote_plus as __quote_plus
    quote_plus = lambda x: __quote_plus(x.encode('utf-8'))


app = Flask('openmoves')
fujs = FlaskUtilJs(app)

app_bcrypt = Bcrypt()
migrate = Migrate(app, db)

register_filters(app)
register_globals(app)


def initialize_config(f):

    random_bytes = os.urandom(32)

    if isinstance(random_bytes[0], str):
        random_bytes = [ord(c) for c in random_bytes]

    data = "SECRET_KEY = '%s'\n" % "".join("{:02x}".format(c) for c in random_bytes)
    f.write(data)


def _sample_to_point(sample):
    return (radian_to_degree(sample.latitude), radian_to_degree(sample.longitude))


def _get_date_range():
    timezone = pytz.timezone(session['timezone'])
    now = timezone.localize(datetime.now())

    if 'start_date' in request.args:
        start_date = request.args.get('start_date')
        start_date = dateutil.parser.parse(start_date)
        start_date = start_date.date()
    else:
        start_date = now.date() - monthdelta(months=1)

    if 'end_date' in request.args:
        end_date = request.args.get('end_date')
        end_date = dateutil.parser.parse(end_date)
        end_date = end_date.date()
    else:
        end_date = now.date()

    return start_date, end_date


def calculate_distances(model, samples):
    total_distance_horizontal = 0.0
    total_distance_real = 0.0
    total_distance_descent = 0.0
    total_distance_ascent = 0.0
    total_distance_flat = 0.0
    previous_gps_sample = None
    current_altitude_sample = None
    previous_altitude_sample = None

    for sample in samples:
        if sample.altitude:
            current_altitude_sample = sample
        if sample.latitude:
            if previous_gps_sample:
                distance_horizontal = vincenty(_sample_to_point(previous_gps_sample), _sample_to_point(sample)).meters
                if previous_altitude_sample:
                    if current_altitude_sample != previous_altitude_sample and distance_horizontal > 0:
                        total_distance_horizontal += distance_horizontal
                        hm = current_altitude_sample.altitude - previous_altitude_sample.altitude
                        distance_real = math.sqrt(distance_horizontal ** 2 + hm ** 2)

                        if hm > 0:
                            total_distance_ascent += distance_real
                        elif hm < 0:
                            total_distance_descent += distance_real
                        else:
                            total_distance_flat += distance_real

                        total_distance_real += distance_real
                        previous_gps_sample = sample
                        previous_altitude_sample = current_altitude_sample
                else:
                    previous_altitude_sample = current_altitude_sample
            else:
                previous_gps_sample = sample

    model['total_distance_horizontal'] = total_distance_horizontal
    model['total_distance_ascent'] = total_distance_ascent
    model['total_distance_descent'] = total_distance_descent
    model['total_distance_flat'] = total_distance_flat
    model['total_distance_real'] = total_distance_real


def init(configfile):
    app.config.from_pyfile('openmoves.cfg.default', silent=False)
    if configfile:
        if not os.path.exists(configfile):
            with open(configfile, 'w') as f:
                initialize_config(f)
            print("created %s" % configfile)

        app.config.from_pyfile(configfile, silent=False)
        assert app.config['SECRET_KEY']

        SESSION_VERSION = 1
        app.config['SECRET_KEY'] = "%s-%d" % (app.config['SECRET_KEY'], SESSION_VERSION)

    db.init_app(app)

    with app.app_context():
        if db.engine.name == 'sqlite':
            db.create_all()

    Bootstrap(app)
    app_bcrypt.init_app(app)

    login_manager.init_app(app)
    login_manager.login_view = "login"

    return app


def command_app_context():
    app.config.update(SQLALCHEMY_ECHO=False)
    return app.app_context()


manager = Manager(init)

manager.add_option('-c', '--config', dest='configfile', default='openmoves.cfg', required=False)

manager.add_command("runserver", Server(use_debugger=True))
manager.add_command('db', MigrateCommand)
manager.add_command('add-user', AddUser(command_app_context, app_bcrypt))
manager.add_command('import-move', ImportMove(command_app_context))
manager.add_command('delete-move', DeleteMove(command_app_context))
manager.add_command('list-moves', ListMoves(command_app_context))


@app.errorhandler(404)
def error404(error):
    return render_template('_404.html'), 404


@app.route('/import', methods=['GET', 'POST'])
@login_required
def move_import():
    xmlfiles = request.files.getlist('files')
    imported_moves = []

    for xmlfile in xmlfiles:
        filename = xmlfile.filename
        if filename:
            app.logger.info("importing '%s'" % filename)
            move = imports.move_import(xmlfile, filename, current_user)
            if move:
                imported_moves.append(move)

    if imported_moves:
        if len(imported_moves) == 1:
            move = imported_moves[0]
            flash("imported '%s': move %d" % (xmlfile.filename, move.id))
            return redirect(url_for('move', id=move.id))
        else:
            flash("imported %d moves" % len(imported_moves))
            return redirect(url_for('moves'))
    else:
        return render_template('import.html')


@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        user = load_user(username=form.username.data)
        if not user:
            flash('no such user', 'error')
            return render_template('login.html', form=form)

        if app_bcrypt.check_password_hash(user.password, form.password.data):
            login_user(user)

            assert 'timezone' in request.form
            pytz.timezone(request.form['timezone'])
            session['timezone'] = request.form['timezone']

            if 'next' in request.args:
                return redirect(request.args.get('next'))

            return redirect(url_for('dashboard'))
        else:
            flash("login failed", 'error')
            return render_template('login.html', form=form)

    return render_template('login.html', form=form)


@app.route('/logout')
@login_required
def logout():
    logout_user()
    return redirect(url_for('index'))


@app.route('/')
def index():
    nr_of_moves = Move.query.count()
    return render_template('index.html', nr_of_moves=nr_of_moves)


@app.route('/dashboard')
@login_required
def dashboard():

    start_date, end_date = _get_date_range()

    moves = _current_user_filtered(Move.query).filter(Move.date_time >= start_date) \
                                              .filter(Move.date_time < end_date + timedelta(days=1)) \
                                              .all()

    model = {}
    model['start_date'] = start_date
    model['end_date'] = end_date
    model['nr_of_moves'] = len(moves)

    total_distance_by_activity = {}
    total_duration_by_activity = {}
    total_average_by_activity = {}
    total_ascent_by_activity = {}
    total_descent_by_activity = {}

    for move in moves:
        if move.activity not in total_distance_by_activity:
            total_distance_by_activity[move.activity] = 0
        total_distance_by_activity[move.activity] += move.distance

        if move.activity not in total_duration_by_activity:
            total_duration_by_activity[move.activity] = 0
        total_duration_by_activity[move.activity] += move.duration.total_seconds()

        if move.activity not in total_ascent_by_activity:
            total_ascent_by_activity[move.activity] = 0
        if move.ascent:
            total_ascent_by_activity[move.activity] += move.ascent

        if move.activity not in total_descent_by_activity:
            total_descent_by_activity[move.activity] = 0
        if move.ascent:
            total_descent_by_activity[move.activity] += move.descent

    # Clean activities without distance or duration
    for activity in list(total_distance_by_activity.keys()):
        if(total_distance_by_activity[activity] == 0):
            del total_distance_by_activity[activity]
    for activity in list(total_duration_by_activity.keys()):
        if(total_duration_by_activity[activity] == timedelta(0)):
            del total_duration_by_activity[activity]
    for activity in list(total_ascent_by_activity.keys()):
        if(total_ascent_by_activity[activity] == 0):
            del total_ascent_by_activity[activity]
    for activity in list(total_descent_by_activity.keys()):
        if(total_descent_by_activity[activity] == 0):
            del total_descent_by_activity[activity]

    # Calculate average speeds
    total_duration_with_distance = 0
    for activity in total_distance_by_activity.keys():
        if activity not in total_duration_by_activity:
            continue
        total_average_by_activity[activity] = total_distance_by_activity[activity] / total_duration_by_activity[activity]
        total_duration_with_distance += total_duration_by_activity[activity]

    # Sort totals by activity
    model['total_distance_by_activity'] = OrderedDict(sorted(total_distance_by_activity.items(), key=operator.itemgetter(1), reverse=True))
    model['total_duration_by_activity'] = OrderedDict(sorted(total_duration_by_activity.items(), key=operator.itemgetter(1), reverse=True))
    model['total_average_by_activity'] = OrderedDict(sorted(total_average_by_activity.items(), key=operator.itemgetter(1), reverse=True))
    model['total_ascent_by_activity'] = OrderedDict(sorted(total_ascent_by_activity.items(), key=operator.itemgetter(1), reverse=True))
    model['total_descent_by_activity'] = OrderedDict(sorted(total_descent_by_activity.items(), key=operator.itemgetter(1), reverse=True))

    # Calculate totals
    model['total_distance'] = sum(total_distance_by_activity.values())
    model['total_duration'] = sum(total_duration_by_activity.values(), 0)
    model['total_average'] = model['total_distance'] / total_duration_with_distance if total_duration_with_distance > 0 else None
    model['total_ascent'] = sum(total_ascent_by_activity.values())
    model['total_descent'] = sum(total_descent_by_activity.values())

    return render_template('dashboard.html', **model)


def _parse_move_filter(filter_query):
    if not filter_query:
        return None

    filter_parts = [part.strip() for part in filter_query.split(':')]
    if len(filter_parts) != 2 or filter_parts[0] not in ('activity'):
        flash("illegal filter: '%s'" % filter_query, 'error')
        return None
    else:
        filter_attr = filter_parts[0]
        filter_value = filter_parts[1]
        return {filter_attr: filter_value}


def _current_user_filtered(query):
    return query.filter_by(user=current_user)


@app.route('/moves')
@login_required
def moves():
    start_date, end_date = _get_date_range()
    filter_end_date = end_date + timedelta(days=1)

    moves = _current_user_filtered(Move.query).filter(Move.date_time >= start_date) \
                                              .filter(Move.date_time < filter_end_date)

    total_moves_count = moves.count()
    move_filter = _parse_move_filter(request.args.get('filter'))
    if move_filter:
        moves = moves.filter_by(**move_filter)

    sort = request.args.get('sort')
    sort_order = request.args.get('sort_order')
    sort_default = 'date_time'
    if not sort:
        sort = sort_default
        sort_order = 'desc'
    if not sort_order:
        sort_order = 'asc'

    if not hasattr(Move, sort):
        flash("illegal sort field: %s" % sort, 'error')
        sort = sort_default

    activity_counts = OrderedDict(_current_user_filtered(db.session.query(Move.activity, func.count(Move.id)))
                                  .filter(Move.date_time >= start_date)
                                  .filter(Move.date_time < filter_end_date)
                                  .group_by(Move.activity)
                                  .order_by(func.count(Move.id).desc()))

    actual_activities_query = _current_user_filtered(db.session.query(distinct(Move.activity)))
    if move_filter:
        actual_activities_query = actual_activities_query.filter_by(**move_filter)
    actual_activities = set([activity for activity, in actual_activities_query])

    sort_attr = getattr(Move, sort)
    if not sort_order or sort_order == 'asc':
        sort_attr = sort_attr.asc()
    else:
        sort_attr = sort_attr.desc()

    if db.engine.name == "postgresql":
        sort_attr = sort_attr.nullslast()

    show_columns = {}
    for column in ('location_address', 'speed_avg', 'speed_max', 'hr_avg', 'ascent', 'descent', 'recovery_time', 'stroke_count', 'pool_length'):
        attr = getattr(Move, column)
        base_query = _current_user_filtered(db.session.query(attr).filter(attr != None))
        if move_filter:
            base_query = base_query.filter_by(**move_filter)
        exists_query = db.session.query(literal(True)).filter(base_query.exists())
        show_columns[column] = exists_query.scalar()

    show_columns['activity'] = not move_filter or 'activity' not in move_filter

    moves = moves.order_by(sort_attr)
    return render_template('moves.html',
                           start_date=start_date,
                           end_date=end_date,
                           moves=moves,
                           total_moves_count=total_moves_count,
                           activity_counts=activity_counts,
                           actual_activities=actual_activities,
                           show_columns=show_columns,
                           sort=sort,
                           sort_order=sort_order)


@app.route('/moves/<int:id>/delete')
@login_required
def delete_move(id):
    move = _current_user_filtered(Move.query).filter_by(id=id).first_or_404()<commit_msg>fix date range filter for "show_columns" exists queries<commit_after>#!/usr/bin/env python
# vim: set fileencoding=utf-8 :

from flask import Flask, render_template, flash, redirect, request, url_for, session, Response, json
from flask_bootstrap import Bootstrap
from flask_login import login_user, current_user, login_required, logout_user
from model import db, Move, Sample, MoveEdit
from datetime import timedelta, datetime
from sqlalchemy.sql import func
from sqlalchemy import distinct, literal
import os
from flask_bcrypt import Bcrypt
import imports
import gpx_export
import csv_export
import dateutil.parser
from flask.helpers import make_response
from flask_script import Manager, Server
from flask_migrate import Migrate, MigrateCommand
from commands import AddUser, ImportMove, DeleteMove, ListMoves
from filters import register_filters, register_globals, radian_to_degree, get_city
from login import login_manager, load_user, LoginForm
import itertools
from collections import OrderedDict
from flask_util_js import FlaskUtilJs
from _import import postprocess_move
from geopy.distance import vincenty
import math
import operator
import pytz
from monthdelta import monthdelta
from jinja2.exceptions import TemplateNotFound
try:
    from urllib.parse import quote_plus
except ImportError:
    from urllib import quote_plus as __quote_plus
    quote_plus = lambda x: __quote_plus(x.encode('utf-8'))


app = Flask('openmoves')
fujs = FlaskUtilJs(app)

app_bcrypt = Bcrypt()
migrate = Migrate(app, db)

register_filters(app)
register_globals(app)


def initialize_config(f):

    random_bytes = os.urandom(32)

    if isinstance(random_bytes[0], str):
        random_bytes = [ord(c) for c in random_bytes]

    data = "SECRET_KEY = '%s'\n" % "".join("{:02x}".format(c) for c in random_bytes)
    f.write(data)


def _sample_to_point(sample):
    return (radian_to_degree(sample.latitude), radian_to_degree(sample.longitude))


def _get_date_range():
    timezone = pytz.timezone(session['timezone'])
    now = timezone.localize(datetime.now())

    if 'start_date' in request.args:
        start_date = request.args.get('start_date')
        start_date = dateutil.parser.parse(start_date)
        start_date = start_date.date()
    else:
        start_date = now.date() - monthdelta(months=1)

    if 'end_date' in request.args:
        end_date = request.args.get('end_date')
        end_date = dateutil.parser.parse(end_date)
        end_date = end_date.date()
    else:
        end_date = now.date()

    return start_date, end_date


def calculate_distances(model, samples):
    total_distance_horizontal = 0.0
    total_distance_real = 0.0
    total_distance_descent = 0.0
    total_distance_ascent = 0.0
    total_distance_flat = 0.0
    previous_gps_sample = None
    current_altitude_sample = None
    previous_altitude_sample = None

    for sample in samples:
        if sample.altitude:
            current_altitude_sample = sample
        if sample.latitude:
            if previous_gps_sample:
                distance_horizontal = vincenty(_sample_to_point(previous_gps_sample), _sample_to_point(sample)).meters
                if previous_altitude_sample:
                    if current_altitude_sample != previous_altitude_sample and distance_horizontal > 0:
                        total_distance_horizontal += distance_horizontal
                        hm = current_altitude_sample.altitude - previous_altitude_sample.altitude
                        distance_real = math.sqrt(distance_horizontal ** 2 + hm ** 2)

                        if hm > 0:
                            total_distance_ascent += distance_real
                        elif hm < 0:
                            total_distance_descent += distance_real
                        else:
                            total_distance_flat += distance_real

                        total_distance_real += distance_real
                        previous_gps_sample = sample
                        previous_altitude_sample = current_altitude_sample
                else:
                    previous_altitude_sample = current_altitude_sample
            else:
                previous_gps_sample = sample

    model['total_distance_horizontal'] = total_distance_horizontal
    model['total_distance_ascent'] = total_distance_ascent
    model['total_distance_descent'] = total_distance_descent
    model['total_distance_flat'] = total_distance_flat
    model['total_distance_real'] = total_distance_real


def init(configfile):
    app.config.from_pyfile('openmoves.cfg.default', silent=False)
    if configfile:
        if not os.path.exists(configfile):
            with open(configfile, 'w') as f:
                initialize_config(f)
            print("created %s" % configfile)

        app.config.from_pyfile(configfile, silent=False)
        assert app.config['SECRET_KEY']

        SESSION_VERSION = 1
        app.config['SECRET_KEY'] = "%s-%d" % (app.config['SECRET_KEY'], SESSION_VERSION)

    db.init_app(app)

    with app.app_context():
        if db.engine.name == 'sqlite':
            db.create_all()

    Bootstrap(app)
    app_bcrypt.init_app(app)

    login_manager.init_app(app)
    login_manager.login_view = "login"

    return app


def command_app_context():
    app.config.update(SQLALCHEMY_ECHO=False)
    return app.app_context()


manager = Manager(init)

manager.add_option('-c', '--config', dest='configfile', default='openmoves.cfg', required=False)

manager.add_command("runserver", Server(use_debugger=True))
manager.add_command('db', MigrateCommand)
manager.add_command('add-user', AddUser(command_app_context, app_bcrypt))
manager.add_command('import-move', ImportMove(command_app_context))
manager.add_command('delete-move', DeleteMove(command_app_context))
manager.add_command('list-moves', ListMoves(command_app_context))


@app.errorhandler(404)
def error404(error):
    return render_template('_404.html'), 404


@app.route('/import', methods=['GET', 'POST'])
@login_required
def move_import():
    xmlfiles = request.files.getlist('files')
    imported_moves = []

    for xmlfile in xmlfiles:
        filename = xmlfile.filename
        if filename:
            app.logger.info("importing '%s'" % filename)
            move = imports.move_import(xmlfile, filename, current_user)
            if move:
                imported_moves.append(move)

    if imported_moves:
        if len(imported_moves) == 1:
            move = imported_moves[0]
            flash("imported '%s': move %d" % (xmlfile.filename, move.id))
            return redirect(url_for('move', id=move.id))
        else:
            flash("imported %d moves" % len(imported_moves))
            return redirect(url_for('moves'))
    else:
        return render_template('import.html')


@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        user = load_user(username=form.username.data)
        if not user:
            flash('no such user', 'error')
            return render_template('login.html', form=form)

        if app_bcrypt.check_password_hash(user.password, form.password.data):
            login_user(user)

            assert 'timezone' in request.form
            pytz.timezone(request.form['timezone'])
            session['timezone'] = request.form['timezone']

            if 'next' in request.args:
                return redirect(request.args.get('next'))

            return redirect(url_for('dashboard'))
        else:
            flash("login failed", 'error')
            return render_template('login.html', form=form)

    return render_template('login.html', form=form)


@app.route('/logout')
@login_required
def logout():
    logout_user()
    return redirect(url_for('index'))


@app.route('/')
def index():
    nr_of_moves = Move.query.count()
    return render_template('index.html', nr_of_moves=nr_of_moves)


@app.route('/dashboard')
@login_required
def dashboard():

    start_date, end_date = _get_date_range()

    moves = _current_user_filtered(Move.query).filter(Move.date_time >= start_date) \
                                              .filter(Move.date_time < end_date + timedelta(days=1)) \
                                              .all()

    model = {}
    model['start_date'] = start_date
    model['end_date'] = end_date
    model['nr_of_moves'] = len(moves)

    total_distance_by_activity = {}
    total_duration_by_activity = {}
    total_average_by_activity = {}
    total_ascent_by_activity = {}
    total_descent_by_activity = {}

    for move in moves:
        if move.activity not in total_distance_by_activity:
            total_distance_by_activity[move.activity] = 0
        total_distance_by_activity[move.activity] += move.distance

        if move.activity not in total_duration_by_activity:
            total_duration_by_activity[move.activity] = 0
        total_duration_by_activity[move.activity] += move.duration.total_seconds()

        if move.activity not in total_ascent_by_activity:
            total_ascent_by_activity[move.activity] = 0
        if move.ascent:
            total_ascent_by_activity[move.activity] += move.ascent

        if move.activity not in total_descent_by_activity:
            total_descent_by_activity[move.activity] = 0
        if move.ascent:
            total_descent_by_activity[move.activity] += move.descent

    # Clean activities without distance or duration
    for activity in list(total_distance_by_activity.keys()):
        if(total_distance_by_activity[activity] == 0):
            del total_distance_by_activity[activity]
    for activity in list(total_duration_by_activity.keys()):
        if(total_duration_by_activity[activity] == timedelta(0)):
            del total_duration_by_activity[activity]
    for activity in list(total_ascent_by_activity.keys()):
        if(total_ascent_by_activity[activity] == 0):
            del total_ascent_by_activity[activity]
    for activity in list(total_descent_by_activity.keys()):
        if(total_descent_by_activity[activity] == 0):
            del total_descent_by_activity[activity]

    # Calculate average speeds
    total_duration_with_distance = 0
    for activity in total_distance_by_activity.keys():
        if activity not in total_duration_by_activity:
            continue
        total_average_by_activity[activity] = total_distance_by_activity[activity] / total_duration_by_activity[activity]
        total_duration_with_distance += total_duration_by_activity[activity]

    # Sort totals by activity
    model['total_distance_by_activity'] = OrderedDict(sorted(total_distance_by_activity.items(), key=operator.itemgetter(1), reverse=True))
    model['total_duration_by_activity'] = OrderedDict(sorted(total_duration_by_activity.items(), key=operator.itemgetter(1), reverse=True))
    model['total_average_by_activity'] = OrderedDict(sorted(total_average_by_activity.items(), key=operator.itemgetter(1), reverse=True))
    model['total_ascent_by_activity'] = OrderedDict(sorted(total_ascent_by_activity.items(), key=operator.itemgetter(1), reverse=True))
    model['total_descent_by_activity'] = OrderedDict(sorted(total_descent_by_activity.items(), key=operator.itemgetter(1), reverse=True))

    # Calculate totals
    model['total_distance'] = sum(total_distance_by_activity.values())
    model['total_duration'] = sum(total_duration_by_activity.values(), 0)
    model['total_average'] = model['total_distance'] / total_duration_with_distance if total_duration_with_distance > 0 else None
    model['total_ascent'] = sum(total_ascent_by_activity.values())
    model['total_descent'] = sum(total_descent_by_activity.values())

    return render_template('dashboard.html', **model)


def _parse_move_filter(filter_query):
    if not filter_query:
        return None

    filter_parts = [part.strip() for part in filter_query.split(':')]
    if len(filter_parts) != 2 or filter_parts[0] not in ('activity'):
        flash("illegal filter: '%s'" % filter_query, 'error')
        return None
    else:
        filter_attr = filter_parts[0]
        filter_value = filter_parts[1]
        return {filter_attr: filter_value}


def _current_user_filtered(query):
    return query.filter_by(user=current_user)


@app.route('/moves')
@login_required
def moves():
    start_date, end_date = _get_date_range()
    filter_end_date = end_date + timedelta(days=1)

    moves = _current_user_filtered(Move.query).filter(Move.date_time >= start_date) \
                                              .filter(Move.date_time < filter_end_date)

    total_moves_count = moves.count()
    move_filter = _parse_move_filter(request.args.get('filter'))
    if move_filter:
        moves = moves.filter_by(**move_filter)

    sort = request.args.get('sort')
    sort_order = request.args.get('sort_order')
    sort_default = 'date_time'
    if not sort:
        sort = sort_default
        sort_order = 'desc'
    if not sort_order:
        sort_order = 'asc'

    if not hasattr(Move, sort):
        flash("illegal sort field: %s" % sort, 'error')
        sort = sort_default

    activity_counts = OrderedDict(_current_user_filtered(db.session.query(Move.activity, func.count(Move.id)))
                                  .filter(Move.date_time >= start_date)
                                  .filter(Move.date_time < filter_end_date)
                                  .group_by(Move.activity)
                                  .order_by(func.count(Move.id).desc()))

    actual_activities_query = _current_user_filtered(db.session.query(distinct(Move.activity)))
    if move_filter:
        actual_activities_query = actual_activities_query.filter_by(**move_filter)
    actual_activities = set([activity for activity, in actual_activities_query])

    sort_attr = getattr(Move, sort)
    if not sort_order or sort_order == 'asc':
        sort_attr = sort_attr.asc()
    else:
        sort_attr = sort_attr.desc()

    if db.engine.name == "postgresql":
        sort_attr = sort_attr.nullslast()

    show_columns = {}
    for column in ('location_address', 'speed_avg', 'speed_max', 'hr_avg', 'ascent', 'descent', 'recovery_time', 'stroke_count', 'pool_length'):
        attr = getattr(Move, column)
        base_query = _current_user_filtered(db.session.query(attr).filter(attr != None)
                                                                  .filter(Move.date_time >= start_date)
                                                                  .filter(Move.date_time < filter_end_date))
        if move_filter:
            base_query = base_query.filter_by(**move_filter)
        exists_query = db.session.query(literal(True)).filter(base_query.exists())
        show_columns[column] = exists_query.scalar()

    show_columns['activity'] = not move_filter or 'activity' not in move_filter

    moves = moves.order_by(sort_attr)
    return render_template('moves.html',
                           start_date=start_date,
                           end_date=end_date,
                           moves=moves,
                           total_moves_count=total_moves_count,
                           activity_counts=activity_counts,
                           actual_activities=actual_activities,
                           show_columns=show_columns,
                           sort=sort,
                           sort_order=sort_order)


@app.route('/moves/<int:id>/delete')
@login_required
def delete_move(id):
    move = _current_user_filtered(Move.query).filter_by(id=id).first_or_404()
{'fraction_non_alphanumeric': 0.09866796621182586, 'fraction_numerical': 0.004158544509421702, 'mean_word_length': 3.0437467157120337, 'pattern_counts': {'":': 2, '<': 14, '<?xml version=': 0, '>': 18, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 12}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 24, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '706193', 'n_tokens_mistral': 9328, 'n_tokens_neox': 9011, 'n_words': 1728}