Git Product home page Git Product logo

freezing-web's Introduction

Freezing Saddles Web

This is the web component for the Freezing Saddles (aka BikeArlington Freezing Saddles, "BAFS") Strava-based winter cycling competition software.

NOTE: This application conists of multiple components that work together (designed to run as Docker containers).

  1. freezing-web - The website for viewing leaderboards
  2. freezing-model - A library of shared database and messaging classes.
  3. freezing-sync - The component that syncs ride data from Strava.
  4. freezing-nq - The component that receives webhooks and queues them up for syncing.

Development Setup

Dependencies

  • Python 3.10+ (will not work with python 2.x)
  • Pip
  • Virtualenv (venv)
  • MySQL 5.7

We recommend that for ease of development and debugging, that you install Python 3.10 and pip directly on your workstation. This is tested to work on macOS 14.1.2 (23B92) (Sonoma), on multiple Linux distributions, and on Windows 10. While this will work on Windows 10, most of the advice below relates to running this on a UNIX-like operating system, such as macOS or Ubuntu. Pull requests to improve cross-platform documentation are welcome.

Installation

Here are some instructions for setting up a development environment:

(If you are running in Windows, run env/Scripts/activate instead of source env/bin/activate.)

# Clone repo
shell$ mkdir freezingsaddles && cd freezingsaddles
shell$ for part in sync web compose nq model; do git clone https://github.com/freezingsaddles/freezing-$part.git; done

# Create and activate a virtual environment for freezing-web
shell$ cd freezing-web
shell$ python3 -m venv env
shell$ source env/bin/activate
(env) shell$ pip install -r requirements.txt
(env) shell$ python setup.py develop

We will assume for all subsequent shell examples that you are running in the freezing-web activated virtualenv. (This is denoted by using the "(env) shell$" prefix before shell commands.)

Database Setup

This application requires MySQL, for historical reasons. @hozn wrote:

I know, MySQL is a horrid database, but since I have to host this myself (and my shared hosting provider only supports MySQL), it's what we're doing.

These days, @obscurerichard hosts the production site on AWS, where we have a choice of many more databases, but since it started as MySQL it will probably stay as MySQL unless there's a really good reason to move. Perhaps PostgreSQL and its geospacial integration would be a better choice in the long run. Also, Amazon Aurora is really slick for MySQL-compatible datbase engines, so we are sticking with MySQL for now.

Database Setup using Docker

We have some development support Docker Compose files that can help make database setup simpler, head over to the freezing-compose repo for those instructions.

Manual Database Setup

Install MySQL, version 5.6 or newer. The current production server for https://freezingsaddles.org/ runs MySQL 5.6.

You should create a database and create a user that can access the database. Something like this might work in the default case:

shell$ mysql -uroot
mysql> create database freezing;
mysql> create user freezing@localhost identified by 'REDACTED';
mysql> grant all on freezing.* to freezing@localhost;

Configure and Run Server

Configuration files are shell environment files (or you can use environment variables dirctly).

There is a sample file (example.cfg) that you can reference. You need to set an environment variable called APP_SETTINGS to the path to the file you wish to use.

Here is an example of starting the webserver using settings from a new development.cfg config file:

(env) shell$ cp example.cfg development.cfg
# Edit the file
(env) shell$ APP_SETTINGS=development.cfg freezing-server

Critical things to set include:

  • Database URI
  • Strava Client info (ID and secret), if you want to test registration/authorization/login.
# The SQLALchemy connection URL for your MySQL database.
# NOTE THE CHARSET!
# NOTE: If you are using docker use 127.0.0.1 as the host, NOT localhost
SQLALCHEMY_URL=mysql+pymysql://freezing@localhost/freezing?charset=utf8mb4&binary_prefix=true

# These are issued when you create a Strava application.
# These are really only needed if you want to test app authorization or login features.
STRAVA_CLIENT_ID=xxxx1234
STRAVA_CLIENT_SECRET=5678zzzz

Development setup to work with freezing-model

During development, you may find you need to make changes to the database. Because this suite of projects uses SQLAlchemy and Alembic, and multiple projects depend on the model, it is in a separate git repo.

This an easy pattern to use to make changes to the project freezing-model that this depends on, without having to push tags to the repository. Assuming you have the project checked out in a directory called workspace below your home directory, try this:

  1. cd ~/workspace/freezing-web
  2. python3 -m venv env
  3. source env/bin/activate
  4. cd ~/workspace/freezing-model
  5. pip install -r requirements.txt && python setup.py develop
  6. cd -
  7. pip install -r requirements.txt && python setup.py develop

Now freezing-model is symlinked in, so you can make changes and add migrations to it.

To get freezing-web to permanently use the freezing-model changes you will have to tag the freezing-model repository with a new version number (don't forget to update setup.py also) and update the tag in freezing-web/requirements.txt to match the tag number. It's ok to make a pull request in freezing-model and bump the version after merging master into your branch.

Coding standards

The freezing-web code is intended to be PEP-8 compliant. Code formatting is done with black and can be linted with flake8. See the .flake8 file and install the test dependencies to get these tools (pip install -r test-requirements.txt).

Docker Deployment

See freezing-compose for guide to deploying this in production along with the related containers.

This component is designed to run as a container and should be configured with environment variables for:

  • DEBUG: Whether to display exception stack traces, etc.
  • SECRET_KEY: Used to cryptographically sign the Flask session cookies.
  • SQLALCHEMY_URL: The URL to the database.
  • STRAVA_CLIENT_ID: The ID of the Strava application.
  • STRAVA_CLIENT_SECRET: Secret key for the app (available from App settings page in Strava)
  • TEAMS: A comma-separated list of team (Strava club) IDs for the competition. = env('TEAMS', cast=list, subcast=int, default=[])
  • OBSERVER_TEAMS: Comma-separated list of any teams that are just observing, not playing (they can get their overall stats included, but won't be part of leaderboards)
  • START_DATE: The beginning of the competition.
  • END_DATE: The end of the competition.

Beginning of year procedures

  • Ensure that someone creates a new Strava main group. Usually the person running the sign-up process does this. Search for "Freezing" and you may be surprised to see it has already been created!

  • Get the numeric club ID from the URL of the Strava Recent Activity page for the club.

  • Gain access to the production server via SSH

  • Ensure you have MySQL client access to the production database, either through SSH port forwarding or by running a MySQL client through docker on the production server, or some other means.

  • Make a backup of the database:

    mysqldump > "freezing-$(date +'%Y-%m-%d').sql"

  • Make a backup of the .env file from /opt/compose/.env:

    cp /opt/compose/.env "/opt/compose/.env-$(date +'%Y-%m-%d')"

  • Edit the .env file for the production server (look in /opt/compose/.env) as follows:

    • Update the start and end dates
    • Update the main Strava team id MAIN_TEAM
    • Remove all the teams in TEAMS and OBSERVER_TEAMS
    • Update the competition title COMPETITION_TITLE to reflect the new year
    • Revise any comments to reflect the new year
  • Delete all the data in the following MySQL tables: (see freezing/sql/year-start.sql)

    • teams
    • athletes
    • rides
    • ride_geo
    • ride_weather
  • Insert a new record into the teams table matching the MAIN_TEAM id:

    insert into teams values (567288, 'Freezing Saddles 2020', 1);

  • Restart the services:

    cd /opt/compose && docker-compose up -d

  • Once the teams are announced (for the original Freezing Saddles competition, typically at the Happy Hour in early January):

    • Add the team IDs for the competition teams and any observer teams (ringer teams) into the production .env file

    • Restart the services:

      cd /opt/compose && docker-compose up -d

Athletes will get assigned to their correct teams as soon as they join exactly one of the defined competition teams.

On dumping and restoring the database

It is convenient to dump and restore the database onto a local development environment, and it may be necessary from time to time to restore a database dump in production.

When restoring the database, you should do so as the MySQL root user, or if you don't have access to the real MySQL root user, as the highest privilege user you have access to. Some systems, such as AWS RDS, do not give full MySQL root access but they do have an administrative user.

It would be a good idea to first drop the database, then recreate it along with the freezing user, before restoring the backup.

You may have to edit the resulting SQL dump to redo the SQL SECURITY DEFINER clauses. The examples below do not have the real production root user name in them, observe the error messages from the production dump restoration to get the user name you will need (or ask @obscurerichard in Slack).

/*!50013 DEFINER=`mysql-admin-user`@`%` SQL SECURITY DEFINER */

In this case you could edit the SQL dump to fix up the root user expressions:

# Thanks https://stackoverflow.com/a/23584470/424301
LC_ALL=C sed -i.bak 's/mysql-admin-user/root/g' freezing-2023-11-20.sql

Here is a lightly redacted transcript of a MySQL interactive session, run on a local dev environment, demonstrating how to prepare for restoring a dump:

$ docker run -it --rm --network=host mysql:5.7 mysql --host=127.0.0.1 --port=3306 --user=root --password=REDACTED
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 33
Server version: 5.7.44 MySQL Community Server (GPL)

Copyright (c) 2000, 2023, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> drop database if exists freezing;
Query OK, 33 rows affected (0.29 sec)

mysql> create database freezing;
Query OK, 1 row affected (0.00 sec)

mysql> use freezing;
Database changed

mysql> drop user if exists freezing@localhost;
Query OK, 0 rows affected (0.00 sec)

mysql> create user freezing@localhost identified by 'REDACTED';
Query OK, 0 rows affected (0.00 sec)

mysql>  grant all on freezing.* to freezing@localhost;
Query OK, 0 rows affected, 1 warning (0.00 sec)

mysql> quit
Bye
$ LC_ALL=C sed -i.bak 's/mysql-admin-user/root/g' freezing-2023-11-20.sql
$ time docker run -i --rm --network=host mysql:5.7 mysql --host=127.0.0.1 --port=3306 --user=root --password=REDACTED --database=freezing --default-character-set=utf8mb4 < freezing-2023-11-20.sql
mysql: [Warning] Using a password on the command line interface can be insecure.

real	0m43.612s
user	0m0.510s
sys	0m0.994s
$

Scoring system

The Freezing Saddles o The scoring system heavily weights the early miles of each ride.

• 10 points for each day of 1 mile+ • Additional mileage points as follows: Mile 1=10 points; Mile 2=9 pts; Mile 3=8 pts, etc. Miles 10 and over = 1 pt each. • There is no weekly point cap or distinction between individual and team points. Ride your hearts out!

Scoring Cheat Sheet for the Mathematically Challenged

Miles = Points
1 = 20
2 = 29
3 = 37
4 = 44
5 = 50
6 = 55
7 = 59
8 = 62
9 = 64
10 = 65
11 = 66
12 = 67
13 = 68
14 = 69
15 = 70
16 = 71
17 = 72
18 = 73
19 = 74
20 = 75

The scores are rounded to the nearest integer point for display, but the system uses precise floating point calculations of points to determine rank. This can lead to some counterintuitive results at first glance, such as a whole-number points tie with the person in the lead having fewer miles recorded.

In 2024, this happened as of Jan 7 between Paul Wilson and Steve Szibler. Check out this detail

mysql> select a.name, ds.distance, ds.points, ds.ride_date from daily_scores ds inner join athletes a on (ds.athlete_id = a.id) where a.name  like 'Steve S%' or name like 'Paul Wilson' order by name, ride_date;
+-------------------+--------------------+--------------------+------------+
| name              | distance           | points             | ride_date  |
+-------------------+--------------------+--------------------+------------+
| Paul Wilson       | 30.233999252319336 |  85.23399925231934 | 2024-01-01 |
| Paul Wilson       | 32.055999755859375 |  87.05599975585938 | 2024-01-02 |
| Paul Wilson       | 35.689998626708984 |  90.68999862670898 | 2024-01-03 |
| Paul Wilson       | 33.128000259399414 |  88.12800025939941 | 2024-01-04 |
| Paul Wilson       |  36.27000045776367 |  91.27000045776367 | 2024-01-05 |
| Paul Wilson       |   35.4640007019043 |   90.4640007019043 | 2024-01-06 |
| Paul Wilson       |  40.28300094604492 |  95.28300094604492 | 2024-01-07 |
| Steve Szibler🕊     |  85.37000274658203 | 140.37000274658203 | 2024-01-01 |
| Steve Szibler🕊     |  31.36400079727173 |  86.36400079727173 | 2024-01-02 |
| Steve Szibler🕊     | 21.209999084472656 |  76.20999908447266 | 2024-01-03 |
| Steve Szibler🕊     |  40.33599853515625 |  95.33599853515625 | 2024-01-04 |
| Steve Szibler🕊     | 20.131000638008118 |  75.13100063800812 | 2024-01-05 |
| Steve Szibler🕊     |  40.17300033569336 |  95.17300033569336 | 2024-01-06 |
| Steve Szibler🕊     |  7.122000217437744 | 59.419558734504676 | 2024-01-07 |
+-------------------+--------------------+--------------------+------------+
14 rows in set (0.01 sec)

mysql> select a.name, sum(ds.distance), sum(ds.points) from daily_scores ds inner join athletes a on (ds.athlete_id = a.id) where a.name  like 'Steve S%' or name like 'Paul Wilson' group by name order by sum(ds.points) desc;
+-------------------+-------------------+-------------------+
| name              | sum(ds.distance)  | sum(ds.points)    |
+-------------------+-------------------+-------------------+
| Paul Wilson       |           243.125 |           628.125 |
| Steve Szibler🕊     | 245.7060023546219 | 628.0035608716888 |
+-------------------+-------------------+-------------------+
2 rows in set (0.02 sec)

Legal

This software is a community-driven effort, and as such the contributions are owned by the individual contributors:

Copyright 2015 Ian Will
Copyright 2019 Hans Lillelid
Copyright 2020 Jon Renaut
Copyright 2020 Merlin Hughes
Copyright 2020 Richard Bullington-McGuire
Copyright 2020 Adrian Porter
Copyright 2020 Joe Tatsuko

This software is licensed under the Apache 2.0 license, with some marked portions available under compatible licenses (such as the [MIT-licensed test/wget-spider.sh].)

freezing-web's People

Contributors

arichnad avatar dependabot[bot] avatar hozn avatar iancw avatar joetats avatar jrenaut avatar merlinorg avatar obscurerichard avatar spfrantz avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

freezing-web's Issues

Add Leaderboard for FS2019coffeechallenge

Chuck and Karen Pena | Chuck and Karen Pena | Dec 14, 2018, 5:06 PM (4 days ago)

 

Can you please confirm that you'll be be able to do my Points Per Mile and Coffee Challenge pointless prizes again this year? Points Per Mile was on the Pointless Leaderboard so I'm assuming it can just carryover without having to do anything. My Coffee Challenge will require being able to tally rides with #FS2019coffeechallenge (case sensitive) in the ride title.

Photo gallery css on homepage needs attention

Seems like we're getting lots of different-sized images (portrait?) than originally designed for and it looks pretty wonky.

Just needs some CSS cleanup. If someone wants to redesign the photos stuff, that would be great too.

Better calendar integration.

The calendar is pretty ugly; unfortunately, there's not a ton we can do about that.

I think that on the main page we should just show a narrow list of upcoming events (agenda mode?) if possible. Then we can have a dedicated page that shows the full calendar.

python3 bug in weather sync

Traceback (most recent call last):
  File "/home/freezingsaddles/sites/freezingsaddles.com/env/bin/bafs-sync-weather", line 11, in <module>
    load_entry_point('bafs', 'console_scripts', 'bafs-sync-weather')()
  File "/home/freezingsaddles/sites/freezingsaddles.com/env/lib/python3.6/site-packages/pkg_resources/__init__.py", line 565, in load_entry_point
    return get_distribution(dist).load_entry_point(group, name)
  File "/home/freezingsaddles/sites/freezingsaddles.com/env/lib/python3.6/site-packages/pkg_resources/__init__.py", line 2631, in load_entry_point
    return ep.load()
  File "/home/freezingsaddles/sites/freezingsaddles.com/env/lib/python3.6/site-packages/pkg_resources/__init__.py", line 2291, in load
    return self.resolve()
  File "/home/freezingsaddles/sites/freezingsaddles.com/env/lib/python3.6/site-packages/pkg_resources/__init__.py", line 2297, in resolve
    module = __import__(self.module_name, fromlist=['__name__'], level=0)
  File "/home/freezingsaddles/sites/freezingsaddles.com/freezingsaddles/bafs/scripts/sync_ride_weather.py", line 9, in <module>
    from weather.sunrise import Sun
  File "/home/freezingsaddles/sites/freezingsaddles.com/freezingsaddles/weather/sunrise.py", line 129
    print s.sunrise(when)
          ^
SyntaxError: invalid syntax

Geoalchemy assertion errors

Perhaps this is happening when we get null spatial points?

Traceback (most recent call last):
  File "/home/freezingsaddles/sites/freezingsaddles.com/freezingsaddles/bafs/scripts/sync_rides.py", line 117, in _write_rides
    ride = data.write_ride(strava_activity)
  File "/home/freezingsaddles/sites/freezingsaddles.com/freezingsaddles/bafs/data.py", line 293, in write_ride
    db.session.merge(ride_geo)
  File "/home/freezingsaddles/sites/freezingsaddles.com/env/lib/python3.6/site-packages/sqlalchemy/orm/scoping.py", line 149, in do
    return getattr(self.registry(), name)(*args, **kwargs)
  File "/home/freezingsaddles/sites/freezingsaddles.com/env/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 1519, in merge
    load=load, _recursive=_recursive)
  File "/home/freezingsaddles/sites/freezingsaddles.com/env/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 1558, in _merge
    merged = self.query(mapper.class_).get(key[1])
  File "/home/freezingsaddles/sites/freezingsaddles.com/env/lib/python3.6/site-packages/sqlalchemy/orm/query.py", line 820, in get
    return loading.load_on_ident(self, key)
  File "/home/freezingsaddles/sites/freezingsaddles.com/env/lib/python3.6/site-packages/sqlalchemy/orm/loading.py", line 226, in load_on_ident
    return q.one()
  File "/home/freezingsaddles/sites/freezingsaddles.com/env/lib/python3.6/site-packages/sqlalchemy/orm/query.py", line 2311, in one
    ret = list(self)
  File "/home/freezingsaddles/sites/freezingsaddles.com/env/lib/python3.6/site-packages/sqlalchemy/orm/loading.py", line 72, in instances
    rows = [process[0](row, None) for row in fetch]
  File "/home/freezingsaddles/sites/freezingsaddles.com/env/lib/python3.6/site-packages/sqlalchemy/orm/loading.py", line 72, in <listcomp>
    rows = [process[0](row, None) for row in fetch]
  File "/home/freezingsaddles/sites/freezingsaddles.com/env/lib/python3.6/site-packages/sqlalchemy/orm/loading.py", line 447, in _instance
    populate_state(state, dict_, row, isnew, only_load_props)
  File "/home/freezingsaddles/sites/freezingsaddles.com/env/lib/python3.6/site-packages/sqlalchemy/orm/loading.py", line 301, in populate_state
    populator(state, dict_, row)
  File "/home/freezingsaddles/sites/freezingsaddles.com/env/lib/python3.6/site-packages/sqlalchemy/orm/strategies.py", line 151, in fetch_col
    dict_[key] = row[col]
  File "/home/freezingsaddles/sites/freezingsaddles.com/env/lib/python3.6/site-packages/sqlalchemy/engine/result.py", line 89, in __getitem__
    return processor(self._row[index])
  File "/home/freezingsaddles/sites/freezingsaddles.com/env/src/geoalchemy/geoalchemy/geometry.py", line 27, in process
    return DialectManager.get_spatial_dialect(dialect).process_result(value, self)
  File "/home/freezingsaddles/sites/freezingsaddles.com/env/src/geoalchemy/geoalchemy/mysql.py", line 123, in process_result
    return MySQLPersistentSpatialElement(WKBSpatialElement(value, type.srid))
  File "/home/freezingsaddles/sites/freezingsaddles.com/env/src/geoalchemy/geoalchemy/base.py", line 90, in __init__
    assert isinstance(desc, str)
AssertionError

Ride days needs to correct for time zone

I agree that the ride should count because of the time difference, etc. But... how can he have 4 days ridden so far when it is January 3rd? Something else seems to be off.

I don't know if I wrote that piece, but I believe the logic is truncating to the date value, which would count as 4 days -- i.e. It isn't calculating a time delta. I think we need to tweak that metric to take the truncated contest dates also into consideration.

Edit: the ride start dates just need to be converted to EST5EDT before the date is truncated.

SQLAlchemy/Geoalchemy errors for binary geometry col data

Getting lots of these warnings on weather sync. They're not preventing the sync, but it would be nice to fix them.

/home/freezingsaddles/sites/freezingsaddles.com/env/lib/python3.6/site-packages/sqlalchemy/engine/default.py:326: Warning: (1300, "Invalid utf8mb4 character string: 'B6300B'")
  cursor.execute(statement, parameters)

This is a MySQL error, so I'm not exactly sure why we're getting this when reading data. This may be because sqlalchemy is trying to apply some sort of casting to the results. Probably need to turn on full query logging to find out what query is triggering this warning.

This could be related here:

Edit: This reported issue nylas/sync-engine#207 suggests that this might actually be a MySQL bug. Perhaps we just need to squelch the warnings as they did in nylas/sync-engine@ea4a9ec

Store GPS tracks for rides, expose as GeoJSON

This ticket describes what we'll call "v1" where we simply have a set of coordinates (a LineString specifically) for the GPS track for all rides [that have GPS data].

(In future versions we'll store other dimensions.)

Plan is to (1) start storing the data and (2) expose an API that returns this data -- probably as a GeometryCollection with options for filtering by date range and team_id.

With regard to privacy we will:

  1. Not store tracks for rides marked private.
  2. Not directly associate tracks with athletes. (We will be associating them with teams, though.)

If the FS application was authorized (by the checkbox) to view private data, then presumably the ride tracks we store will include data inside of any privacy zones. Thus our desire not to link routes directly to athletes.

Add an "about"

Some one brought up freezingsaddles in the DCTech Slack #cycling channel, and none of us seem to actually know what freezingsaddles is. Various thoughts are:

  • it's a strava club and then a web app that uses the strava API and a weather API to build a leader board of people who rode on sub freezing days
  • it's just a Strava group (they split you out into teams) that pledges to bike commute to work between certain dates -- don't recall it having anything to do with the actual weather, just "ride your bike to/from work 2 Jan - 1 March" or something similar
  • I could be wrong, but I think you get bonus points if it’s snowing or something? Or maybe that was when your team rides in a group

So.. a nice quick summary of what this actually does on the homepage would be great!

Add support for Highcharts charts

Highcharts looks aesthetically more pleasing at least. Probably we could abstract out the actual charting functions and have a number of different renderers, including the current Google Charts.

Highcharts CDN is http so some browsers are blocking

The Highcharts CDN is http-only so now that we're using https, some browsers (Chrome for sure, probably others) are blocking the js files, meaning charts don't work.

Is this just a matter of adding the js file to the static/js directory and updating links in templates, or is there additional static file handling that needs to happen?

Better error handling for bogus trainer rides

This is actually a bogus trainer ride, but we probably want to also handle the exception from wunderground a little smarter?

Is there a way to detect the bogus trainer rides and exclude them?

ERROR:sync-weather:Error getting weather data for ride: <Ride id=238200342 name=u'01/07/2015 United States Minor Outlying Islands'>
Traceback (most recent call last):
  File "/home/hozn/sites/freezingsaddles.com/bafs/bafs/scripts.py", line 313, in sync_ride_weather
    rw.ride_temp_start = start_obs.temp
AttributeError: 'NoneType' object has no attribute 'temp'

SA / mysqlclient string vs. bytes issue

This is going to require some stepping-through / debugging, since the error isn't providing much of a hint about what here is bytes vs. string.

Traceback (most recent call last):
  File "/home/freezingsaddles/sites/freezingsaddles.com/freezingsaddles/bafs/scripts/sync_activity_detail.py", line 206, in execute
    db.session.commit()
  File "/home/freezingsaddles/sites/freezingsaddles.com/env/lib/python3.6/site-packages/sqlalchemy/orm/scoping.py", line 149, in do
    return getattr(self.registry(), name)(*args, **kwargs)
  File "/home/freezingsaddles/sites/freezingsaddles.com/env/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 721, in commit
    self.transaction.commit()
  File "/home/freezingsaddles/sites/freezingsaddles.com/env/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 354, in commit
    self._prepare_impl()
  File "/home/freezingsaddles/sites/freezingsaddles.com/env/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 334, in _prepare_impl
    self.session.flush()
  File "/home/freezingsaddles/sites/freezingsaddles.com/env/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 1818, in flush
    self._flush(objects)
  File "/home/freezingsaddles/sites/freezingsaddles.com/env/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 1936, in _flush
    transaction.rollback(_capture_exception=True)
  File "/home/freezingsaddles/sites/freezingsaddles.com/env/lib/python3.6/site-packages/sqlalchemy/util/langhelpers.py", line 59, in __exit__
    compat.reraise(exc_type, exc_value, exc_tb)
  File "/home/freezingsaddles/sites/freezingsaddles.com/env/lib/python3.6/site-packages/sqlalchemy/util/compat.py", line 184, in reraise
    raise value
  File "/home/freezingsaddles/sites/freezingsaddles.com/env/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 1900, in _flush
    flush_context.execute()
  File "/home/freezingsaddles/sites/freezingsaddles.com/env/lib/python3.6/site-packages/sqlalchemy/orm/unitofwork.py", line 372, in execute
    rec.execute(self)
  File "/home/freezingsaddles/sites/freezingsaddles.com/env/lib/python3.6/site-packages/sqlalchemy/orm/unitofwork.py", line 525, in execute
    uow
  File "/home/freezingsaddles/sites/freezingsaddles.com/env/lib/python3.6/site-packages/sqlalchemy/orm/persistence.py", line 64, in save_obj
    table, insert)
  File "/home/freezingsaddles/sites/freezingsaddles.com/env/lib/python3.6/site-packages/sqlalchemy/orm/persistence.py", line 541, in _emit_insert_statements
    execute(statement, multiparams)
  File "/home/freezingsaddles/sites/freezingsaddles.com/env/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 662, in execute
    params)
  File "/home/freezingsaddles/sites/freezingsaddles.com/env/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 761, in _execute_clauseelement
    compiled_sql, distilled_params
  File "/home/freezingsaddles/sites/freezingsaddles.com/env/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 874, in _execute_context
    context)
  File "/home/freezingsaddles/sites/freezingsaddles.com/env/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1027, in _handle_dbapi_exception
    util.reraise(*exc_info)
  File "/home/freezingsaddles/sites/freezingsaddles.com/env/lib/python3.6/site-packages/sqlalchemy/util/compat.py", line 184, in reraise
    raise value
  File "/home/freezingsaddles/sites/freezingsaddles.com/env/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 856, in _execute_context
    context)
  File "/home/freezingsaddles/sites/freezingsaddles.com/env/lib/python3.6/site-packages/sqlalchemy/connectors/mysqldb.py", line 60, in do_executemany
    rowcount = cursor.executemany(statement, parameters)
  File "/home/freezingsaddles/sites/freezingsaddles.com/env/lib/python3.6/site-packages/MySQLdb/cursors.py", line 273, in executemany
    m = RE_INSERT_VALUES.match(query)
TypeError: cannot use a string pattern on a bytes-like object

Need to handle transaction rollback/commit more explicitly

Right now it appears that errors in the web framework put db connection into an invalid state. We need to ensure that we are handling exceptions in the framework and rolling back transaction.

(Likely this was being handled by flask-sqlalchemy plugin.)

Store photo URLs rather than caching locally.

Caching the photos was probably the dumbest idea of fs2015 development. We don't need to cache them; we do need to lookup the actual (Instagram) photo URLs for both thumbnail-size and large, but we just store those and we're done.

Optimize and smooth strava requests to avoid exceeding rate limits.

  • Review the need for a --rewrite option on ride sync. (Check key attribs and queue for full-rewrite only if needed?)
  • Consolidate the logic that operates on a full activity ("detail" level) fetch.
  • Remove unnecessary fetching of photos (only fetch photos if ride has photos also try to get the easy wins in the activity detail sync stage).
  • Use modulus to spread out detail ride processing. Add bookkeeping for ride tracking (detail_fetched col?). E.g. if we run a script hourly, use activity_id % 24 == 0 to figure out if we should resync it.
  • Finally, add bookkeeping to athlete table to track last sync and rewrite times. Display this in the UI, as it might preempt some support questions.

Handle RateLimitExceeded errors

We should immediately stop our sync tasks if/when we hit RateLimitExceeded. (Maybe this is already true, but needs to be confirmed.) Additionally, we should probably ensure our cron is setup so that we don't incur the error again at next run. E.g. maybe move ride sync to 5 minutes.

What we really need is to implement the webhook API.

Traceback (most recent call last):
  File "/home/freezingsaddles/sites/freezingsaddles.com/env/bin/bafs-sync", line 11, in <module>
    load_entry_point('bafs', 'console_scripts', 'bafs-sync')()
  File "/home/freezingsaddles/sites/freezingsaddles.com/freezingsaddles/bafs/scripts/sync_rides.py", line 177, in main
    SyncRides().run()
  File "/home/freezingsaddles/sites/freezingsaddles.com/freezingsaddles/bafs/scripts/__init__.py", line 105, in run
    self.execute(options, args)
  File "/home/freezingsaddles/sites/freezingsaddles.com/freezingsaddles/bafs/scripts/sync_rides.py", line 81, in execute
    self._write_rides(start, end_date, athlete=athlete, rewrite=options.rewrite)
  File "/home/freezingsaddles/sites/freezingsaddles.com/freezingsaddles/bafs/scripts/sync_rides.py", line 94, in _write_rides
    exclude_keywords=app.config.get('BAFS_EXCLUDE_KEYWORDS'))
  File "/home/freezingsaddles/sites/freezingsaddles.com/freezingsaddles/bafs/data.py", line 228, in list_rides
    filtered_rides = [a for a in activities if
  File "/home/freezingsaddles/sites/freezingsaddles.com/freezingsaddles/bafs/data.py", line 228, in <listcomp>
    filtered_rides = [a for a in activities if
  File "/home/freezingsaddles/sites/freezingsaddles.com/stravalib/stravalib/client.py", line 1587, in __next__
    return self.next()
  File "/home/freezingsaddles/sites/freezingsaddles.com/stravalib/stravalib/client.py", line 1593, in next
    self._fill_buffer()
  File "/home/freezingsaddles/sites/freezingsaddles.com/stravalib/stravalib/client.py", line 1564, in _fill_buffer
    raw_results = self.result_fetcher(page=self._page, per_page=self.per_page)
  File "/home/freezingsaddles/sites/freezingsaddles.com/stravalib/stravalib/protocol.py", line 241, in get
    return self._request(url, params=params, check_for_errors=check_for_errors, use_webhook_server=use_webhook_server)
  File "/home/freezingsaddles/sites/freezingsaddles.com/stravalib/stravalib/protocol.py", line 171, in _request
    self.rate_limiter(raw.headers)
  File "/home/freezingsaddles/sites/freezingsaddles.com/stravalib/stravalib/util/limiter.py", line 235, in __call__
    r(args)
  File "/home/freezingsaddles/sites/freezingsaddles.com/stravalib/stravalib/util/limiter.py", line 96, in __call__
    self._check_limit_rates(limit)
  File "/home/freezingsaddles/sites/freezingsaddles.com/stravalib/stravalib/util/limiter.py", line 109, in _check_limit_rates
    self._raise_rate_limit_exception(limit['limit'], limit['time'])
  File "/home/freezingsaddles/sites/freezingsaddles.com/stravalib/stravalib/util/limiter.py", line 123, in _raise_rate_limit_exception
    .format(limit_rate, timeout))
stravalib.exc.RateLimitExceeded: Rate limit of 900 exceeded. Try again in 600 seconds.

Incorrect encoding issue for names

Not sure how this has happened again, but seeing this in athlete's sync:

/home/freezingsaddles/sites/freezingsaddles.com/env/lib/python3.6/site-packages/sqlalchemy/engine/default.py:326: Warning: (1366, "Incorrect string value: '\\xF0\\x9F\\x87\\xB5\\xF0\\x9F...' for column 'name' at row 1")

Looks like something needs to be correctly UTF-8 encoded or decoded. (Our mysql client lib should be using utf-8, so not exactly sure how this is happening.)

Leaderboard for #donut

Hi, I would like a table to see how many rides, and riders, claimed Doughnut Rides.
Thanks, Kate

Leaderboard for In the beginning was the deed

Ideally I would like people to get points equal to the number of times they have ridden the segment that they have ridden the fewest times (IE if there were three segments, A, B, and C, and someone has ridden A and B 5 times, but ridden C 2 times, there score would be 2)

Here is the complete list of segments for this prize.

The Deed 7 (WB on Washington Blvd)
N. Arlington to N. Illinois
New Bike Lane on Wilson Blvd.
BikeArlington Commuter Counter (New, downhill)
Library Sprint - if you're lucky
The Deed 8 (NB Eads)
The Deed 9 (NB Eads)
The Deed 10 (Hayes NB)
The Deed 11 (Hayes SB)
The Deed 12(18th EB)
The Deed 13(18th WB)
The Deed 14(Crystal NB)
The Deed 15(Crystal SB)
The Deed
The Deed 4 (King Street Buffered SB)
 The Deed 2
The Deed 5 (SB Bolshevik Lane)
 The Deed 6 Prince Street
Cam'ron Dash
The Deed 3
 King to Ft Ward Hillcrest
The Deed 18 (G Street)
The Deed 19 (Eye Street)
The Deed 16 (L Street)
 The Deed 17 (M Street)

Thanks for your assistance
Ken

Leaderboard for #midnight, #midnight 2-fer

For the PaulRevere Midnight Rider Pointless Prize, I would like to see a leaderboard to see how many riders have claimed this distinction using the #midnight and its variations.
Thanks, Kate

Differences (rounding?) between a user's points on individual leaderboard (text vs chart)

Right now for the top 3, I'm seeing this:

User|Points (Text)|Points (Chart)
--------------------------------
Josh A|111|110
David H|92|91
Bob J|91|91

The point value from the text version is from the hover/tool-tip. Sometimes they're equal, sometimes they're not. Haven't checked to see if there's any users with an absolute value of the difference greater than 1.

Eliminate redundant code for register_athlete_team

The code in the freezing/web/data.py function register_athlete_team is derived from the code in freezingsaddles/freezing-sync/freezing/sync/data/athlete.py and has to be maintained in parallel right now. Figure out a way to extract the functionality and share it between the two.

Maybe this could move into freezing-model's orm class for Athlete.
Maybe it could live in a shared library both of these projects share.

This may also be related to #188 - related code calls this.

Rider with 3 rides from 12/31

Getting some complaints that one rider has rides that are clearly from 12/31. Can I just delete them from the database, or are they just going to get pulled back in? Looking at the import script it doesn't look like they'd get selected.

Design generic pointless leaderboard system

Current copy/paste approach to the pointless prize leaderboards is a bit silly.

Some of the leaderboards are based on different values and numbers, but they can all be distilled down into tables with some common columns:

  • Athlete
  • Rank
  • "Score" (some number and units)

The backend queries can coerce everything into this basic structure.

Bonus points would be storing the queries themselves into the database so that we wouldn't have to change the application when we add pointless prizes.

Edit (from comment below): rather than store these in the database, I think it would be even better to put these into YAML files. We'd probably want a basic caching mechanism, though, to avoid re-parsing the yaml every time someone hit the leaderboard page.

---
key: dirtybiker
title: Dirty Biker Leaderboard
description: (Rules ...)
sql: |
  select athlete_name, rank, ponts from ....  order by rank asc;

Cap points per week per rider to 170 (100 + 10 for each 1+ mile day) for team ride calculations

The rules for Freezing Saddles 2019 are changing to make the competition emphasize regular riding more than riding more miles, for the team standings only. We will change the rules as follows (as originally suggested by Steve O., Henry Dunbar, and company):

  • 10 points for each day of 1 mile+ (same as before)
  • 1 point per mile (same as before)
  • Team contribution per individual capped at 100 miles per week
    (individual rankings would remain the same - no cap on the individual scoreboard)

The maximum someone could contribute to their team is 170 points per week. If they miss a day, then 160. This helps teams that ride more days and keeps a couple of superhumans from carrying entire teams. In fact, it would probably make the contributions of players 4-7 much more valuable, since the top few players on each team would all max out, negating their advantage.

Let's use the Strava standard week of Monday - Sunday as the week, so it is easy for people to look at Strava to see how many miles they have for the week. Yes, this means we will have short weeks at the beginning and end of the competition, and we will keep the 100 point mileage cap per person for those weeks, so people can ride more those days and have it reflected in the team standings.

Database Version

From your README.md, you state this app needs the MySQL database. What version of MySQL are you testing with?

Thanks,

Bob

Track related activities

As requested by @jrenaut
https://strava.github.io/api/v3/activities/#get-related

Questions

  • Do we only want to track activities that are in our rides table (i.e. by other freezing saddles participants) or do we track everything? (I think we probably track everything, though that means we lose on referential integrity to rides table...)
  • How do we manage re-syncing this data? This is expensive (API call per ride), but this data also is gonna change (as others in the group post their rides later, etc.). Not sure on best strategy to refresh this information.

Let competitors queue a resync of a ride

We have a photo resync now, but a full ride resync would be nice. This should just push into beanstalkd, probably.

Probably needs a new worker queue endpoint.

Authorizing with Strava should queue this single athlete resync too so that people don't need to wait as long for their rides to show after they first sign up.

Implement db view / leaderboard for #fs2018dirtybiker

Here are the rules for this prize: http://bikearlingtonforum.com/showthread.php?12874-fs2018dirtybiker-Pointless-Prize-Mountain-for-Dirt-Miles-for-Freezing-Saddles-2018

Basically, it's just like the other pointless prizes, but there is a max of 1 point per day.

See the views/pointless.py for lots of examples of other pointless prize queries and the templates/pointeless/*.html for examples of how these turn into views. That's pretty much it! (You might need to also make a link to your new leaderboard in the navigation.)

Pop up bubble for logged-in user on distance/speed/elevation bubble chart

Thanks to Chris ...

"This javascript pops a particular username to the top (in a vaguely hack-y, but workable way)."

var username = 'jdoe';
var circle = $( "text[aria-hidden='true']:contains(" + username + ")" ).parent();
circle.parent().append(circle.prev());
circle.parent().append(circle);

Decrease lat/lon resolution for weather data to take advantage of caching

Proposal was to decrease the precision/scale of the lat/lon points, to "snap" folks to a grid.

This methodology/implementation was elaborated by SolaBikeCar on the forum:

That's what I'd do as well. Round every geo-point to 1 decimal place. Most of the metro area riders would be in the 36 geo blocks between (38.6,-78.4) and 39.2,-76.8). Each of the blocks would be roughly 5 square miles. People outside the area would cluster around a few more geo-block areas. If you took last years data you might find you have only 100 or so unique values once you rounded every geocode down to 1 decimal of precision. I'd then give everyone in that geo-block the weather as returned by the API for the center of the block (i.e Lat+.05,Lon+.05). This method would adjust to the minimum number of geo-blocks needed each day and could vary as players travel to far-off places and returned.

So the idea would be to normalize lat/lon for the rides before fetching the weather data (or checking whether it had already been cached -- which is quite likely in this model!).

Invalid utf8 (?) in athlete name(s)

This is annoying as I'm getting emailed with these errors/warnings every time we do an athlete sync:

/home/freezingsaddles/sites/freezingsaddles.com/env/lib/python3.6/site-packages/sqlalchemy/engine/default.py:326: Warning: (1366, "Incorrect string value: '\\xF0\\x9F\\x87\\xB5\\xF0\\x9F...' for column 'name' at row 1")
  cursor.execute(statement, parameters)

I am fairly confident that we're using utf8mb4 for the database. It's possible that people are using emoji that truly is not utf8? We should check this first in python probably. If it's not valid utf8, we should just remove or replace (e.g. with '_'?) the offending characters. I suspect MySQL is just silently truncating the data for us, as that's how it likes to approach potential errors.

Make a read-only web db user

There are a couple of exceptions where we want to be able to write to the database from the web app:

  • Strava authorization
  • Manually requesting resync of ride data (e.g. photos)

Otherwise, everything should be read-only. I think this is a pre-req for #42

Create a new "leaderboards" page to display all the leaderboards

Right now there's no page that lists all the leaderboards. This would be useful so we can keep the menus only showing the key (or official) leaderboard and a link to view the others.

We probably wan to break this into sections like:

  • Official
  • Pointless Prizes (will change every year)
  • Alternative Scoring (trying out new ideas)
  • Data Exploration (just for fun) -- though this could remain its own top-level section too.

Track commute, build reporting views/queries

Shawn says:
I was hoping to award a pointless prize based on the ratio of 'commute' rides to total rides for the duration of the competition. Is that data available? I've seen the 'commute' banner on strava rides recently and was hoping that info was included in data that could be pulled down.

So i guess i'd just need two numbers for each rider:

total number of rides
total number of rides flagged with 'commute'

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.